@tishlang/tish-lsp 2.16.13 → 2.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tish-lsp +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +12 -2
- package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
- package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
- package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
- package/crates/tish/tests/fs_parity.rs +103 -0
- package/crates/tish/tests/shortcircuit.rs +3 -3
- package/crates/tish_ast/src/ast.rs +77 -3
- package/crates/tish_builtins/Cargo.toml +1 -0
- package/crates/tish_builtins/src/array.rs +195 -16
- package/crates/tish_builtins/src/collections.rs +37 -2
- package/crates/tish_builtins/src/globals.rs +215 -25
- package/crates/tish_builtins/src/math.rs +65 -7
- package/crates/tish_builtins/src/number.rs +173 -0
- package/crates/tish_builtins/src/string.rs +168 -15
- package/crates/tish_bytecode/src/compiler.rs +56 -4
- package/crates/tish_bytecode/src/opcode.rs +4 -2
- package/crates/tish_compile/src/codegen.rs +513 -44
- package/crates/tish_compile/src/infer.rs +26 -11
- package/crates/tish_compile/src/resolve.rs +87 -10
- package/crates/tish_compile_js/src/codegen.rs +82 -24
- package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +17 -2
- package/crates/tish_core/src/json.rs +5 -1
- package/crates/tish_core/src/lib.rs +48 -2
- package/crates/tish_core/src/uri.rs +45 -0
- package/crates/tish_core/src/value.rs +6 -0
- package/crates/tish_eval/Cargo.toml +2 -1
- package/crates/tish_eval/src/eval.rs +655 -27
- package/crates/tish_eval/src/natives.rs +264 -18
- package/crates/tish_eval/src/regex.rs +47 -1
- package/crates/tish_fmt/src/lib.rs +23 -4
- package/crates/tish_lsp/Cargo.toml +1 -1
- package/crates/tish_opt/src/lib.rs +20 -1
- package/crates/tish_parser/src/lib.rs +41 -0
- package/crates/tish_parser/src/parser.rs +180 -11
- package/crates/tish_resolve/src/lib.rs +66 -5
- package/crates/tish_runtime/Cargo.toml +5 -1
- package/crates/tish_runtime/src/fs_ext.rs +359 -0
- package/crates/tish_runtime/src/http_hyper.rs +41 -18
- package/crates/tish_runtime/src/lib.rs +128 -9
- package/crates/tish_ui/src/jsx.rs +18 -3
- package/crates/tish_vm/src/jit.rs +1 -1
- package/crates/tish_vm/src/vm.rs +270 -60
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-lsp +0 -0
- package/platform/darwin-x64/tish-lsp +0 -0
- package/platform/linux-arm64/tish-lsp +0 -0
- package/platform/linux-x64/tish-lsp +0 -0
- package/platform/win32-x64/tish-lsp.exe +0 -0
|
@@ -317,6 +317,59 @@ pub fn trim(s: &Value) -> Value {
|
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
pub fn trim_start(s: &Value) -> Value {
|
|
321
|
+
if let Value::String(s) = s {
|
|
322
|
+
Value::String(s.trim_start().into())
|
|
323
|
+
} else {
|
|
324
|
+
Value::Null
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/// Pure `String.prototype.normalize` core (ECMA-262 §22.1.3.13). `form` is one of NFC/NFD/NFKC/NFKD;
|
|
329
|
+
/// returns `None` for an unrecognized form so the caller can surface a `RangeError`. Shared by every
|
|
330
|
+
/// backend so interp/vm/native stay byte-identical.
|
|
331
|
+
pub fn normalize_form(s: &str, form: &str) -> Option<String> {
|
|
332
|
+
use unicode_normalization::UnicodeNormalization;
|
|
333
|
+
match form {
|
|
334
|
+
"NFC" => Some(s.nfc().collect()),
|
|
335
|
+
"NFD" => Some(s.nfd().collect()),
|
|
336
|
+
"NFKC" => Some(s.nfkc().collect()),
|
|
337
|
+
"NFKD" => Some(s.nfkd().collect()),
|
|
338
|
+
_ => None,
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/// `String.prototype.normalize(form?)` — core-`Value` entry (vm/native). An omitted form defaults to
|
|
343
|
+
/// NFC; an invalid form parks a catchable `RangeError`.
|
|
344
|
+
pub fn normalize(s: &Value, form: &Value) -> Value {
|
|
345
|
+
let input = match s {
|
|
346
|
+
Value::String(s) => s.as_ref(),
|
|
347
|
+
_ => return Value::Null,
|
|
348
|
+
};
|
|
349
|
+
let f: String = match form {
|
|
350
|
+
Value::Null => "NFC".to_string(),
|
|
351
|
+
Value::String(f) => f.to_string(),
|
|
352
|
+
v => v.to_display_string(),
|
|
353
|
+
};
|
|
354
|
+
match normalize_form(input, &f) {
|
|
355
|
+
Some(out) => Value::String(out.into()),
|
|
356
|
+
None => {
|
|
357
|
+
tishlang_core::set_pending_throw(tishlang_core::range_error(
|
|
358
|
+
"The normalization form should be one of NFC, NFD, NFKC, NFKD.",
|
|
359
|
+
));
|
|
360
|
+
Value::Null
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
pub fn trim_end(s: &Value) -> Value {
|
|
366
|
+
if let Value::String(s) = s {
|
|
367
|
+
Value::String(s.trim_end().into())
|
|
368
|
+
} else {
|
|
369
|
+
Value::Null
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
320
373
|
pub fn to_upper_case(s: &Value) -> Value {
|
|
321
374
|
if let Value::String(s) = s {
|
|
322
375
|
Value::String(s.to_uppercase().into())
|
|
@@ -333,22 +386,74 @@ pub fn to_lower_case(s: &Value) -> Value {
|
|
|
333
386
|
}
|
|
334
387
|
}
|
|
335
388
|
|
|
336
|
-
pub fn starts_with(s: &Value, search: &Value) -> Value {
|
|
389
|
+
pub fn starts_with(s: &Value, search: &Value, position: Option<&Value>) -> Value {
|
|
337
390
|
if let (Value::String(s), Value::String(search)) = (s, search) {
|
|
338
|
-
|
|
391
|
+
// `position`: test as if the string began at char `position` (clamped to [0, len]). Absent → 0.
|
|
392
|
+
let pos = match position {
|
|
393
|
+
Some(Value::Number(n)) if *n > 0.0 => *n as usize,
|
|
394
|
+
_ => 0,
|
|
395
|
+
};
|
|
396
|
+
let byte_start = s.char_indices().nth(pos).map(|(b, _)| b).unwrap_or(s.len());
|
|
397
|
+
Value::Bool(s[byte_start..].starts_with(search.as_str()))
|
|
339
398
|
} else {
|
|
340
399
|
Value::Bool(false)
|
|
341
400
|
}
|
|
342
401
|
}
|
|
343
402
|
|
|
344
|
-
pub fn ends_with(s: &Value, search: &Value) -> Value {
|
|
403
|
+
pub fn ends_with(s: &Value, search: &Value, end_position: Option<&Value>) -> Value {
|
|
345
404
|
if let (Value::String(s), Value::String(search)) = (s, search) {
|
|
346
|
-
|
|
405
|
+
// `endPosition`: test as if the string ended at char `endPosition` (clamped to [0, len]).
|
|
406
|
+
// Absent → the full length.
|
|
407
|
+
let char_count = s.chars().count();
|
|
408
|
+
let end = match end_position {
|
|
409
|
+
Some(Value::Number(n)) if *n >= 0.0 => (*n as usize).min(char_count),
|
|
410
|
+
Some(Value::Number(_)) => 0,
|
|
411
|
+
_ => char_count,
|
|
412
|
+
};
|
|
413
|
+
let byte_end = s.char_indices().nth(end).map(|(b, _)| b).unwrap_or(s.len());
|
|
414
|
+
Value::Bool(s[..byte_end].ends_with(search.as_str()))
|
|
347
415
|
} else {
|
|
348
416
|
Value::Bool(false)
|
|
349
417
|
}
|
|
350
418
|
}
|
|
351
419
|
|
|
420
|
+
/// Expand a string replacement's `$` patterns for one match: `$$`→`$`, `$&`→the match, `` $` ``→the
|
|
421
|
+
/// text before it, `$'`→the text after it. Any other `$x` is literal. (String-search `replace`; the
|
|
422
|
+
/// numbered `$1` captures only apply to regex replacements.)
|
|
423
|
+
pub fn expand_replacement(repl: &str, matched: &str, before: &str, after: &str) -> String {
|
|
424
|
+
if !repl.contains('$') {
|
|
425
|
+
return repl.to_string();
|
|
426
|
+
}
|
|
427
|
+
let mut out = String::with_capacity(repl.len());
|
|
428
|
+
let mut chars = repl.chars().peekable();
|
|
429
|
+
while let Some(c) = chars.next() {
|
|
430
|
+
if c == '$' {
|
|
431
|
+
match chars.peek() {
|
|
432
|
+
Some('$') => {
|
|
433
|
+
out.push('$');
|
|
434
|
+
chars.next();
|
|
435
|
+
}
|
|
436
|
+
Some('&') => {
|
|
437
|
+
out.push_str(matched);
|
|
438
|
+
chars.next();
|
|
439
|
+
}
|
|
440
|
+
Some('`') => {
|
|
441
|
+
out.push_str(before);
|
|
442
|
+
chars.next();
|
|
443
|
+
}
|
|
444
|
+
Some('\'') => {
|
|
445
|
+
out.push_str(after);
|
|
446
|
+
chars.next();
|
|
447
|
+
}
|
|
448
|
+
_ => out.push('$'),
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
out.push(c);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
out
|
|
455
|
+
}
|
|
456
|
+
|
|
352
457
|
fn replace_impl(s: &Value, search: &Value, replacement: &Value, all: bool) -> Value {
|
|
353
458
|
if let Value::String(s) = s {
|
|
354
459
|
let search_str = match search {
|
|
@@ -359,12 +464,37 @@ fn replace_impl(s: &Value, search: &Value, replacement: &Value, all: bool) -> Va
|
|
|
359
464
|
Value::String(ss) => ss.as_str(),
|
|
360
465
|
_ => "",
|
|
361
466
|
};
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
467
|
+
// Fast path: no `$` in the replacement (and non-empty search) → the library replace.
|
|
468
|
+
if !repl_str.contains('$') || search_str.is_empty() {
|
|
469
|
+
let result = if all {
|
|
470
|
+
s.replace(search_str, repl_str)
|
|
471
|
+
} else {
|
|
472
|
+
s.replacen(search_str, repl_str, 1)
|
|
473
|
+
};
|
|
474
|
+
return Value::String(result.into());
|
|
475
|
+
}
|
|
476
|
+
// `$`-expansion path: expand per match against the original string's before/after context.
|
|
477
|
+
let sref = s.as_str();
|
|
478
|
+
let mut out = String::with_capacity(sref.len());
|
|
479
|
+
let mut last = 0usize;
|
|
480
|
+
let mut start = 0usize;
|
|
481
|
+
while let Some(pos) = sref[start..].find(search_str) {
|
|
482
|
+
let m = start + pos;
|
|
483
|
+
out.push_str(&sref[last..m]);
|
|
484
|
+
out.push_str(&expand_replacement(
|
|
485
|
+
repl_str,
|
|
486
|
+
search_str,
|
|
487
|
+
&sref[..m],
|
|
488
|
+
&sref[m + search_str.len()..],
|
|
489
|
+
));
|
|
490
|
+
last = m + search_str.len();
|
|
491
|
+
start = last;
|
|
492
|
+
if !all {
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
out.push_str(&sref[last..]);
|
|
497
|
+
Value::String(out.into())
|
|
368
498
|
} else {
|
|
369
499
|
Value::Null
|
|
370
500
|
}
|
|
@@ -507,6 +637,23 @@ pub fn char_code_at(s: &Value, idx: &Value) -> Value {
|
|
|
507
637
|
}
|
|
508
638
|
}
|
|
509
639
|
|
|
640
|
+
/// `codePointAt(i)` — the Unicode code point at char index `i`, or `null` (JS `undefined`) if out of
|
|
641
|
+
/// range. tish strings are code-point sequences, so this returns the same value as `charCodeAt` for
|
|
642
|
+
/// an in-range index, but reads `null` (not `NaN`) past the end.
|
|
643
|
+
pub fn code_point_at(s: &Value, idx: &Value) -> Value {
|
|
644
|
+
if let Value::String(s) = s {
|
|
645
|
+
let idx = match idx {
|
|
646
|
+
Value::Number(n) if *n >= 0.0 => *n as usize,
|
|
647
|
+
_ => return Value::Null,
|
|
648
|
+
};
|
|
649
|
+
char_at_idx(s, idx)
|
|
650
|
+
.map(|c| Value::Number(c as u32 as f64))
|
|
651
|
+
.unwrap_or(Value::Null)
|
|
652
|
+
} else {
|
|
653
|
+
Value::Null
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
510
657
|
pub fn repeat(s: &Value, count: &Value) -> Value {
|
|
511
658
|
if let Value::String(s) = s {
|
|
512
659
|
let count = match count {
|
|
@@ -525,12 +672,15 @@ fn pad_impl(s: &Value, target_len: &Value, pad: &Value, at_start: bool) -> Value
|
|
|
525
672
|
Value::Number(n) => *n as usize,
|
|
526
673
|
_ => return Value::String(s.clone()),
|
|
527
674
|
};
|
|
675
|
+
// An *explicit* empty fill string means "no padding" (spec: `padStart(n, "")` → original,
|
|
676
|
+
// unchanged); only an ABSENT pad arg (`Value::Null`) defaults to a space. The old
|
|
677
|
+
// `if !p.is_empty()` guard conflated the two, space-padding on an explicit `""`.
|
|
528
678
|
let pad_str = match pad {
|
|
529
|
-
Value::String(p)
|
|
679
|
+
Value::String(p) => p.as_str(),
|
|
530
680
|
_ => " ",
|
|
531
681
|
};
|
|
532
682
|
let char_count = s.chars().count();
|
|
533
|
-
if char_count >= target_len {
|
|
683
|
+
if char_count >= target_len || pad_str.is_empty() {
|
|
534
684
|
return Value::String(s.clone());
|
|
535
685
|
}
|
|
536
686
|
let needed = target_len - char_count;
|
|
@@ -696,9 +846,12 @@ mod tests {
|
|
|
696
846
|
fn case_and_prefix_suffix() {
|
|
697
847
|
assert_same!(to_upper_case(&s("aB")), s("AB"));
|
|
698
848
|
assert_same!(to_lower_case(&s("aB")), s("ab"));
|
|
699
|
-
assert_same!(starts_with(&s("/api"), &s("/api")), Value::Bool(true));
|
|
700
|
-
assert_same!(ends_with(&s("x.js"), &s(".js")), Value::Bool(true));
|
|
701
|
-
assert_same!(starts_with(&n(1.0), &s("")), Value::Bool(false));
|
|
849
|
+
assert_same!(starts_with(&s("/api"), &s("/api"), None), Value::Bool(true));
|
|
850
|
+
assert_same!(ends_with(&s("x.js"), &s(".js"), None), Value::Bool(true));
|
|
851
|
+
assert_same!(starts_with(&n(1.0), &s(""), None), Value::Bool(false));
|
|
852
|
+
// 2nd-arg: position / endPosition.
|
|
853
|
+
assert_same!(starts_with(&s("abc"), &s("bc"), Some(&n(1.0))), Value::Bool(true));
|
|
854
|
+
assert_same!(ends_with(&s("abc"), &s("ab"), Some(&n(2.0))), Value::Bool(true));
|
|
702
855
|
}
|
|
703
856
|
|
|
704
857
|
#[test]
|
|
@@ -334,10 +334,16 @@ fn stmt_rebinds(s: &Statement, name: &str) -> bool {
|
|
|
334
334
|
}
|
|
335
335
|
Statement::ForOf {
|
|
336
336
|
name: n,
|
|
337
|
-
iterable,
|
|
337
|
+
iterable: e,
|
|
338
338
|
body,
|
|
339
339
|
..
|
|
340
|
-
}
|
|
340
|
+
}
|
|
341
|
+
| Statement::ForIn {
|
|
342
|
+
name: n,
|
|
343
|
+
object: e,
|
|
344
|
+
body,
|
|
345
|
+
..
|
|
346
|
+
} => n.as_ref() == name || expr_rebinds(e, name) || stmt_rebinds(body, name),
|
|
341
347
|
Statement::Switch {
|
|
342
348
|
expr,
|
|
343
349
|
cases,
|
|
@@ -504,10 +510,16 @@ fn name_rebinds_in_stmt(s: &Statement, name: &str) -> bool {
|
|
|
504
510
|
}
|
|
505
511
|
Statement::ForOf {
|
|
506
512
|
name: n,
|
|
507
|
-
iterable,
|
|
513
|
+
iterable: e,
|
|
508
514
|
body,
|
|
509
515
|
..
|
|
510
|
-
}
|
|
516
|
+
}
|
|
517
|
+
| Statement::ForIn {
|
|
518
|
+
name: n,
|
|
519
|
+
object: e,
|
|
520
|
+
body,
|
|
521
|
+
..
|
|
522
|
+
} => n.as_ref() == name || expr_rebinds(e, name) || name_rebinds_in_stmt(body, name),
|
|
511
523
|
Statement::Switch {
|
|
512
524
|
expr,
|
|
513
525
|
cases,
|
|
@@ -1803,6 +1815,46 @@ impl<'a> Compiler<'a> {
|
|
|
1803
1815
|
self.emit(Opcode::LoopVarsEnd);
|
|
1804
1816
|
self.exit_block_scope();
|
|
1805
1817
|
}
|
|
1818
|
+
Statement::ForIn {
|
|
1819
|
+
name,
|
|
1820
|
+
name_span,
|
|
1821
|
+
object,
|
|
1822
|
+
body,
|
|
1823
|
+
span,
|
|
1824
|
+
} => {
|
|
1825
|
+
// Lower `for (let k in obj)` to `for (let k of Object.keys(obj))` (#413): Object.keys
|
|
1826
|
+
// yields exactly the own enumerable keys the interpreter enumerates — insertion order
|
|
1827
|
+
// for objects, index strings for arrays, and `[]` for a non-object (so `for (k in
|
|
1828
|
+
// null)` no-ops). Reuses the whole for-of iteration path.
|
|
1829
|
+
let dummy = Span {
|
|
1830
|
+
start: (0, 0),
|
|
1831
|
+
end: (0, 0),
|
|
1832
|
+
};
|
|
1833
|
+
let keys_call = Expr::Call {
|
|
1834
|
+
callee: Box::new(Expr::Member {
|
|
1835
|
+
object: Box::new(Expr::Ident {
|
|
1836
|
+
name: Arc::from("Object"),
|
|
1837
|
+
span: dummy,
|
|
1838
|
+
}),
|
|
1839
|
+
prop: MemberProp::Name {
|
|
1840
|
+
name: Arc::from("keys"),
|
|
1841
|
+
span: dummy,
|
|
1842
|
+
},
|
|
1843
|
+
optional: false,
|
|
1844
|
+
span: dummy,
|
|
1845
|
+
}),
|
|
1846
|
+
args: vec![CallArg::Expr(object.clone())],
|
|
1847
|
+
span: dummy,
|
|
1848
|
+
};
|
|
1849
|
+
let lowered = Statement::ForOf {
|
|
1850
|
+
name: Arc::clone(name),
|
|
1851
|
+
name_span: *name_span,
|
|
1852
|
+
iterable: keys_call,
|
|
1853
|
+
body: body.clone(),
|
|
1854
|
+
span: *span,
|
|
1855
|
+
};
|
|
1856
|
+
self.compile_statement(&lowered)?;
|
|
1857
|
+
}
|
|
1806
1858
|
Statement::Return { value, .. } => {
|
|
1807
1859
|
// Evaluate the return value first (JS order), then run any enclosing `finally`
|
|
1808
1860
|
// blocks (they're stack-neutral, so the value stays on top), then return.
|
|
@@ -258,7 +258,9 @@ impl MathUnaryFn {
|
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
MathUnaryFn::Trunc => x.trunc(),
|
|
261
|
-
// JS `Math.sign` (matches `tishlang_builtins::math::sign`): NaN→NaN, else ±1
|
|
261
|
+
// JS `Math.sign` (matches `tishlang_builtins::math::sign`): NaN→NaN, else ±1; and for
|
|
262
|
+
// ±0 return `x` unchanged so `Math.sign(-0)` is `-0` (`x < 0.0` is false for -0, so it
|
|
263
|
+
// lands in the else — bare `0.0` would drop the sign).
|
|
262
264
|
MathUnaryFn::Sign => {
|
|
263
265
|
if x.is_nan() {
|
|
264
266
|
f64::NAN
|
|
@@ -267,7 +269,7 @@ impl MathUnaryFn {
|
|
|
267
269
|
} else if x < 0.0 {
|
|
268
270
|
-1.0
|
|
269
271
|
} else {
|
|
270
|
-
|
|
272
|
+
x
|
|
271
273
|
}
|
|
272
274
|
}
|
|
273
275
|
MathUnaryFn::Sin => x.sin(),
|