@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.
Files changed (50) hide show
  1. package/bin/tish-lsp +0 -0
  2. package/crates/js_to_tish/src/transform/expr.rs +12 -2
  3. package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
  4. package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
  5. package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
  6. package/crates/tish/tests/fs_parity.rs +103 -0
  7. package/crates/tish/tests/shortcircuit.rs +3 -3
  8. package/crates/tish_ast/src/ast.rs +77 -3
  9. package/crates/tish_builtins/Cargo.toml +1 -0
  10. package/crates/tish_builtins/src/array.rs +195 -16
  11. package/crates/tish_builtins/src/collections.rs +37 -2
  12. package/crates/tish_builtins/src/globals.rs +215 -25
  13. package/crates/tish_builtins/src/math.rs +65 -7
  14. package/crates/tish_builtins/src/number.rs +173 -0
  15. package/crates/tish_builtins/src/string.rs +168 -15
  16. package/crates/tish_bytecode/src/compiler.rs +56 -4
  17. package/crates/tish_bytecode/src/opcode.rs +4 -2
  18. package/crates/tish_compile/src/codegen.rs +513 -44
  19. package/crates/tish_compile/src/infer.rs +26 -11
  20. package/crates/tish_compile/src/resolve.rs +87 -10
  21. package/crates/tish_compile_js/src/codegen.rs +82 -24
  22. package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
  23. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +17 -2
  24. package/crates/tish_core/src/json.rs +5 -1
  25. package/crates/tish_core/src/lib.rs +48 -2
  26. package/crates/tish_core/src/uri.rs +45 -0
  27. package/crates/tish_core/src/value.rs +6 -0
  28. package/crates/tish_eval/Cargo.toml +2 -1
  29. package/crates/tish_eval/src/eval.rs +655 -27
  30. package/crates/tish_eval/src/natives.rs +264 -18
  31. package/crates/tish_eval/src/regex.rs +47 -1
  32. package/crates/tish_fmt/src/lib.rs +23 -4
  33. package/crates/tish_lsp/Cargo.toml +1 -1
  34. package/crates/tish_opt/src/lib.rs +20 -1
  35. package/crates/tish_parser/src/lib.rs +41 -0
  36. package/crates/tish_parser/src/parser.rs +180 -11
  37. package/crates/tish_resolve/src/lib.rs +66 -5
  38. package/crates/tish_runtime/Cargo.toml +5 -1
  39. package/crates/tish_runtime/src/fs_ext.rs +359 -0
  40. package/crates/tish_runtime/src/http_hyper.rs +41 -18
  41. package/crates/tish_runtime/src/lib.rs +128 -9
  42. package/crates/tish_ui/src/jsx.rs +18 -3
  43. package/crates/tish_vm/src/jit.rs +1 -1
  44. package/crates/tish_vm/src/vm.rs +270 -60
  45. package/package.json +1 -1
  46. package/platform/darwin-arm64/tish-lsp +0 -0
  47. package/platform/darwin-x64/tish-lsp +0 -0
  48. package/platform/linux-arm64/tish-lsp +0 -0
  49. package/platform/linux-x64/tish-lsp +0 -0
  50. 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
- Value::Bool(s.starts_with(search.as_str()))
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
- Value::Bool(s.ends_with(search.as_str()))
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
- let result = if all {
363
- s.replace(search_str, repl_str)
364
- } else {
365
- s.replacen(search_str, repl_str, 1)
366
- };
367
- Value::String(result.into())
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) if !p.is_empty() => p.as_str(),
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
- } => n.as_ref() == name || expr_rebinds(iterable, name) || stmt_rebinds(body, name),
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
- } => n.as_ref() == name || expr_rebinds(iterable, name) || name_rebinds_in_stmt(body, name),
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, and 0/-0→+0.
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
- 0.0
272
+ x
271
273
  }
272
274
  }
273
275
  MathUnaryFn::Sin => x.sin(),