@tishlang/tish-lsp 2.16.13 → 2.36.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 (52) 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/fixtures/process_capture.tish +16 -0
  7. package/crates/tish/tests/fs_parity.rs +103 -0
  8. package/crates/tish/tests/process_capture.rs +40 -0
  9. package/crates/tish/tests/shortcircuit.rs +3 -3
  10. package/crates/tish_ast/src/ast.rs +77 -3
  11. package/crates/tish_builtins/Cargo.toml +1 -0
  12. package/crates/tish_builtins/src/array.rs +195 -16
  13. package/crates/tish_builtins/src/collections.rs +37 -2
  14. package/crates/tish_builtins/src/globals.rs +215 -25
  15. package/crates/tish_builtins/src/math.rs +65 -7
  16. package/crates/tish_builtins/src/number.rs +173 -0
  17. package/crates/tish_builtins/src/string.rs +168 -15
  18. package/crates/tish_bytecode/src/compiler.rs +56 -4
  19. package/crates/tish_bytecode/src/opcode.rs +4 -2
  20. package/crates/tish_compile/src/codegen.rs +519 -46
  21. package/crates/tish_compile/src/infer.rs +26 -11
  22. package/crates/tish_compile/src/resolve.rs +87 -10
  23. package/crates/tish_compile_js/src/codegen.rs +82 -24
  24. package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
  25. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +17 -2
  26. package/crates/tish_core/src/json.rs +5 -1
  27. package/crates/tish_core/src/lib.rs +48 -2
  28. package/crates/tish_core/src/uri.rs +45 -0
  29. package/crates/tish_core/src/value.rs +6 -0
  30. package/crates/tish_eval/Cargo.toml +2 -1
  31. package/crates/tish_eval/src/eval.rs +661 -27
  32. package/crates/tish_eval/src/natives.rs +318 -18
  33. package/crates/tish_eval/src/regex.rs +47 -1
  34. package/crates/tish_fmt/src/lib.rs +23 -4
  35. package/crates/tish_lsp/Cargo.toml +1 -1
  36. package/crates/tish_opt/src/lib.rs +20 -1
  37. package/crates/tish_parser/src/lib.rs +41 -0
  38. package/crates/tish_parser/src/parser.rs +180 -11
  39. package/crates/tish_resolve/src/lib.rs +66 -5
  40. package/crates/tish_runtime/Cargo.toml +5 -1
  41. package/crates/tish_runtime/src/fs_ext.rs +359 -0
  42. package/crates/tish_runtime/src/http_hyper.rs +41 -18
  43. package/crates/tish_runtime/src/lib.rs +236 -10
  44. package/crates/tish_ui/src/jsx.rs +18 -3
  45. package/crates/tish_vm/src/jit.rs +1 -1
  46. package/crates/tish_vm/src/vm.rs +294 -60
  47. package/package.json +1 -1
  48. package/platform/darwin-arm64/tish-lsp +0 -0
  49. package/platform/darwin-x64/tish-lsp +0 -0
  50. package/platform/linux-arm64/tish-lsp +0 -0
  51. package/platform/linux-x64/tish-lsp +0 -0
  52. package/platform/win32-x64/tish-lsp.exe +0 -0
@@ -180,28 +180,65 @@ pub fn all_compiled_capabilities() -> HashSet<String> {
180
180
  fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str) -> Option<Value> {
181
181
  #[cfg(feature = "fs")]
182
182
  if spec == "tish:fs" && cap_allows(enabled, "fs") {
183
+ use tishlang_runtime::fs_ext as fx;
183
184
  return match export_name {
184
- "readFile" => Some(Value::native(|args: &[Value]| {
185
- tishlang_runtime::read_file(args)
186
- })),
187
- "writeFile" => Some(Value::native(|args: &[Value]| {
188
- tishlang_runtime::write_file(args)
189
- })),
190
- "fileExists" => Some(Value::native(|args: &[Value]| {
191
- tishlang_runtime::file_exists(args)
192
- })),
193
- "isDir" => Some(Value::native(|args: &[Value]| {
194
- tishlang_runtime::is_dir(args)
195
- })),
196
- "readDir" => Some(Value::native(|args: &[Value]| {
197
- tishlang_runtime::read_dir(args)
198
- })),
199
- "mkdir" => Some(Value::native(|args: &[Value]| {
200
- tishlang_runtime::mkdir(args)
201
- })),
202
- "readFileBytes" => Some(Value::native(|args: &[Value]| {
203
- tishlang_runtime::read_file_bytes(args)
204
- })),
185
+ // tish names → the dual fs_ext functions (sync, or Node callback if a fn is passed)
186
+ "readFile" => Some(Value::native(|a: &[Value]| fx::read_file(a))),
187
+ "writeFile" => Some(Value::native(|a: &[Value]| fx::write_file(a))),
188
+ "fileExists" => Some(Value::native(|a: &[Value]| tishlang_runtime::file_exists(a))),
189
+ "isDir" => Some(Value::native(|a: &[Value]| tishlang_runtime::is_dir(a))),
190
+ "readDir" => Some(Value::native(|a: &[Value]| fx::readdir(a))),
191
+ "readFileBytes" => Some(Value::native(|a: &[Value]| fx::read_file_bytes(a))),
192
+ // Node-compatible names + new methods (sync) — issue #122
193
+ "readFileSync" => Some(Value::native(|a: &[Value]| fx::read_file(a))),
194
+ "readFileBytesSync" => Some(Value::native(|a: &[Value]| fx::read_file_bytes(a))),
195
+ "writeFileSync" => Some(Value::native(|a: &[Value]| fx::write_file(a))),
196
+ "appendFile" | "appendFileSync" => Some(Value::native(|a: &[Value]| fx::append_file(a))),
197
+ "existsSync" => Some(Value::native(|a: &[Value]| fx::exists(a))),
198
+ "mkdir" | "mkdirSync" => Some(Value::native(|a: &[Value]| fx::mkdir(a))),
199
+ "readdir" | "readdirSync" => Some(Value::native(|a: &[Value]| fx::readdir(a))),
200
+ "stat" | "statSync" => Some(Value::native(|a: &[Value]| fx::stat(a))),
201
+ "lstat" | "lstatSync" => Some(Value::native(|a: &[Value]| fx::lstat(a))),
202
+ "rm" | "rmSync" => Some(Value::native(|a: &[Value]| fx::rm(a))),
203
+ "rmdir" | "rmdirSync" => Some(Value::native(|a: &[Value]| fx::rmdir(a))),
204
+ "unlink" | "unlinkSync" => Some(Value::native(|a: &[Value]| fx::unlink(a))),
205
+ "rename" | "renameSync" => Some(Value::native(|a: &[Value]| fx::rename(a))),
206
+ "copyFile" | "copyFileSync" => Some(Value::native(|a: &[Value]| fx::copy_file(a))),
207
+ "realpath" | "realpathSync" => Some(Value::native(|a: &[Value]| fx::realpath(a))),
208
+ "readlink" | "readlinkSync" => Some(Value::native(|a: &[Value]| fx::readlink(a))),
209
+ "truncate" | "truncateSync" => Some(Value::native(|a: &[Value]| fx::truncate(a))),
210
+ "mkdtemp" | "mkdtempSync" => Some(Value::native(|a: &[Value]| fx::mkdtemp(a))),
211
+ "cp" | "cpSync" => Some(Value::native(|a: &[Value]| fx::cp(a))),
212
+ "access" | "accessSync" => Some(Value::native(|a: &[Value]| fx::access(a))),
213
+ "constants" => Some(fx::constants()),
214
+ _ => None,
215
+ };
216
+ }
217
+ #[cfg(feature = "fs")]
218
+ if spec == "tish:fs/promises" && cap_allows(enabled, "fs") {
219
+ use tishlang_runtime::fs_ext as fx;
220
+ return match export_name {
221
+ "readFile" => Some(Value::native(|a: &[Value]| fx::read_file_promise(a))),
222
+ "readFileBytes" => Some(Value::native(|a: &[Value]| fx::read_file_bytes_promise(a))),
223
+ "writeFile" => Some(Value::native(|a: &[Value]| fx::write_file_promise(a))),
224
+ "appendFile" => Some(Value::native(|a: &[Value]| fx::append_file_promise(a))),
225
+ "mkdir" => Some(Value::native(|a: &[Value]| fx::mkdir_promise(a))),
226
+ "readdir" => Some(Value::native(|a: &[Value]| fx::readdir_promise(a))),
227
+ "stat" => Some(Value::native(|a: &[Value]| fx::stat_promise(a))),
228
+ "lstat" => Some(Value::native(|a: &[Value]| fx::lstat_promise(a))),
229
+ "rm" => Some(Value::native(|a: &[Value]| fx::rm_promise(a))),
230
+ "rmdir" => Some(Value::native(|a: &[Value]| fx::rmdir_promise(a))),
231
+ "unlink" => Some(Value::native(|a: &[Value]| fx::unlink_promise(a))),
232
+ "rename" => Some(Value::native(|a: &[Value]| fx::rename_promise(a))),
233
+ "copyFile" => Some(Value::native(|a: &[Value]| fx::copy_file_promise(a))),
234
+ "realpath" => Some(Value::native(|a: &[Value]| fx::realpath_promise(a))),
235
+ "readlink" => Some(Value::native(|a: &[Value]| fx::readlink_promise(a))),
236
+ "truncate" => Some(Value::native(|a: &[Value]| fx::truncate_promise(a))),
237
+ "mkdtemp" => Some(Value::native(|a: &[Value]| fx::mkdtemp_promise(a))),
238
+ "cp" => Some(Value::native(|a: &[Value]| fx::cp_promise(a))),
239
+ "access" => Some(Value::native(|a: &[Value]| fx::access_promise(a))),
240
+ "exists" => Some(Value::native(|a: &[Value]| fx::exists_promise(a))),
241
+ "constants" => Some(fx::constants()),
205
242
  _ => None,
206
243
  };
207
244
  }
@@ -300,6 +337,12 @@ fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str)
300
337
  "execFile" => Some(Value::native(|args: &[Value]| {
301
338
  tishlang_runtime::process_exec_file(args)
302
339
  })),
340
+ "execCapture" => Some(Value::native(|args: &[Value]| {
341
+ tishlang_runtime::process_exec_capture(args)
342
+ })),
343
+ "execFileCapture" => Some(Value::native(|args: &[Value]| {
344
+ tishlang_runtime::process_exec_file_capture(args)
345
+ })),
303
346
  "argv" => Some(Value::Array(VmRef::new(
304
347
  tishlang_core::process_argv()
305
348
  .into_iter()
@@ -329,6 +372,16 @@ fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str)
329
372
  "execFile".into(),
330
373
  Value::native(|args: &[Value]| tishlang_runtime::process_exec_file(args)),
331
374
  );
375
+ m.insert(
376
+ "execCapture".into(),
377
+ Value::native(|args: &[Value]| tishlang_runtime::process_exec_capture(args)),
378
+ );
379
+ m.insert(
380
+ "execFileCapture".into(),
381
+ Value::native(|args: &[Value]| {
382
+ tishlang_runtime::process_exec_file_capture(args)
383
+ }),
384
+ );
332
385
  m.insert(
333
386
  "argv".into(),
334
387
  Value::Array(VmRef::new(
@@ -537,6 +590,13 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
537
590
  "pow".into(),
538
591
  Value::native(|args: &[Value]| math_builtins::pow(args)),
539
592
  );
593
+ // `imul` delegates to the shared builtin (ToInt32 + wrapping i32 multiply) so the vm never
594
+ // diverges from interp/native (#247). Was previously absent from the vm `Math` object, so
595
+ // `Math.imul(...)` threw "Call of non-function" on the bytecode VM.
596
+ math.insert(
597
+ "imul".into(),
598
+ Value::native(|args: &[Value]| math_builtins::imul(args)),
599
+ );
540
600
  math.insert(
541
601
  "sin".into(),
542
602
  Value::native(|args: &[Value]| math_builtins::sin(args)),
@@ -597,13 +657,12 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
597
657
  Value::Number(n.acos())
598
658
  }),
599
659
  );
660
+ // hypot delegates to the shared builtin (variadic, NaN/Infinity-correct) so the vm never diverges
661
+ // from interp/native (#247). The old inline `filter_map(as_number)` dropped non-number args
662
+ // instead of propagating NaN.
600
663
  math.insert(
601
664
  "hypot".into(),
602
- Value::native(|args: &[Value]| {
603
- let nums: Vec<f64> = args.iter().filter_map(|v| v.as_number()).collect();
604
- let sum_sq: f64 = nums.iter().map(|n| n * n).sum();
605
- Value::Number(sum_sq.sqrt())
606
- }),
665
+ Value::native(|args: &[Value]| math_builtins::hypot(args)),
607
666
  );
608
667
  // Hyperbolic, inverse-hyperbolic, cbrt and base-2/10 logs. Like the trig block above
609
668
  // these aren't in `math_builtins`, and on the wasm/native VM there is no host `Math`
@@ -628,6 +687,10 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
628
687
  math_unary!("cbrt", cbrt);
629
688
  math_unary!("log2", log2);
630
689
  math_unary!("log10", log10);
690
+ math_unary!("expm1", exp_m1);
691
+ math_unary!("log1p", ln_1p);
692
+ math.insert("clz32".into(), Value::native(|a: &[Value]| math_builtins::clz32(a)));
693
+ math.insert("fround".into(), Value::native(|a: &[Value]| math_builtins::fround(a)));
631
694
  math.insert("PI".into(), Value::Number(std::f64::consts::PI));
632
695
  math.insert("E".into(), Value::Number(std::f64::consts::E));
633
696
  g.insert("Math".into(), value_object_from_map(math));
@@ -668,6 +731,18 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
668
731
  "decodeURI".into(),
669
732
  Value::native(|args: &[Value]| globals_builtins::decode_uri(args)),
670
733
  );
734
+ g.insert(
735
+ "encodeURIComponent".into(),
736
+ Value::native(|args: &[Value]| globals_builtins::encode_uri_component(args)),
737
+ );
738
+ g.insert(
739
+ "decodeURIComponent".into(),
740
+ Value::native(|args: &[Value]| globals_builtins::decode_uri_component(args)),
741
+ );
742
+ g.insert(
743
+ "structuredClone".into(),
744
+ Value::native(|args: &[Value]| globals_builtins::structured_clone(args)),
745
+ );
671
746
  g.insert(
672
747
  "htmlEscape".into(),
673
748
  Value::native(|args: &[Value]| {
@@ -769,6 +844,18 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
769
844
  "assign".into(),
770
845
  Value::native(|args: &[Value]| globals_builtins::object_assign(args)),
771
846
  );
847
+ object_methods.insert(
848
+ "is".into(),
849
+ Value::native(|args: &[Value]| globals_builtins::object_is(args)),
850
+ );
851
+ object_methods.insert(
852
+ "hasOwn".into(),
853
+ Value::native(|args: &[Value]| globals_builtins::object_has_own(args)),
854
+ );
855
+ object_methods.insert(
856
+ "getOwnPropertyNames".into(),
857
+ Value::native(|args: &[Value]| globals_builtins::object_keys(args)),
858
+ );
772
859
  object_methods.insert(
773
860
  "fromEntries".into(),
774
861
  Value::native(|args: &[Value]| globals_builtins::object_from_entries(args)),
@@ -794,6 +881,14 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
794
881
  "isArray".into(),
795
882
  Value::native(|args: &[Value]| globals_builtins::array_is_array(args)),
796
883
  );
884
+ array_static.insert(
885
+ "of".into(),
886
+ Value::native(|args: &[Value]| globals_builtins::array_of(args)),
887
+ );
888
+ array_static.insert(
889
+ "from".into(),
890
+ Value::native(|args: &[Value]| globals_builtins::array_from(args)),
891
+ );
797
892
  array_static.insert(
798
893
  Arc::from("__call"),
799
894
  Value::native(|args: &[Value]| construct_builtin::array_construct(args)),
@@ -810,12 +905,26 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
810
905
  string_static.insert(Arc::from("__call"), string_convert_fn);
811
906
  g.insert("String".into(), value_object_from_map(string_static));
812
907
 
813
- // Number(value) coercion as a callable global (issue #36).
908
+ // Number(value) coercion as a callable global (issue #36) + the `Number.*` statics.
814
909
  let mut number_static = ObjectMap::default();
815
910
  number_static.insert(
816
911
  Arc::from("__call"),
817
912
  Value::native(|args: &[Value]| globals_builtins::number_convert(args)),
818
913
  );
914
+ number_static.insert(Arc::from("isInteger"), Value::native(|a: &[Value]| globals_builtins::number_is_integer(a)));
915
+ number_static.insert(Arc::from("isSafeInteger"), Value::native(|a: &[Value]| globals_builtins::number_is_safe_integer(a)));
916
+ number_static.insert(Arc::from("isNaN"), Value::native(|a: &[Value]| globals_builtins::number_is_nan(a)));
917
+ number_static.insert(Arc::from("isFinite"), Value::native(|a: &[Value]| globals_builtins::number_is_finite(a)));
918
+ number_static.insert(Arc::from("parseInt"), Value::native(|a: &[Value]| globals_builtins::parse_int(a)));
919
+ number_static.insert(Arc::from("parseFloat"), Value::native(|a: &[Value]| globals_builtins::parse_float(a)));
920
+ number_static.insert(Arc::from("MAX_SAFE_INTEGER"), Value::Number(9_007_199_254_740_991.0));
921
+ number_static.insert(Arc::from("MIN_SAFE_INTEGER"), Value::Number(-9_007_199_254_740_991.0));
922
+ number_static.insert(Arc::from("EPSILON"), Value::Number(f64::EPSILON));
923
+ number_static.insert(Arc::from("MAX_VALUE"), Value::Number(f64::MAX));
924
+ number_static.insert(Arc::from("MIN_VALUE"), Value::Number(5e-324));
925
+ number_static.insert(Arc::from("POSITIVE_INFINITY"), Value::Number(f64::INFINITY));
926
+ number_static.insert(Arc::from("NEGATIVE_INFINITY"), Value::Number(f64::NEG_INFINITY));
927
+ number_static.insert(Arc::from("NaN"), Value::Number(f64::NAN));
819
928
  g.insert("Number".into(), value_object_from_map(number_static));
820
929
 
821
930
  // JSX / Lattish: stubs for bytecode VM when no DOM (e.g. console). Override via set_global in browser.
@@ -866,6 +975,14 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
866
975
  "execFile".into(),
867
976
  Value::native(|args: &[Value]| tishlang_runtime::process_exec_file(args)),
868
977
  );
978
+ process_obj.insert(
979
+ "execCapture".into(),
980
+ Value::native(|args: &[Value]| tishlang_runtime::process_exec_capture(args)),
981
+ );
982
+ process_obj.insert(
983
+ "execFileCapture".into(),
984
+ Value::native(|args: &[Value]| tishlang_runtime::process_exec_file_capture(args)),
985
+ );
869
986
  process_obj.insert(
870
987
  "argv".into(),
871
988
  Value::Array(VmRef::new(
@@ -1383,7 +1500,7 @@ mod hof_fusion {
1383
1500
  /// (which would pass a `Value::Bool`, not a number). No-initial-value semantics match the generic
1384
1501
  /// path: absent init (`Value::Null`) with a non-empty array seeds `acc = data[0]` and scans from
1385
1502
  /// index 1; an empty array with no init throws (bail to the generic path, which raises it).
1386
- pub(super) fn reduce(arr: &Value, cb: &Value, init: &Value) -> Option<Value> {
1503
+ pub(super) fn reduce(arr: &Value, cb: &Value, initial: Option<&Value>) -> Option<Value> {
1387
1504
  let nf = fused_numeric_fn(cb)?;
1388
1505
  if nf.arity() > 3 || nf.result_is_bool() {
1389
1506
  return None;
@@ -1392,11 +1509,11 @@ mod hof_fusion {
1392
1509
  let arity = nf.arity();
1393
1510
  // Determine seed. Reduce with no initial value AND an empty array is a TypeError in JS — let
1394
1511
  // the generic path produce that exact throw rather than replicate it here.
1395
- let (start, mut acc) = match init {
1396
- Value::Null if !data.is_empty() => (1usize, data[0]),
1397
- Value::Null => return None, // empty + no init → generic path throws
1398
- Value::Number(n) => (0usize, *n),
1399
- _ => return None, // non-numeric explicit init → generic path (acc wouldn't stay f64)
1512
+ let (start, mut acc) = match initial {
1513
+ None if !data.is_empty() => (1usize, data[0]),
1514
+ None => return None, // empty + no init → generic path throws
1515
+ Some(Value::Number(n)) => (0usize, *n),
1516
+ Some(_) => return None, // non-numeric/explicit-null init → generic path (acc wouldn't stay f64)
1400
1517
  };
1401
1518
  let mut args = [0.0f64; 3];
1402
1519
  for (i, &x) in data.iter().enumerate().skip(start) {
@@ -3823,13 +3940,21 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
3823
3940
  }),
3824
3941
  "reduce" => make_native_fn(move |args| {
3825
3942
  let cb = args.first().cloned().unwrap_or(Value::Null);
3826
- let init = args.get(1).cloned().unwrap_or(Value::Null);
3943
+ let init = args.get(1).cloned(); // None = no initial value (empty array → throw)
3827
3944
  #[cfg(not(target_arch = "wasm32"))]
3828
- if let Some(v) = hof_fusion::reduce(&bv, &cb, &init) {
3945
+ if let Some(v) = hof_fusion::reduce(&bv, &cb, init.as_ref()) {
3829
3946
  return v;
3830
3947
  }
3831
- arr_builtins::reduce(&bv, &cb, &init)
3948
+ arr_builtins::reduce(&bv, &cb, init.as_ref())
3832
3949
  }),
3950
+ "reduceRight" => make_native_fn(move |args| {
3951
+ let cb = args.first().cloned().unwrap_or(Value::Null);
3952
+ let init = args.get(1).cloned();
3953
+ arr_builtins::reduce_right(&bv, &cb, init.as_ref())
3954
+ }),
3955
+ "keys" => make_native_fn(move |_| arr_builtins::keys(&bv)),
3956
+ "values" => make_native_fn(move |_| arr_builtins::values(&bv)),
3957
+ "entries" => make_native_fn(move |_| arr_builtins::entries(&bv)),
3833
3958
  "forEach" => make_native_fn(move |args| {
3834
3959
  let cb = args.first().cloned().unwrap_or(Value::Null);
3835
3960
  #[cfg(not(target_arch = "wasm32"))]
@@ -3879,6 +4004,21 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
3879
4004
  arr_builtins::flat_map(&bv, &cb)
3880
4005
  }),
3881
4006
  "reverse" => make_native_fn(move |_| arr_builtins::reverse(&bv)),
4007
+ "toReversed" => make_native_fn(move |_| arr_builtins::to_reversed(&bv)),
4008
+ "toSorted" => {
4009
+ make_native_fn(move |args| arr_builtins::to_sorted(&bv, args.first()))
4010
+ }
4011
+ "with" => make_native_fn(move |args| {
4012
+ let i = args.first().cloned().unwrap_or(Value::Null);
4013
+ let v = args.get(1).cloned().unwrap_or(Value::Null);
4014
+ arr_builtins::with(&bv, &i, &v)
4015
+ }),
4016
+ "toSpliced" => make_native_fn(move |args| {
4017
+ let start = args.first().cloned().unwrap_or(Value::Null);
4018
+ let dc = args.get(1).cloned();
4019
+ let items: Vec<Value> = args.iter().skip(2).cloned().collect();
4020
+ arr_builtins::to_spliced(&bv, &start, dc.as_ref(), &items)
4021
+ }),
3882
4022
  "fill" => make_native_fn(move |args| {
3883
4023
  let v = args.first().cloned().unwrap_or(Value::Null);
3884
4024
  let s = args.get(1).cloned().unwrap_or(Value::Null);
@@ -3893,7 +4033,17 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
3893
4033
  "concat" => make_native_fn(move |args| arr_builtins::concat(&bv, args)),
3894
4034
  "indexOf" => make_native_fn(move |args| {
3895
4035
  let s = args.first().cloned().unwrap_or(Value::Null);
3896
- arr_builtins::index_of(&bv, &s)
4036
+ arr_builtins::index_of(&bv, &s, args.get(1))
4037
+ }),
4038
+ "lastIndexOf" => make_native_fn(move |args| {
4039
+ let s = args.first().cloned().unwrap_or(Value::Null);
4040
+ arr_builtins::last_index_of(&bv, &s, args.get(1))
4041
+ }),
4042
+ "copyWithin" => make_native_fn(move |args| {
4043
+ let t = args.first().cloned().unwrap_or(Value::Null);
4044
+ let s = args.get(1).cloned().unwrap_or(Value::Null);
4045
+ let e = args.get(2).cloned().unwrap_or(Value::Null);
4046
+ arr_builtins::copy_within(&bv, &t, &s, &e)
3897
4047
  }),
3898
4048
  "includes" => make_native_fn(move |args| {
3899
4049
  let s = args.first().cloned().unwrap_or(Value::Null);
@@ -3966,7 +4116,17 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
3966
4116
  }),
3967
4117
  "indexOf" => make_native_fn(move |args: &[Value]| {
3968
4118
  let search = args.first().unwrap_or(&Value::Null);
3969
- arr_builtins::index_of(&Value::Array(a_clone.clone()), search)
4119
+ arr_builtins::index_of(&Value::Array(a_clone.clone()), search, args.get(1))
4120
+ }),
4121
+ "lastIndexOf" => make_native_fn(move |args: &[Value]| {
4122
+ let search = args.first().unwrap_or(&Value::Null);
4123
+ arr_builtins::last_index_of(&Value::Array(a_clone.clone()), search, args.get(1))
4124
+ }),
4125
+ "copyWithin" => make_native_fn(move |args: &[Value]| {
4126
+ let t = args.first().cloned().unwrap_or(Value::Null);
4127
+ let s = args.get(1).cloned().unwrap_or(Value::Null);
4128
+ let e = args.get(2).cloned().unwrap_or(Value::Null);
4129
+ arr_builtins::copy_within(&Value::Array(a_clone.clone()), &t, &s, &e)
3970
4130
  }),
3971
4131
  "includes" => make_native_fn(move |args: &[Value]| {
3972
4132
  let search = args.first().unwrap_or(&Value::Null);
@@ -3995,14 +4155,22 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
3995
4155
  }),
3996
4156
  "reduce" => make_native_fn(move |args: &[Value]| {
3997
4157
  let cb = args.first().cloned().unwrap_or(Value::Null);
3998
- let init = args.get(1).cloned().unwrap_or(Value::Null);
4158
+ let init = args.get(1).cloned(); // None = no initial value (empty array → throw)
3999
4159
  let arr = Value::Array(a_clone.clone());
4000
4160
  #[cfg(not(target_arch = "wasm32"))]
4001
- if let Some(v) = hof_fusion::reduce(&arr, &cb, &init) {
4161
+ if let Some(v) = hof_fusion::reduce(&arr, &cb, init.as_ref()) {
4002
4162
  return v;
4003
4163
  }
4004
- arr_builtins::reduce(&arr, &cb, &init)
4164
+ arr_builtins::reduce(&arr, &cb, init.as_ref())
4005
4165
  }),
4166
+ "reduceRight" => make_native_fn(move |args: &[Value]| {
4167
+ let cb = args.first().cloned().unwrap_or(Value::Null);
4168
+ let init = args.get(1).cloned();
4169
+ arr_builtins::reduce_right(&Value::Array(a_clone.clone()), &cb, init.as_ref())
4170
+ }),
4171
+ "keys" => make_native_fn(move |_: &[Value]| arr_builtins::keys(&Value::Array(a_clone.clone()))),
4172
+ "values" => make_native_fn(move |_: &[Value]| arr_builtins::values(&Value::Array(a_clone.clone()))),
4173
+ "entries" => make_native_fn(move |_: &[Value]| arr_builtins::entries(&Value::Array(a_clone.clone()))),
4006
4174
  "forEach" => make_native_fn(move |args: &[Value]| {
4007
4175
  let cb = args.first().cloned().unwrap_or(Value::Null);
4008
4176
  let arr = Value::Array(a_clone.clone());
@@ -4070,6 +4238,28 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
4070
4238
  &items,
4071
4239
  )
4072
4240
  }),
4241
+ "toReversed" => make_native_fn(move |_: &[Value]| {
4242
+ arr_builtins::to_reversed(&Value::Array(a_clone.clone()))
4243
+ }),
4244
+ "toSorted" => make_native_fn(move |args: &[Value]| {
4245
+ arr_builtins::to_sorted(&Value::Array(a_clone.clone()), args.first())
4246
+ }),
4247
+ "with" => make_native_fn(move |args: &[Value]| {
4248
+ let i = args.first().cloned().unwrap_or(Value::Null);
4249
+ let v = args.get(1).cloned().unwrap_or(Value::Null);
4250
+ arr_builtins::with(&Value::Array(a_clone.clone()), &i, &v)
4251
+ }),
4252
+ "toSpliced" => make_native_fn(move |args: &[Value]| {
4253
+ let start = args.first().unwrap_or(&Value::Null);
4254
+ let delete_count = args.get(1).map(|v| v as &Value);
4255
+ let items: Vec<Value> = args.get(2..).unwrap_or(&[]).to_vec();
4256
+ arr_builtins::to_spliced(
4257
+ &Value::Array(a_clone.clone()),
4258
+ start,
4259
+ delete_count,
4260
+ &items,
4261
+ )
4262
+ }),
4073
4263
  _ => return Err(format!("Property '{}' not found", key)),
4074
4264
  };
4075
4265
  Ok(Value::Function(method))
@@ -4136,6 +4326,16 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
4136
4326
  "trim" => make_native_fn(move |_args: &[Value]| {
4137
4327
  str_builtins::trim(&Value::String(s_clone.clone()))
4138
4328
  }),
4329
+ "trimStart" => make_native_fn(move |_args: &[Value]| {
4330
+ str_builtins::trim_start(&Value::String(s_clone.clone()))
4331
+ }),
4332
+ "trimEnd" => make_native_fn(move |_args: &[Value]| {
4333
+ str_builtins::trim_end(&Value::String(s_clone.clone()))
4334
+ }),
4335
+ "normalize" => make_native_fn(move |args: &[Value]| {
4336
+ let form = args.first().cloned().unwrap_or(Value::Null);
4337
+ str_builtins::normalize(&Value::String(s_clone.clone()), &form)
4338
+ }),
4139
4339
  "toUpperCase" => make_native_fn(move |_args: &[Value]| {
4140
4340
  str_builtins::to_upper_case(&Value::String(s_clone.clone()))
4141
4341
  }),
@@ -4144,11 +4344,11 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
4144
4344
  }),
4145
4345
  "startsWith" => make_native_fn(move |args: &[Value]| {
4146
4346
  let search = args.first().unwrap_or(&Value::Null);
4147
- str_builtins::starts_with(&Value::String(s_clone.clone()), search)
4347
+ str_builtins::starts_with(&Value::String(s_clone.clone()), search, args.get(1))
4148
4348
  }),
4149
4349
  "endsWith" => make_native_fn(move |args: &[Value]| {
4150
4350
  let search = args.first().unwrap_or(&Value::Null);
4151
- str_builtins::ends_with(&Value::String(s_clone.clone()), search)
4351
+ str_builtins::ends_with(&Value::String(s_clone.clone()), search, args.get(1))
4152
4352
  }),
4153
4353
  "replace" => make_native_fn(move |args: &[Value]| {
4154
4354
  let search = args.first().unwrap_or(&Value::Null);
@@ -4176,6 +4376,11 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
4176
4376
  tishlang_runtime::string_match_regex(&Value::String(s_clone.clone()), re)
4177
4377
  }),
4178
4378
  #[cfg(feature = "regex")]
4379
+ "matchAll" => make_native_fn(move |args: &[Value]| {
4380
+ let re = args.first().unwrap_or(&Value::Null);
4381
+ tishlang_runtime::string_match_all_regex(&Value::String(s_clone.clone()), re)
4382
+ }),
4383
+ #[cfg(feature = "regex")]
4179
4384
  "search" => make_native_fn(move |args: &[Value]| {
4180
4385
  let re = args.first().unwrap_or(&Value::Null);
4181
4386
  tishlang_runtime::string_search_regex(&Value::String(s_clone.clone()), re)
@@ -4192,6 +4397,15 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
4192
4397
  let idx = args.first().unwrap_or(&Value::Null);
4193
4398
  str_builtins::char_code_at(&Value::String(s_clone.clone()), idx)
4194
4399
  }),
4400
+ "codePointAt" => make_native_fn(move |args: &[Value]| {
4401
+ let idx = args.first().unwrap_or(&Value::Null);
4402
+ str_builtins::code_point_at(&Value::String(s_clone.clone()), idx)
4403
+ }),
4404
+ "substr" => make_native_fn(move |args: &[Value]| {
4405
+ let start = args.first().cloned().unwrap_or(Value::Null);
4406
+ let length = args.get(1).cloned().unwrap_or(Value::Null);
4407
+ str_builtins::substr(&Value::String(s_clone.clone()), &start, &length)
4408
+ }),
4195
4409
  "repeat" => make_native_fn(move |args: &[Value]| {
4196
4410
  let count = args.first().unwrap_or(&Value::Null);
4197
4411
  str_builtins::repeat(&Value::String(s_clone.clone()), count)
@@ -4219,6 +4433,14 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
4219
4433
  let digits = args.first().unwrap_or(&Value::Null);
4220
4434
  num_builtins::to_fixed(&Value::Number(n_val), digits)
4221
4435
  }),
4436
+ "toExponential" => make_native_fn(move |args: &[Value]| {
4437
+ let digits = args.first().unwrap_or(&Value::Null);
4438
+ num_builtins::to_exponential(&Value::Number(n_val), digits)
4439
+ }),
4440
+ "toPrecision" => make_native_fn(move |args: &[Value]| {
4441
+ let precision = args.first().unwrap_or(&Value::Null);
4442
+ num_builtins::to_precision(&Value::Number(n_val), precision)
4443
+ }),
4222
4444
  "toString" => make_native_fn(move |args: &[Value]| {
4223
4445
  let radix = args.first().unwrap_or(&Value::Null);
4224
4446
  num_builtins::to_string(&Value::Number(n_val), radix)
@@ -4333,12 +4555,13 @@ fn get_index(obj: &Value, idx: &Value) -> Result<Value, String> {
4333
4555
  Value::NumberArray(a) => {
4334
4556
  let i = match idx {
4335
4557
  Value::Number(n) => *n as usize,
4336
- _ => {
4337
- return Err(format!(
4338
- "Array index must be number, got {}",
4339
- idx.type_name()
4340
- ))
4341
- }
4558
+ // A canonical integer string key ("0","1",…) indexes the array (#432); any other key
4559
+ // reads Null (JS reads a missing property as undefined) rather than throwing.
4560
+ Value::String(s) => match tishlang_core::str_to_array_index(s) {
4561
+ Some(i) => i,
4562
+ None => return Ok(Value::Null),
4563
+ },
4564
+ _ => return Ok(Value::Null),
4342
4565
  };
4343
4566
  // NaN is used as the hole marker (sparse-array positions); reads return Null.
4344
4567
  Ok(a.borrow()
@@ -4355,12 +4578,12 @@ fn get_index(obj: &Value, idx: &Value) -> Result<Value, String> {
4355
4578
  Value::Array(a) => {
4356
4579
  let i = match idx {
4357
4580
  Value::Number(n) => *n as usize,
4358
- _ => {
4359
- return Err(format!(
4360
- "Array index must be number, got {}",
4361
- idx.type_name()
4362
- ));
4363
- }
4581
+ // Canonical integer string key → index (#432); any other key reads Null, not a throw.
4582
+ Value::String(s) => match tishlang_core::str_to_array_index(s) {
4583
+ Some(i) => i,
4584
+ None => return Ok(Value::Null),
4585
+ },
4586
+ _ => return Ok(Value::Null),
4364
4587
  };
4365
4588
  Ok(a.borrow().get(i).cloned().unwrap_or(Value::Null))
4366
4589
  }
@@ -4369,10 +4592,9 @@ fn get_index(obj: &Value, idx: &Value) -> Result<Value, String> {
4369
4592
  Value::Number(n) => {
4370
4593
  let n = *n;
4371
4594
  if n < 0.0 || n.fract() != 0.0 {
4372
- return Err(format!(
4373
- "String index must be non-negative integer, got {}",
4374
- n
4375
- ));
4595
+ // A negative / non-integer string index reads `null` (JS `undefined`), NOT a
4596
+ // throw — interp and native already return null; the vm must agree. (#437)
4597
+ return Ok(Value::Null);
4376
4598
  }
4377
4599
  n as usize
4378
4600
  }
@@ -4387,7 +4609,8 @@ fn get_index(obj: &Value, idx: &Value) -> Result<Value, String> {
4387
4609
  // O(n) `chars().count()` pre-check (#203).
4388
4610
  match str_builtins::nth_char(s, i) {
4389
4611
  Some(c) => Ok(Value::String(tishlang_core::ArcStr::from(c.to_string()))),
4390
- None => Err("Index out of bounds".to_string()),
4612
+ // Past the end → `null` (JS `undefined`), matching interp/native, not a throw. (#437)
4613
+ None => Ok(Value::Null),
4391
4614
  }
4392
4615
  }
4393
4616
  // A missing own property returns `null`, not a thrown error — matching dot reads
@@ -4447,6 +4670,12 @@ fn set_index(obj: &Value, idx: &Value, val: Value) -> Result<(), String> {
4447
4670
  Value::NumberArray(a) => {
4448
4671
  let i = match idx {
4449
4672
  Value::Number(n) => *n as usize,
4673
+ // Canonical integer string key → index (#432); a non-index string key can't be stored
4674
+ // in a Vec-backed array, so the write is a no-op (rather than a throw).
4675
+ Value::String(s) => match tishlang_core::str_to_array_index(s) {
4676
+ Some(i) => i,
4677
+ None => return Ok(()),
4678
+ },
4450
4679
  _ => {
4451
4680
  return Err(format!(
4452
4681
  "Array index must be number, got {}",
@@ -4487,6 +4716,11 @@ fn set_index(obj: &Value, idx: &Value, val: Value) -> Result<(), String> {
4487
4716
  Value::Array(a) => {
4488
4717
  let i = match idx {
4489
4718
  Value::Number(n) => *n as usize,
4719
+ // Canonical integer string key → index (#432); a non-index string key is a no-op write.
4720
+ Value::String(s) => match tishlang_core::str_to_array_index(s) {
4721
+ Some(i) => i,
4722
+ None => return Ok(()),
4723
+ },
4490
4724
  _ => {
4491
4725
  return Err(format!(
4492
4726
  "Array index must be number, got {}",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tishlang/tish-lsp",
3
- "version": "2.16.13",
3
+ "version": "2.36.0",
4
4
  "description": "Tish language server — diagnostics, completion, formatting, hover, go-to-definition (LSP over stdio)",
5
5
  "license": "PIF",
6
6
  "repository": {
Binary file
Binary file
Binary file
Binary file
Binary file