@tishlang/tish-format 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-format +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/Cargo.toml +1 -1
  33. package/crates/tish_fmt/src/lib.rs +23 -4
  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-fmt +0 -0
  47. package/platform/darwin-x64/tish-fmt +0 -0
  48. package/platform/linux-arm64/tish-fmt +0 -0
  49. package/platform/linux-x64/tish-fmt +0 -0
  50. package/platform/win32-x64/tish-fmt.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
  }
@@ -537,6 +574,13 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
537
574
  "pow".into(),
538
575
  Value::native(|args: &[Value]| math_builtins::pow(args)),
539
576
  );
577
+ // `imul` delegates to the shared builtin (ToInt32 + wrapping i32 multiply) so the vm never
578
+ // diverges from interp/native (#247). Was previously absent from the vm `Math` object, so
579
+ // `Math.imul(...)` threw "Call of non-function" on the bytecode VM.
580
+ math.insert(
581
+ "imul".into(),
582
+ Value::native(|args: &[Value]| math_builtins::imul(args)),
583
+ );
540
584
  math.insert(
541
585
  "sin".into(),
542
586
  Value::native(|args: &[Value]| math_builtins::sin(args)),
@@ -597,13 +641,12 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
597
641
  Value::Number(n.acos())
598
642
  }),
599
643
  );
644
+ // hypot delegates to the shared builtin (variadic, NaN/Infinity-correct) so the vm never diverges
645
+ // from interp/native (#247). The old inline `filter_map(as_number)` dropped non-number args
646
+ // instead of propagating NaN.
600
647
  math.insert(
601
648
  "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
- }),
649
+ Value::native(|args: &[Value]| math_builtins::hypot(args)),
607
650
  );
608
651
  // Hyperbolic, inverse-hyperbolic, cbrt and base-2/10 logs. Like the trig block above
609
652
  // these aren't in `math_builtins`, and on the wasm/native VM there is no host `Math`
@@ -628,6 +671,10 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
628
671
  math_unary!("cbrt", cbrt);
629
672
  math_unary!("log2", log2);
630
673
  math_unary!("log10", log10);
674
+ math_unary!("expm1", exp_m1);
675
+ math_unary!("log1p", ln_1p);
676
+ math.insert("clz32".into(), Value::native(|a: &[Value]| math_builtins::clz32(a)));
677
+ math.insert("fround".into(), Value::native(|a: &[Value]| math_builtins::fround(a)));
631
678
  math.insert("PI".into(), Value::Number(std::f64::consts::PI));
632
679
  math.insert("E".into(), Value::Number(std::f64::consts::E));
633
680
  g.insert("Math".into(), value_object_from_map(math));
@@ -668,6 +715,18 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
668
715
  "decodeURI".into(),
669
716
  Value::native(|args: &[Value]| globals_builtins::decode_uri(args)),
670
717
  );
718
+ g.insert(
719
+ "encodeURIComponent".into(),
720
+ Value::native(|args: &[Value]| globals_builtins::encode_uri_component(args)),
721
+ );
722
+ g.insert(
723
+ "decodeURIComponent".into(),
724
+ Value::native(|args: &[Value]| globals_builtins::decode_uri_component(args)),
725
+ );
726
+ g.insert(
727
+ "structuredClone".into(),
728
+ Value::native(|args: &[Value]| globals_builtins::structured_clone(args)),
729
+ );
671
730
  g.insert(
672
731
  "htmlEscape".into(),
673
732
  Value::native(|args: &[Value]| {
@@ -769,6 +828,18 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
769
828
  "assign".into(),
770
829
  Value::native(|args: &[Value]| globals_builtins::object_assign(args)),
771
830
  );
831
+ object_methods.insert(
832
+ "is".into(),
833
+ Value::native(|args: &[Value]| globals_builtins::object_is(args)),
834
+ );
835
+ object_methods.insert(
836
+ "hasOwn".into(),
837
+ Value::native(|args: &[Value]| globals_builtins::object_has_own(args)),
838
+ );
839
+ object_methods.insert(
840
+ "getOwnPropertyNames".into(),
841
+ Value::native(|args: &[Value]| globals_builtins::object_keys(args)),
842
+ );
772
843
  object_methods.insert(
773
844
  "fromEntries".into(),
774
845
  Value::native(|args: &[Value]| globals_builtins::object_from_entries(args)),
@@ -794,6 +865,14 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
794
865
  "isArray".into(),
795
866
  Value::native(|args: &[Value]| globals_builtins::array_is_array(args)),
796
867
  );
868
+ array_static.insert(
869
+ "of".into(),
870
+ Value::native(|args: &[Value]| globals_builtins::array_of(args)),
871
+ );
872
+ array_static.insert(
873
+ "from".into(),
874
+ Value::native(|args: &[Value]| globals_builtins::array_from(args)),
875
+ );
797
876
  array_static.insert(
798
877
  Arc::from("__call"),
799
878
  Value::native(|args: &[Value]| construct_builtin::array_construct(args)),
@@ -810,12 +889,26 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
810
889
  string_static.insert(Arc::from("__call"), string_convert_fn);
811
890
  g.insert("String".into(), value_object_from_map(string_static));
812
891
 
813
- // Number(value) coercion as a callable global (issue #36).
892
+ // Number(value) coercion as a callable global (issue #36) + the `Number.*` statics.
814
893
  let mut number_static = ObjectMap::default();
815
894
  number_static.insert(
816
895
  Arc::from("__call"),
817
896
  Value::native(|args: &[Value]| globals_builtins::number_convert(args)),
818
897
  );
898
+ number_static.insert(Arc::from("isInteger"), Value::native(|a: &[Value]| globals_builtins::number_is_integer(a)));
899
+ number_static.insert(Arc::from("isSafeInteger"), Value::native(|a: &[Value]| globals_builtins::number_is_safe_integer(a)));
900
+ number_static.insert(Arc::from("isNaN"), Value::native(|a: &[Value]| globals_builtins::number_is_nan(a)));
901
+ number_static.insert(Arc::from("isFinite"), Value::native(|a: &[Value]| globals_builtins::number_is_finite(a)));
902
+ number_static.insert(Arc::from("parseInt"), Value::native(|a: &[Value]| globals_builtins::parse_int(a)));
903
+ number_static.insert(Arc::from("parseFloat"), Value::native(|a: &[Value]| globals_builtins::parse_float(a)));
904
+ number_static.insert(Arc::from("MAX_SAFE_INTEGER"), Value::Number(9_007_199_254_740_991.0));
905
+ number_static.insert(Arc::from("MIN_SAFE_INTEGER"), Value::Number(-9_007_199_254_740_991.0));
906
+ number_static.insert(Arc::from("EPSILON"), Value::Number(f64::EPSILON));
907
+ number_static.insert(Arc::from("MAX_VALUE"), Value::Number(f64::MAX));
908
+ number_static.insert(Arc::from("MIN_VALUE"), Value::Number(5e-324));
909
+ number_static.insert(Arc::from("POSITIVE_INFINITY"), Value::Number(f64::INFINITY));
910
+ number_static.insert(Arc::from("NEGATIVE_INFINITY"), Value::Number(f64::NEG_INFINITY));
911
+ number_static.insert(Arc::from("NaN"), Value::Number(f64::NAN));
819
912
  g.insert("Number".into(), value_object_from_map(number_static));
820
913
 
821
914
  // JSX / Lattish: stubs for bytecode VM when no DOM (e.g. console). Override via set_global in browser.
@@ -1383,7 +1476,7 @@ mod hof_fusion {
1383
1476
  /// (which would pass a `Value::Bool`, not a number). No-initial-value semantics match the generic
1384
1477
  /// path: absent init (`Value::Null`) with a non-empty array seeds `acc = data[0]` and scans from
1385
1478
  /// 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> {
1479
+ pub(super) fn reduce(arr: &Value, cb: &Value, initial: Option<&Value>) -> Option<Value> {
1387
1480
  let nf = fused_numeric_fn(cb)?;
1388
1481
  if nf.arity() > 3 || nf.result_is_bool() {
1389
1482
  return None;
@@ -1392,11 +1485,11 @@ mod hof_fusion {
1392
1485
  let arity = nf.arity();
1393
1486
  // Determine seed. Reduce with no initial value AND an empty array is a TypeError in JS — let
1394
1487
  // 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)
1488
+ let (start, mut acc) = match initial {
1489
+ None if !data.is_empty() => (1usize, data[0]),
1490
+ None => return None, // empty + no init → generic path throws
1491
+ Some(Value::Number(n)) => (0usize, *n),
1492
+ Some(_) => return None, // non-numeric/explicit-null init → generic path (acc wouldn't stay f64)
1400
1493
  };
1401
1494
  let mut args = [0.0f64; 3];
1402
1495
  for (i, &x) in data.iter().enumerate().skip(start) {
@@ -3823,13 +3916,21 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
3823
3916
  }),
3824
3917
  "reduce" => make_native_fn(move |args| {
3825
3918
  let cb = args.first().cloned().unwrap_or(Value::Null);
3826
- let init = args.get(1).cloned().unwrap_or(Value::Null);
3919
+ let init = args.get(1).cloned(); // None = no initial value (empty array → throw)
3827
3920
  #[cfg(not(target_arch = "wasm32"))]
3828
- if let Some(v) = hof_fusion::reduce(&bv, &cb, &init) {
3921
+ if let Some(v) = hof_fusion::reduce(&bv, &cb, init.as_ref()) {
3829
3922
  return v;
3830
3923
  }
3831
- arr_builtins::reduce(&bv, &cb, &init)
3924
+ arr_builtins::reduce(&bv, &cb, init.as_ref())
3832
3925
  }),
3926
+ "reduceRight" => make_native_fn(move |args| {
3927
+ let cb = args.first().cloned().unwrap_or(Value::Null);
3928
+ let init = args.get(1).cloned();
3929
+ arr_builtins::reduce_right(&bv, &cb, init.as_ref())
3930
+ }),
3931
+ "keys" => make_native_fn(move |_| arr_builtins::keys(&bv)),
3932
+ "values" => make_native_fn(move |_| arr_builtins::values(&bv)),
3933
+ "entries" => make_native_fn(move |_| arr_builtins::entries(&bv)),
3833
3934
  "forEach" => make_native_fn(move |args| {
3834
3935
  let cb = args.first().cloned().unwrap_or(Value::Null);
3835
3936
  #[cfg(not(target_arch = "wasm32"))]
@@ -3879,6 +3980,21 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
3879
3980
  arr_builtins::flat_map(&bv, &cb)
3880
3981
  }),
3881
3982
  "reverse" => make_native_fn(move |_| arr_builtins::reverse(&bv)),
3983
+ "toReversed" => make_native_fn(move |_| arr_builtins::to_reversed(&bv)),
3984
+ "toSorted" => {
3985
+ make_native_fn(move |args| arr_builtins::to_sorted(&bv, args.first()))
3986
+ }
3987
+ "with" => make_native_fn(move |args| {
3988
+ let i = args.first().cloned().unwrap_or(Value::Null);
3989
+ let v = args.get(1).cloned().unwrap_or(Value::Null);
3990
+ arr_builtins::with(&bv, &i, &v)
3991
+ }),
3992
+ "toSpliced" => make_native_fn(move |args| {
3993
+ let start = args.first().cloned().unwrap_or(Value::Null);
3994
+ let dc = args.get(1).cloned();
3995
+ let items: Vec<Value> = args.iter().skip(2).cloned().collect();
3996
+ arr_builtins::to_spliced(&bv, &start, dc.as_ref(), &items)
3997
+ }),
3882
3998
  "fill" => make_native_fn(move |args| {
3883
3999
  let v = args.first().cloned().unwrap_or(Value::Null);
3884
4000
  let s = args.get(1).cloned().unwrap_or(Value::Null);
@@ -3893,7 +4009,17 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
3893
4009
  "concat" => make_native_fn(move |args| arr_builtins::concat(&bv, args)),
3894
4010
  "indexOf" => make_native_fn(move |args| {
3895
4011
  let s = args.first().cloned().unwrap_or(Value::Null);
3896
- arr_builtins::index_of(&bv, &s)
4012
+ arr_builtins::index_of(&bv, &s, args.get(1))
4013
+ }),
4014
+ "lastIndexOf" => make_native_fn(move |args| {
4015
+ let s = args.first().cloned().unwrap_or(Value::Null);
4016
+ arr_builtins::last_index_of(&bv, &s, args.get(1))
4017
+ }),
4018
+ "copyWithin" => make_native_fn(move |args| {
4019
+ let t = args.first().cloned().unwrap_or(Value::Null);
4020
+ let s = args.get(1).cloned().unwrap_or(Value::Null);
4021
+ let e = args.get(2).cloned().unwrap_or(Value::Null);
4022
+ arr_builtins::copy_within(&bv, &t, &s, &e)
3897
4023
  }),
3898
4024
  "includes" => make_native_fn(move |args| {
3899
4025
  let s = args.first().cloned().unwrap_or(Value::Null);
@@ -3966,7 +4092,17 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
3966
4092
  }),
3967
4093
  "indexOf" => make_native_fn(move |args: &[Value]| {
3968
4094
  let search = args.first().unwrap_or(&Value::Null);
3969
- arr_builtins::index_of(&Value::Array(a_clone.clone()), search)
4095
+ arr_builtins::index_of(&Value::Array(a_clone.clone()), search, args.get(1))
4096
+ }),
4097
+ "lastIndexOf" => make_native_fn(move |args: &[Value]| {
4098
+ let search = args.first().unwrap_or(&Value::Null);
4099
+ arr_builtins::last_index_of(&Value::Array(a_clone.clone()), search, args.get(1))
4100
+ }),
4101
+ "copyWithin" => make_native_fn(move |args: &[Value]| {
4102
+ let t = args.first().cloned().unwrap_or(Value::Null);
4103
+ let s = args.get(1).cloned().unwrap_or(Value::Null);
4104
+ let e = args.get(2).cloned().unwrap_or(Value::Null);
4105
+ arr_builtins::copy_within(&Value::Array(a_clone.clone()), &t, &s, &e)
3970
4106
  }),
3971
4107
  "includes" => make_native_fn(move |args: &[Value]| {
3972
4108
  let search = args.first().unwrap_or(&Value::Null);
@@ -3995,14 +4131,22 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
3995
4131
  }),
3996
4132
  "reduce" => make_native_fn(move |args: &[Value]| {
3997
4133
  let cb = args.first().cloned().unwrap_or(Value::Null);
3998
- let init = args.get(1).cloned().unwrap_or(Value::Null);
4134
+ let init = args.get(1).cloned(); // None = no initial value (empty array → throw)
3999
4135
  let arr = Value::Array(a_clone.clone());
4000
4136
  #[cfg(not(target_arch = "wasm32"))]
4001
- if let Some(v) = hof_fusion::reduce(&arr, &cb, &init) {
4137
+ if let Some(v) = hof_fusion::reduce(&arr, &cb, init.as_ref()) {
4002
4138
  return v;
4003
4139
  }
4004
- arr_builtins::reduce(&arr, &cb, &init)
4140
+ arr_builtins::reduce(&arr, &cb, init.as_ref())
4005
4141
  }),
4142
+ "reduceRight" => make_native_fn(move |args: &[Value]| {
4143
+ let cb = args.first().cloned().unwrap_or(Value::Null);
4144
+ let init = args.get(1).cloned();
4145
+ arr_builtins::reduce_right(&Value::Array(a_clone.clone()), &cb, init.as_ref())
4146
+ }),
4147
+ "keys" => make_native_fn(move |_: &[Value]| arr_builtins::keys(&Value::Array(a_clone.clone()))),
4148
+ "values" => make_native_fn(move |_: &[Value]| arr_builtins::values(&Value::Array(a_clone.clone()))),
4149
+ "entries" => make_native_fn(move |_: &[Value]| arr_builtins::entries(&Value::Array(a_clone.clone()))),
4006
4150
  "forEach" => make_native_fn(move |args: &[Value]| {
4007
4151
  let cb = args.first().cloned().unwrap_or(Value::Null);
4008
4152
  let arr = Value::Array(a_clone.clone());
@@ -4070,6 +4214,28 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
4070
4214
  &items,
4071
4215
  )
4072
4216
  }),
4217
+ "toReversed" => make_native_fn(move |_: &[Value]| {
4218
+ arr_builtins::to_reversed(&Value::Array(a_clone.clone()))
4219
+ }),
4220
+ "toSorted" => make_native_fn(move |args: &[Value]| {
4221
+ arr_builtins::to_sorted(&Value::Array(a_clone.clone()), args.first())
4222
+ }),
4223
+ "with" => make_native_fn(move |args: &[Value]| {
4224
+ let i = args.first().cloned().unwrap_or(Value::Null);
4225
+ let v = args.get(1).cloned().unwrap_or(Value::Null);
4226
+ arr_builtins::with(&Value::Array(a_clone.clone()), &i, &v)
4227
+ }),
4228
+ "toSpliced" => make_native_fn(move |args: &[Value]| {
4229
+ let start = args.first().unwrap_or(&Value::Null);
4230
+ let delete_count = args.get(1).map(|v| v as &Value);
4231
+ let items: Vec<Value> = args.get(2..).unwrap_or(&[]).to_vec();
4232
+ arr_builtins::to_spliced(
4233
+ &Value::Array(a_clone.clone()),
4234
+ start,
4235
+ delete_count,
4236
+ &items,
4237
+ )
4238
+ }),
4073
4239
  _ => return Err(format!("Property '{}' not found", key)),
4074
4240
  };
4075
4241
  Ok(Value::Function(method))
@@ -4136,6 +4302,16 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
4136
4302
  "trim" => make_native_fn(move |_args: &[Value]| {
4137
4303
  str_builtins::trim(&Value::String(s_clone.clone()))
4138
4304
  }),
4305
+ "trimStart" => make_native_fn(move |_args: &[Value]| {
4306
+ str_builtins::trim_start(&Value::String(s_clone.clone()))
4307
+ }),
4308
+ "trimEnd" => make_native_fn(move |_args: &[Value]| {
4309
+ str_builtins::trim_end(&Value::String(s_clone.clone()))
4310
+ }),
4311
+ "normalize" => make_native_fn(move |args: &[Value]| {
4312
+ let form = args.first().cloned().unwrap_or(Value::Null);
4313
+ str_builtins::normalize(&Value::String(s_clone.clone()), &form)
4314
+ }),
4139
4315
  "toUpperCase" => make_native_fn(move |_args: &[Value]| {
4140
4316
  str_builtins::to_upper_case(&Value::String(s_clone.clone()))
4141
4317
  }),
@@ -4144,11 +4320,11 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
4144
4320
  }),
4145
4321
  "startsWith" => make_native_fn(move |args: &[Value]| {
4146
4322
  let search = args.first().unwrap_or(&Value::Null);
4147
- str_builtins::starts_with(&Value::String(s_clone.clone()), search)
4323
+ str_builtins::starts_with(&Value::String(s_clone.clone()), search, args.get(1))
4148
4324
  }),
4149
4325
  "endsWith" => make_native_fn(move |args: &[Value]| {
4150
4326
  let search = args.first().unwrap_or(&Value::Null);
4151
- str_builtins::ends_with(&Value::String(s_clone.clone()), search)
4327
+ str_builtins::ends_with(&Value::String(s_clone.clone()), search, args.get(1))
4152
4328
  }),
4153
4329
  "replace" => make_native_fn(move |args: &[Value]| {
4154
4330
  let search = args.first().unwrap_or(&Value::Null);
@@ -4176,6 +4352,11 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
4176
4352
  tishlang_runtime::string_match_regex(&Value::String(s_clone.clone()), re)
4177
4353
  }),
4178
4354
  #[cfg(feature = "regex")]
4355
+ "matchAll" => make_native_fn(move |args: &[Value]| {
4356
+ let re = args.first().unwrap_or(&Value::Null);
4357
+ tishlang_runtime::string_match_all_regex(&Value::String(s_clone.clone()), re)
4358
+ }),
4359
+ #[cfg(feature = "regex")]
4179
4360
  "search" => make_native_fn(move |args: &[Value]| {
4180
4361
  let re = args.first().unwrap_or(&Value::Null);
4181
4362
  tishlang_runtime::string_search_regex(&Value::String(s_clone.clone()), re)
@@ -4192,6 +4373,15 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
4192
4373
  let idx = args.first().unwrap_or(&Value::Null);
4193
4374
  str_builtins::char_code_at(&Value::String(s_clone.clone()), idx)
4194
4375
  }),
4376
+ "codePointAt" => make_native_fn(move |args: &[Value]| {
4377
+ let idx = args.first().unwrap_or(&Value::Null);
4378
+ str_builtins::code_point_at(&Value::String(s_clone.clone()), idx)
4379
+ }),
4380
+ "substr" => make_native_fn(move |args: &[Value]| {
4381
+ let start = args.first().cloned().unwrap_or(Value::Null);
4382
+ let length = args.get(1).cloned().unwrap_or(Value::Null);
4383
+ str_builtins::substr(&Value::String(s_clone.clone()), &start, &length)
4384
+ }),
4195
4385
  "repeat" => make_native_fn(move |args: &[Value]| {
4196
4386
  let count = args.first().unwrap_or(&Value::Null);
4197
4387
  str_builtins::repeat(&Value::String(s_clone.clone()), count)
@@ -4219,6 +4409,14 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
4219
4409
  let digits = args.first().unwrap_or(&Value::Null);
4220
4410
  num_builtins::to_fixed(&Value::Number(n_val), digits)
4221
4411
  }),
4412
+ "toExponential" => make_native_fn(move |args: &[Value]| {
4413
+ let digits = args.first().unwrap_or(&Value::Null);
4414
+ num_builtins::to_exponential(&Value::Number(n_val), digits)
4415
+ }),
4416
+ "toPrecision" => make_native_fn(move |args: &[Value]| {
4417
+ let precision = args.first().unwrap_or(&Value::Null);
4418
+ num_builtins::to_precision(&Value::Number(n_val), precision)
4419
+ }),
4222
4420
  "toString" => make_native_fn(move |args: &[Value]| {
4223
4421
  let radix = args.first().unwrap_or(&Value::Null);
4224
4422
  num_builtins::to_string(&Value::Number(n_val), radix)
@@ -4333,12 +4531,13 @@ fn get_index(obj: &Value, idx: &Value) -> Result<Value, String> {
4333
4531
  Value::NumberArray(a) => {
4334
4532
  let i = match idx {
4335
4533
  Value::Number(n) => *n as usize,
4336
- _ => {
4337
- return Err(format!(
4338
- "Array index must be number, got {}",
4339
- idx.type_name()
4340
- ))
4341
- }
4534
+ // A canonical integer string key ("0","1",…) indexes the array (#432); any other key
4535
+ // reads Null (JS reads a missing property as undefined) rather than throwing.
4536
+ Value::String(s) => match tishlang_core::str_to_array_index(s) {
4537
+ Some(i) => i,
4538
+ None => return Ok(Value::Null),
4539
+ },
4540
+ _ => return Ok(Value::Null),
4342
4541
  };
4343
4542
  // NaN is used as the hole marker (sparse-array positions); reads return Null.
4344
4543
  Ok(a.borrow()
@@ -4355,12 +4554,12 @@ fn get_index(obj: &Value, idx: &Value) -> Result<Value, String> {
4355
4554
  Value::Array(a) => {
4356
4555
  let i = match idx {
4357
4556
  Value::Number(n) => *n as usize,
4358
- _ => {
4359
- return Err(format!(
4360
- "Array index must be number, got {}",
4361
- idx.type_name()
4362
- ));
4363
- }
4557
+ // Canonical integer string key → index (#432); any other key reads Null, not a throw.
4558
+ Value::String(s) => match tishlang_core::str_to_array_index(s) {
4559
+ Some(i) => i,
4560
+ None => return Ok(Value::Null),
4561
+ },
4562
+ _ => return Ok(Value::Null),
4364
4563
  };
4365
4564
  Ok(a.borrow().get(i).cloned().unwrap_or(Value::Null))
4366
4565
  }
@@ -4369,10 +4568,9 @@ fn get_index(obj: &Value, idx: &Value) -> Result<Value, String> {
4369
4568
  Value::Number(n) => {
4370
4569
  let n = *n;
4371
4570
  if n < 0.0 || n.fract() != 0.0 {
4372
- return Err(format!(
4373
- "String index must be non-negative integer, got {}",
4374
- n
4375
- ));
4571
+ // A negative / non-integer string index reads `null` (JS `undefined`), NOT a
4572
+ // throw — interp and native already return null; the vm must agree. (#437)
4573
+ return Ok(Value::Null);
4376
4574
  }
4377
4575
  n as usize
4378
4576
  }
@@ -4387,7 +4585,8 @@ fn get_index(obj: &Value, idx: &Value) -> Result<Value, String> {
4387
4585
  // O(n) `chars().count()` pre-check (#203).
4388
4586
  match str_builtins::nth_char(s, i) {
4389
4587
  Some(c) => Ok(Value::String(tishlang_core::ArcStr::from(c.to_string()))),
4390
- None => Err("Index out of bounds".to_string()),
4588
+ // Past the end → `null` (JS `undefined`), matching interp/native, not a throw. (#437)
4589
+ None => Ok(Value::Null),
4391
4590
  }
4392
4591
  }
4393
4592
  // A missing own property returns `null`, not a thrown error — matching dot reads
@@ -4447,6 +4646,12 @@ fn set_index(obj: &Value, idx: &Value, val: Value) -> Result<(), String> {
4447
4646
  Value::NumberArray(a) => {
4448
4647
  let i = match idx {
4449
4648
  Value::Number(n) => *n as usize,
4649
+ // Canonical integer string key → index (#432); a non-index string key can't be stored
4650
+ // in a Vec-backed array, so the write is a no-op (rather than a throw).
4651
+ Value::String(s) => match tishlang_core::str_to_array_index(s) {
4652
+ Some(i) => i,
4653
+ None => return Ok(()),
4654
+ },
4450
4655
  _ => {
4451
4656
  return Err(format!(
4452
4657
  "Array index must be number, got {}",
@@ -4487,6 +4692,11 @@ fn set_index(obj: &Value, idx: &Value, val: Value) -> Result<(), String> {
4487
4692
  Value::Array(a) => {
4488
4693
  let i = match idx {
4489
4694
  Value::Number(n) => *n as usize,
4695
+ // Canonical integer string key → index (#432); a non-index string key is a no-op write.
4696
+ Value::String(s) => match tishlang_core::str_to_array_index(s) {
4697
+ Some(i) => i,
4698
+ None => return Ok(()),
4699
+ },
4490
4700
  _ => {
4491
4701
  return Err(format!(
4492
4702
  "Array index must be number, got {}",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tishlang/tish-format",
3
- "version": "2.16.13",
3
+ "version": "2.35.0",
4
4
  "description": "Tish code formatter",
5
5
  "license": "PIF",
6
6
  "repository": {
Binary file
Binary file
Binary file
Binary file
Binary file