@tishlang/tish-format 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-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/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/Cargo.toml +1 -1
  35. package/crates/tish_fmt/src/lib.rs +23 -4
  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-fmt +0 -0
  49. package/platform/darwin-x64/tish-fmt +0 -0
  50. package/platform/linux-arm64/tish-fmt +0 -0
  51. package/platform/linux-x64/tish-fmt +0 -0
  52. package/platform/win32-x64/tish-fmt.exe +0 -0
@@ -79,7 +79,12 @@ impl UsageAnalyzer {
79
79
  }
80
80
  self.analyze_statement(body);
81
81
  }
82
- Statement::ForOf { iterable, body, .. } => {
82
+ Statement::ForOf { iterable, body, .. }
83
+ | Statement::ForIn {
84
+ object: iterable,
85
+ body,
86
+ ..
87
+ } => {
83
88
  self.analyze_expr(iterable);
84
89
  self.analyze_statement(body);
85
90
  }
@@ -330,6 +335,7 @@ fn program_uses_async(program: &Program) -> bool {
330
335
  Statement::While { body, .. }
331
336
  | Statement::For { body, .. }
332
337
  | Statement::ForOf { body, .. }
338
+ | Statement::ForIn { body, .. }
333
339
  | Statement::DoWhile { body, .. } => stmt_has_async(body),
334
340
  Statement::Switch {
335
341
  cases,
@@ -464,7 +470,12 @@ fn program_uses_async(program: &Program) -> bool {
464
470
  || update.as_ref().is_some_and(expr_has_await)
465
471
  || stmt_has_await(body)
466
472
  }
467
- Statement::ForOf { iterable, body, .. } => {
473
+ Statement::ForOf { iterable, body, .. }
474
+ | Statement::ForIn {
475
+ object: iterable,
476
+ body,
477
+ ..
478
+ } => {
468
479
  expr_has_await(iterable) || stmt_has_await(body)
469
480
  }
470
481
  Statement::Return { value, .. } => value.as_ref().is_some_and(expr_has_await),
@@ -917,6 +928,13 @@ pub(crate) struct Codegen {
917
928
  module_const_f64_aliases: std::collections::HashMap<String, String>,
918
929
  /// #181: locals initialized with `new Map()` — direct `map_has`/`get`/`set`/`values` dispatch.
919
930
  map_instance_locals: std::collections::HashSet<String>,
931
+ /// #456 regression guard: locals initialized with `new Map()` OR `new Set()`. Arrays have no
932
+ /// runtime method table (`get_prop` returns Null for a method name), so `keys`/`values`/`entries`
933
+ /// MUST use the array fast-path — but that same fast-path would hijack a Map/Set's own
934
+ /// `keys`/`values`/`entries` (which DO resolve as bound methods via `get_prop`). So the array
935
+ /// fast-path fires by default and is suppressed only for a known collection local, which then
936
+ /// falls through to the generic bound-method call.
937
+ collection_instance_locals: std::collections::HashSet<String>,
920
938
  /// String-scan hoist: inside a `for (i; i < s.length; …)` loop over an invariant native `String`
921
939
  /// `s`, the loop body's `s.charCodeAt/charAt/at(i)` route to O(1) `Vec<char>` slice ops instead
922
940
  /// of `str_char_code_at`'s `chars().nth(i)` = O(i). Maps the source ident → the hoisted `_scN`
@@ -1143,6 +1161,7 @@ impl Codegen {
1143
1161
  module_const_f64_cum: std::collections::HashMap::new(),
1144
1162
  module_const_f64_aliases: std::collections::HashMap::new(),
1145
1163
  map_instance_locals: std::collections::HashSet::new(),
1164
+ collection_instance_locals: std::collections::HashSet::new(),
1146
1165
  hoisted_string_chars: std::collections::HashMap::new(),
1147
1166
  native_fns: std::collections::HashSet::new(),
1148
1167
  native_fn_body_emit: false,
@@ -1308,6 +1327,7 @@ impl Codegen {
1308
1327
  }
1309
1328
  Statement::For { body, .. }
1310
1329
  | Statement::ForOf { body, .. }
1330
+ | Statement::ForIn { body, .. }
1311
1331
  | Statement::While { body, .. }
1312
1332
  | Statement::DoWhile { body, .. } => {
1313
1333
  Self::walk_type_aliases(std::slice::from_ref(body.as_ref()), out);
@@ -1590,13 +1610,59 @@ impl Codegen {
1590
1610
  fn builtin_native_module_rust_init(&self, spec: &str, export_name: &str) -> Option<String> {
1591
1611
  let init = match spec {
1592
1612
  "tish:fs" if self.has_feature("fs") => match export_name {
1593
- "readFile" => Some("Value::native(|args: &[Value]| tish_read_file(args))"),
1594
- "writeFile" => Some("Value::native(|args: &[Value]| tish_write_file(args))"),
1613
+ // tish names → the dual fs_ext functions (sync, or Node callback if a fn is passed)
1614
+ "readFile" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::read_file(args))"),
1615
+ "writeFile" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::write_file(args))"),
1595
1616
  "fileExists" => Some("Value::native(|args: &[Value]| tish_file_exists(args))"),
1596
1617
  "isDir" => Some("Value::native(|args: &[Value]| tish_is_dir(args))"),
1597
- "readDir" => Some("Value::native(|args: &[Value]| tish_read_dir(args))"),
1598
- "readFileBytes" => Some("Value::native(|args: &[Value]| tish_read_file_bytes(args))"),
1599
- "mkdir" => Some("Value::native(|args: &[Value]| tish_mkdir(args))"),
1618
+ "readDir" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::readdir(args))"),
1619
+ "readFileBytes" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::read_file_bytes(args))"),
1620
+ // Node-compatible names + new methods (sync) — issue #122
1621
+ "readFileSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::read_file(args))"),
1622
+ "readFileBytesSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::read_file_bytes(args))"),
1623
+ "writeFileSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::write_file(args))"),
1624
+ "appendFile" | "appendFileSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::append_file(args))"),
1625
+ "existsSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::exists(args))"),
1626
+ "mkdir" | "mkdirSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::mkdir(args))"),
1627
+ "readdir" | "readdirSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::readdir(args))"),
1628
+ "stat" | "statSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::stat(args))"),
1629
+ "lstat" | "lstatSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::lstat(args))"),
1630
+ "rm" | "rmSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::rm(args))"),
1631
+ "rmdir" | "rmdirSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::rmdir(args))"),
1632
+ "unlink" | "unlinkSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::unlink(args))"),
1633
+ "rename" | "renameSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::rename(args))"),
1634
+ "copyFile" | "copyFileSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::copy_file(args))"),
1635
+ "realpath" | "realpathSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::realpath(args))"),
1636
+ "readlink" | "readlinkSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::readlink(args))"),
1637
+ "truncate" | "truncateSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::truncate(args))"),
1638
+ "mkdtemp" | "mkdtempSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::mkdtemp(args))"),
1639
+ "cp" | "cpSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::cp(args))"),
1640
+ "access" | "accessSync" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::access(args))"),
1641
+ "constants" => Some("tishlang_runtime::fs_ext::constants()"),
1642
+ _ => None,
1643
+ },
1644
+ "tish:fs/promises" if self.has_feature("fs") => match export_name {
1645
+ "readFile" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::read_file_promise(args))"),
1646
+ "readFileBytes" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::read_file_bytes_promise(args))"),
1647
+ "writeFile" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::write_file_promise(args))"),
1648
+ "appendFile" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::append_file_promise(args))"),
1649
+ "mkdir" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::mkdir_promise(args))"),
1650
+ "readdir" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::readdir_promise(args))"),
1651
+ "stat" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::stat_promise(args))"),
1652
+ "lstat" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::lstat_promise(args))"),
1653
+ "rm" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::rm_promise(args))"),
1654
+ "rmdir" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::rmdir_promise(args))"),
1655
+ "unlink" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::unlink_promise(args))"),
1656
+ "rename" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::rename_promise(args))"),
1657
+ "copyFile" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::copy_file_promise(args))"),
1658
+ "realpath" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::realpath_promise(args))"),
1659
+ "readlink" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::readlink_promise(args))"),
1660
+ "truncate" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::truncate_promise(args))"),
1661
+ "mkdtemp" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::mkdtemp_promise(args))"),
1662
+ "cp" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::cp_promise(args))"),
1663
+ "access" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::access_promise(args))"),
1664
+ "exists" => Some("Value::native(|args: &[Value]| tishlang_runtime::fs_ext::exists_promise(args))"),
1665
+ "constants" => Some("tishlang_runtime::fs_ext::constants()"),
1600
1666
  _ => None,
1601
1667
  },
1602
1668
  "tish:http" if self.has_feature("http") => match export_name {
@@ -1624,9 +1690,11 @@ impl Codegen {
1624
1690
  "cwd" => Some("Value::native(|args: &[Value]| tish_process_cwd(args))"),
1625
1691
  "exec" => Some("Value::native(|args: &[Value]| tish_process_exec(args))"),
1626
1692
  "execFile" => Some("Value::native(|args: &[Value]| tish_process_exec_file(args))"),
1693
+ "execCapture" => Some("Value::native(|args: &[Value]| tish_process_exec_capture(args))"),
1694
+ "execFileCapture" => Some("Value::native(|args: &[Value]| tish_process_exec_file_capture(args))"),
1627
1695
  "argv" => Some("Value::Array(VmRef::new(std::env::args().map(|s| Value::String(s.into())).collect()))"),
1628
1696
  "env" => Some("Value::object(std::env::vars().map(|(k,v)| (Arc::from(k.as_str()), Value::String(v.into()))).collect())"),
1629
- "process" => Some("{ let mut m = ObjectMap::default(); m.insert(Arc::from(\"exit\"), Value::native(|args: &[Value]| tish_process_exit(args))); m.insert(Arc::from(\"cwd\"), Value::native(|args: &[Value]| tish_process_cwd(args))); m.insert(Arc::from(\"exec\"), Value::native(|args: &[Value]| tish_process_exec(args))); m.insert(Arc::from(\"execFile\"), Value::native(|args: &[Value]| tish_process_exec_file(args))); m.insert(Arc::from(\"argv\"), Value::Array(VmRef::new(std::env::args().map(|s| Value::String(s.into())).collect()))); m.insert(Arc::from(\"env\"), Value::object(std::env::vars().map(|(k,v)| (Arc::from(k.as_str()), Value::String(v.into()))).collect::<ObjectMap>())); Value::object(m) }"),
1697
+ "process" => Some("{ let mut m = ObjectMap::default(); m.insert(Arc::from(\"exit\"), Value::native(|args: &[Value]| tish_process_exit(args))); m.insert(Arc::from(\"cwd\"), Value::native(|args: &[Value]| tish_process_cwd(args))); m.insert(Arc::from(\"exec\"), Value::native(|args: &[Value]| tish_process_exec(args))); m.insert(Arc::from(\"execFile\"), Value::native(|args: &[Value]| tish_process_exec_file(args))); m.insert(Arc::from(\"execCapture\"), Value::native(|args: &[Value]| tish_process_exec_capture(args))); m.insert(Arc::from(\"execFileCapture\"), Value::native(|args: &[Value]| tish_process_exec_file_capture(args))); m.insert(Arc::from(\"argv\"), Value::Array(VmRef::new(std::env::args().map(|s| Value::String(s.into())).collect()))); m.insert(Arc::from(\"env\"), Value::object(std::env::vars().map(|(k,v)| (Arc::from(k.as_str()), Value::String(v.into()))).collect::<ObjectMap>())); Value::object(m) }"),
1630
1698
  _ => None,
1631
1699
  },
1632
1700
  "tish:ws" if self.has_feature("ws") => match export_name {
@@ -2175,12 +2243,12 @@ impl Codegen {
2175
2243
  self.write("use std::cell::RefCell;\n");
2176
2244
  self.write("use std::rc::Rc;\n");
2177
2245
  self.write("use std::sync::Arc;\n");
2178
- self.write("use tishlang_runtime::{console_debug as tish_console_debug, console_info as tish_console_info, console_log as tish_console_log, console_warn as tish_console_warn, console_error as tish_console_error, boolean as tish_boolean, decode_uri as tish_decode_uri, encode_uri as tish_encode_uri, string_escape_html_impl as tish_escape_html, in_operator as tish_in_operator, is_finite as tish_is_finite, is_nan as tish_is_nan, json_parse as tish_json_parse, json_stringify as tish_json_stringify, math_abs as tish_math_abs, math_ceil as tish_math_ceil, math_floor as tish_math_floor, math_max as tish_math_max, math_min as tish_math_min, math_round as tish_math_round, math_sqrt as tish_math_sqrt, parse_float as tish_parse_float, parse_int as tish_parse_int, math_random as tish_math_random, math_pow as tish_math_pow, math_sin as tish_math_sin, math_cos as tish_math_cos, math_tan as tish_math_tan, math_log as tish_math_log, math_exp as tish_math_exp, math_sign as tish_math_sign, math_trunc as tish_math_trunc, math_imul as tish_math_imul, math_sinh as tish_math_sinh, math_cosh as tish_math_cosh, math_tanh as tish_math_tanh, math_asinh as tish_math_asinh, math_acosh as tish_math_acosh, math_atanh as tish_math_atanh, math_cbrt as tish_math_cbrt, math_log2 as tish_math_log2, math_log10 as tish_math_log10, math_hypot as tish_math_hypot, math_atan2 as tish_math_atan2, math_asin as tish_math_asin, math_acos as tish_math_acos, math_atan as tish_math_atan, array_is_array as tish_array_is_array, array_construct as tish_array_construct, string_from_char_code as tish_string_from_char_code, string_convert as tish_string_convert, number_convert as tish_number_convert, object_assign as tish_object_assign, object_keys as tish_object_keys, object_values as tish_object_values, object_entries as tish_object_entries, object_from_entries as tish_object_from_entries, symbol_object as tish_symbol_object, tish_construct, tish_error_constructor, tish_date_constructor, tish_set_constructor, tish_map_constructor, map_get as tish_map_get, map_has as tish_map_has, map_set as tish_map_set, map_values as tish_map_values, tish_float64_array_constructor, tish_float32_array_constructor, tish_int8_array_constructor, tish_uint8_array_constructor, tish_uint8_clamped_array_constructor, tish_int16_array_constructor, tish_uint16_array_constructor, tish_int32_array_constructor, tish_uint32_array_constructor, tish_audio_context_constructor, ObjectMap, ObjectData, PropMap, TishError, Value, VmRef};\n");
2246
+ self.write("use tishlang_runtime::{console_debug as tish_console_debug, console_info as tish_console_info, console_log as tish_console_log, console_warn as tish_console_warn, console_error as tish_console_error, boolean as tish_boolean, decode_uri as tish_decode_uri, encode_uri as tish_encode_uri, encode_uri_component as tish_encode_uri_component, decode_uri_component as tish_decode_uri_component, string_escape_html_impl as tish_escape_html, in_operator as tish_in_operator, is_finite as tish_is_finite, is_nan as tish_is_nan, json_parse as tish_json_parse, json_stringify as tish_json_stringify, math_abs as tish_math_abs, math_ceil as tish_math_ceil, math_floor as tish_math_floor, math_max as tish_math_max, math_min as tish_math_min, math_round as tish_math_round, math_sqrt as tish_math_sqrt, parse_float as tish_parse_float, parse_int as tish_parse_int, math_random as tish_math_random, math_pow as tish_math_pow, math_sin as tish_math_sin, math_cos as tish_math_cos, math_tan as tish_math_tan, math_log as tish_math_log, math_exp as tish_math_exp, math_sign as tish_math_sign, math_trunc as tish_math_trunc, math_imul as tish_math_imul, math_sinh as tish_math_sinh, math_cosh as tish_math_cosh, math_tanh as tish_math_tanh, math_asinh as tish_math_asinh, math_acosh as tish_math_acosh, math_atanh as tish_math_atanh, math_cbrt as tish_math_cbrt, math_log2 as tish_math_log2, math_log10 as tish_math_log10, math_expm1 as tish_math_expm1, math_log1p as tish_math_log1p, math_clz32 as tish_math_clz32, math_fround as tish_math_fround, math_hypot as tish_math_hypot, math_atan2 as tish_math_atan2, math_asin as tish_math_asin, math_acos as tish_math_acos, math_atan as tish_math_atan, array_is_array as tish_array_is_array, array_of as tish_array_of, array_from as tish_array_from, object_is as tish_object_is, object_has_own as tish_object_has_own, structured_clone as tish_structured_clone, array_construct as tish_array_construct, string_from_char_code as tish_string_from_char_code, string_convert as tish_string_convert, number_convert as tish_number_convert, number_is_integer as tish_number_is_integer, number_is_safe_integer as tish_number_is_safe_integer, number_is_nan as tish_number_is_nan, number_is_finite as tish_number_is_finite, object_assign as tish_object_assign, object_keys as tish_object_keys, object_values as tish_object_values, object_entries as tish_object_entries, object_from_entries as tish_object_from_entries, symbol_object as tish_symbol_object, tish_construct, tish_error_constructor, tish_date_constructor, tish_set_constructor, tish_map_constructor, map_get as tish_map_get, map_has as tish_map_has, map_set as tish_map_set, map_values as tish_map_values, tish_float64_array_constructor, tish_float32_array_constructor, tish_int8_array_constructor, tish_uint8_array_constructor, tish_uint8_clamped_array_constructor, tish_int16_array_constructor, tish_uint16_array_constructor, tish_int32_array_constructor, tish_uint32_array_constructor, tish_audio_context_constructor, ObjectMap, ObjectData, PropMap, TishError, Value, VmRef};\n");
2179
2247
  if self.program_has_jsx {
2180
2248
  self.write("use tishlang_ui::{fragment_value, install_thread_local_host, native_create_root, native_use_state, ui_h, ui_text, HeadlessHost};\n");
2181
2249
  }
2182
2250
  if self.has_feature("process") {
2183
- self.write("use tishlang_runtime::{process_exit as tish_process_exit, process_cwd as tish_process_cwd, process_exec as tish_process_exec, process_exec_file as tish_process_exec_file};\n");
2251
+ self.write("use tishlang_runtime::{process_exit as tish_process_exit, process_cwd as tish_process_cwd, process_exec as tish_process_exec, process_exec_file as tish_process_exec_file, process_exec_capture as tish_process_exec_capture, process_exec_file_capture as tish_process_exec_file_capture};\n");
2184
2252
  }
2185
2253
  if self.has_feature("timers") {
2186
2254
  self.write("use tishlang_runtime::{timer_set_timeout as tish_timer_set_timeout, timer_clear_timeout as tish_timer_clear_timeout, timer_set_interval as tish_timer_set_interval, timer_clear_interval as tish_timer_clear_interval};\n");
@@ -2195,6 +2263,11 @@ impl Codegen {
2195
2263
  self.write("use tishlang_runtime::{fetch_promise as tish_fetch_promise, fetch_all_promise as tish_fetch_all_promise, http_serve as tish_http_serve};\n");
2196
2264
  }
2197
2265
  }
2266
+ // `fs/promises` (and any promise-using async program) needs the await machinery even
2267
+ // without http. `fs` implies the runtime `promise` feature, so these resolve.
2268
+ if self.is_async && !self.has_feature("http") && self.has_feature("fs") {
2269
+ self.write("use tishlang_runtime::{promise_object as tish_promise_object, await_promise as tish_await_promise, await_promise_throw as tish_await_promise_throw};\n");
2270
+ }
2198
2271
  if self.has_feature("fs") {
2199
2272
  self.write("use tishlang_runtime::{read_file as tish_read_file, read_file_bytes as tish_read_file_bytes, write_file as tish_write_file, file_exists as tish_file_exists, is_dir as tish_is_dir, read_dir as tish_read_dir, mkdir as tish_mkdir};\n");
2200
2273
  }
@@ -2378,6 +2451,9 @@ impl Codegen {
2378
2451
  self.writeln("let parseFloat = Value::native(|args: &[Value]| tish_parse_float(args));");
2379
2452
  self.writeln("let decodeURI = Value::native(|args: &[Value]| tish_decode_uri(args));");
2380
2453
  self.writeln("let encodeURI = Value::native(|args: &[Value]| tish_encode_uri(args));");
2454
+ self.writeln("let encodeURIComponent = Value::native(|args: &[Value]| tish_encode_uri_component(args));");
2455
+ self.writeln("let decodeURIComponent = Value::native(|args: &[Value]| tish_decode_uri_component(args));");
2456
+ self.writeln("let structuredClone = Value::native(|args: &[Value]| tish_structured_clone(args));");
2381
2457
  // `registerStaticRoute` calls the http-gated runtime fn, so only bind it when http is linked
2382
2458
  // (matches the conditional `use` above; otherwise non-http builds fail to resolve it).
2383
2459
  if self.has_feature("http") {
@@ -2439,6 +2515,10 @@ impl Codegen {
2439
2515
  ("cbrt", "tish_math_cbrt"),
2440
2516
  ("log2", "tish_math_log2"),
2441
2517
  ("log10", "tish_math_log10"),
2518
+ ("expm1", "tish_math_expm1"),
2519
+ ("log1p", "tish_math_log1p"),
2520
+ ("clz32", "tish_math_clz32"),
2521
+ ("fround", "tish_math_fround"),
2442
2522
  ("hypot", "tish_math_hypot"),
2443
2523
  ("atan2", "tish_math_atan2"),
2444
2524
  ("asin", "tish_math_asin"),
@@ -2467,6 +2547,12 @@ impl Codegen {
2467
2547
  self.writeln(
2468
2548
  "(Arc::from(\"isArray\"), Value::native(|args: &[Value]| tish_array_is_array(args))),",
2469
2549
  );
2550
+ self.writeln(
2551
+ "(Arc::from(\"of\"), Value::native(|args: &[Value]| tish_array_of(args))),",
2552
+ );
2553
+ self.writeln(
2554
+ "(Arc::from(\"from\"), Value::native(|args: &[Value]| tish_array_from(args))),",
2555
+ );
2470
2556
  // `Array(n)` / `new Array(n)` constructor (issue #72); `__call` covers both forms.
2471
2557
  self.writeln(
2472
2558
  "(Arc::from(\"__call\"), Value::native(|args: &[Value]| tish_array_construct(args))),",
@@ -2482,10 +2568,24 @@ impl Codegen {
2482
2568
  self.indent -= 1;
2483
2569
  self.writeln("]));");
2484
2570
 
2485
- // `Number(value)` coercion callable (issue #36).
2571
+ // `Number(value)` coercion callable (issue #36) + the `Number.*` statics.
2486
2572
  self.writeln("let Number = Value::object(ObjectMap::from([");
2487
2573
  self.indent += 1;
2488
2574
  self.writeln("(Arc::from(\"__call\"), Value::native(|args: &[Value]| tish_number_convert(args))),");
2575
+ self.writeln("(Arc::from(\"isInteger\"), Value::native(|args: &[Value]| tish_number_is_integer(args))),");
2576
+ self.writeln("(Arc::from(\"isSafeInteger\"), Value::native(|args: &[Value]| tish_number_is_safe_integer(args))),");
2577
+ self.writeln("(Arc::from(\"isNaN\"), Value::native(|args: &[Value]| tish_number_is_nan(args))),");
2578
+ self.writeln("(Arc::from(\"isFinite\"), Value::native(|args: &[Value]| tish_number_is_finite(args))),");
2579
+ self.writeln("(Arc::from(\"parseInt\"), Value::native(|args: &[Value]| tish_parse_int(args))),");
2580
+ self.writeln("(Arc::from(\"parseFloat\"), Value::native(|args: &[Value]| tish_parse_float(args))),");
2581
+ self.writeln("(Arc::from(\"MAX_SAFE_INTEGER\"), Value::Number(9007199254740991.0)),");
2582
+ self.writeln("(Arc::from(\"MIN_SAFE_INTEGER\"), Value::Number(-9007199254740991.0)),");
2583
+ self.writeln("(Arc::from(\"EPSILON\"), Value::Number(f64::EPSILON)),");
2584
+ self.writeln("(Arc::from(\"MAX_VALUE\"), Value::Number(f64::MAX)),");
2585
+ self.writeln("(Arc::from(\"MIN_VALUE\"), Value::Number(5e-324)),");
2586
+ self.writeln("(Arc::from(\"POSITIVE_INFINITY\"), Value::Number(f64::INFINITY)),");
2587
+ self.writeln("(Arc::from(\"NEGATIVE_INFINITY\"), Value::Number(f64::NEG_INFINITY)),");
2588
+ self.writeln("(Arc::from(\"NaN\"), Value::Number(f64::NAN)),");
2489
2589
  self.indent -= 1;
2490
2590
  self.writeln("]));");
2491
2591
 
@@ -2500,6 +2600,15 @@ impl Codegen {
2500
2600
  self.writeln(
2501
2601
  "(Arc::from(\"assign\"), Value::native(|args: &[Value]| tish_object_assign(args))),",
2502
2602
  );
2603
+ self.writeln(
2604
+ "(Arc::from(\"is\"), Value::native(|args: &[Value]| tish_object_is(args))),",
2605
+ );
2606
+ self.writeln(
2607
+ "(Arc::from(\"hasOwn\"), Value::native(|args: &[Value]| tish_object_has_own(args))),",
2608
+ );
2609
+ self.writeln(
2610
+ "(Arc::from(\"getOwnPropertyNames\"), Value::native(|args: &[Value]| tish_object_keys(args))),",
2611
+ );
2503
2612
  self.writeln(
2504
2613
  "(Arc::from(\"keys\"), Value::native(|args: &[Value]| tish_object_keys(args))),",
2505
2614
  );
@@ -2544,6 +2653,8 @@ impl Codegen {
2544
2653
  self.writeln("p.insert(Arc::from(\"cwd\"), Value::native(|args: &[Value]| tish_process_cwd(args)));");
2545
2654
  self.writeln("p.insert(Arc::from(\"exec\"), Value::native(|args: &[Value]| tish_process_exec(args)));");
2546
2655
  self.writeln("p.insert(Arc::from(\"execFile\"), Value::native(|args: &[Value]| tish_process_exec_file(args)));");
2656
+ self.writeln("p.insert(Arc::from(\"execCapture\"), Value::native(|args: &[Value]| tish_process_exec_capture(args)));");
2657
+ self.writeln("p.insert(Arc::from(\"execFileCapture\"), Value::native(|args: &[Value]| tish_process_exec_file_capture(args)));");
2547
2658
  self.writeln("let argv: Vec<Value> = std::env::args().map(|s| Value::String(s.into())).collect();");
2548
2659
  self.writeln("p.insert(Arc::from(\"argv\"), Value::Array(VmRef::new(argv)));");
2549
2660
  self.writeln("let mut env_obj = ObjectMap::default();");
@@ -4590,6 +4701,11 @@ impl Codegen {
4590
4701
  if Self::expr_is_map_construct(init_e) {
4591
4702
  self.map_instance_locals.insert(name.to_string());
4592
4703
  }
4704
+ // #456 regression: a `new Map()`/`new Set()` local must not have its
4705
+ // keys/values/entries hijacked by the array fast-path.
4706
+ if Self::expr_is_map_construct(init_e) || Self::expr_is_set_construct(init_e) {
4707
+ self.collection_instance_locals.insert(name.to_string());
4708
+ }
4593
4709
  }
4594
4710
 
4595
4711
  // `let cum = cumulative_nv(&probs)` inherits `probs`' fixed length for in-bounds reads.
@@ -4805,6 +4921,46 @@ impl Codegen {
4805
4921
  self.indent -= 1;
4806
4922
  self.writeln("}");
4807
4923
  }
4924
+ Statement::ForIn {
4925
+ name,
4926
+ name_span,
4927
+ object,
4928
+ body,
4929
+ span,
4930
+ } => {
4931
+ // Lower `for (let k in obj)` to `for (let k of Object.keys(obj))` (#413) and reuse the
4932
+ // native for-of emission. Object.keys yields the own enumerable keys the interpreter
4933
+ // enumerates (insertion order for objects, index strings for arrays, `[]` for a
4934
+ // non-object → `for (k in null)` no-ops).
4935
+ let dummy = Span {
4936
+ start: (0, 0),
4937
+ end: (0, 0),
4938
+ };
4939
+ let keys_call = Expr::Call {
4940
+ callee: Box::new(Expr::Member {
4941
+ object: Box::new(Expr::Ident {
4942
+ name: Arc::from("Object"),
4943
+ span: dummy,
4944
+ }),
4945
+ prop: MemberProp::Name {
4946
+ name: Arc::from("keys"),
4947
+ span: dummy,
4948
+ },
4949
+ optional: false,
4950
+ span: dummy,
4951
+ }),
4952
+ args: vec![CallArg::Expr(object.clone())],
4953
+ span: dummy,
4954
+ };
4955
+ let lowered = Statement::ForOf {
4956
+ name: Arc::clone(name),
4957
+ name_span: *name_span,
4958
+ iterable: keys_call,
4959
+ body: body.clone(),
4960
+ span: *span,
4961
+ };
4962
+ self.emit_statement(&lowered)?;
4963
+ }
4808
4964
  Statement::ForOf {
4809
4965
  name,
4810
4966
  iterable,
@@ -5591,6 +5747,9 @@ impl Codegen {
5591
5747
  "isFinite",
5592
5748
  "encodeURI",
5593
5749
  "decodeURI",
5750
+ "encodeURIComponent",
5751
+ "decodeURIComponent",
5752
+ "structuredClone",
5594
5753
  "htmlEscape",
5595
5754
  "registerStaticRoute",
5596
5755
  "String",
@@ -6728,9 +6887,15 @@ impl Codegen {
6728
6887
  return Ok(format!("tish_map_get({}, {})", map_ref, key_ref));
6729
6888
  }
6730
6889
  "set" if args.len() == 2 => {
6890
+ // Borrow the key/val places (like `get`/`has`) so a keyed local
6891
+ // isn't MOVED into the call and can still be used afterward — the
6892
+ // runtime `map_set` clones on insert (#442). `&Value` args also
6893
+ // route native-typed operands through the boxed path correctly.
6894
+ let key_ref = self.emit_borrowed_arg(&args[0])?;
6895
+ let val_ref = self.emit_borrowed_arg(&args[1])?;
6731
6896
  return Ok(format!(
6732
6897
  "tish_map_set({}, {}, {})",
6733
- map_ref, arg_exprs[0], arg_exprs[1]
6898
+ map_ref, key_ref, val_ref
6734
6899
  ));
6735
6900
  }
6736
6901
  "values" if args.is_empty() => {
@@ -6786,9 +6951,14 @@ impl Codegen {
6786
6951
  "indexOf" => {
6787
6952
  let search = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
6788
6953
  let from = arg_exprs.get(1).cloned().unwrap_or_else(|| "Value::Null".to_string());
6954
+ // Array `indexOf` takes `Option<&Value>` fromIndex; string keeps the &Value/Null form.
6955
+ let from_opt = match arg_exprs.get(1) {
6956
+ Some(e) => format!("Some(&{})", e),
6957
+ None => "None".to_string(),
6958
+ };
6789
6959
  return Ok(format!(
6790
- "{{ let _obj = ({}).clone(); match &_obj {{ Value::Array(_) => tishlang_runtime::array_index_of(&_obj, &{}), Value::String(_) => tishlang_runtime::string_index_of(&_obj, &{}, &{}), _ => Value::Number(-1.0) }} }}",
6791
- obj_expr, search, search, from
6960
+ "{{ let _obj = ({}).clone(); match &_obj {{ Value::Array(_) => tishlang_runtime::array_index_of(&_obj, &{}, {}), Value::String(_) => tishlang_runtime::string_index_of(&_obj, &{}, &{}), _ => Value::Number(-1.0) }} }}",
6961
+ obj_expr, search, from_opt, search, from
6792
6962
  ));
6793
6963
  }
6794
6964
  "lastIndexOf" => {
@@ -6798,9 +6968,23 @@ impl Codegen {
6798
6968
  } else {
6799
6969
  "Value::Number(f64::INFINITY)".to_string()
6800
6970
  };
6971
+ // Array lastIndexOf takes Option<&Value> fromIndex; string keeps &Value.
6972
+ let from_opt = match arg_exprs.get(1) {
6973
+ Some(e) => format!("Some(&{})", e),
6974
+ None => "None".to_string(),
6975
+ };
6976
+ return Ok(format!(
6977
+ "{{ let _obj = ({}).clone(); match &_obj {{ Value::Array(_) => tishlang_runtime::array_last_index_of(&_obj, &{}, {}), Value::String(_) => tishlang_runtime::string_last_index_of(&_obj, &{}, &{}), _ => Value::Number(-1.0) }} }}",
6978
+ obj_expr, search, from_opt, search, position
6979
+ ));
6980
+ }
6981
+ "copyWithin" => {
6982
+ let t = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
6983
+ let s = arg_exprs.get(1).cloned().unwrap_or_else(|| "Value::Null".to_string());
6984
+ let e = arg_exprs.get(2).cloned().unwrap_or_else(|| "Value::Null".to_string());
6801
6985
  return Ok(format!(
6802
- "{{ let _obj = ({}).clone(); match &_obj {{ Value::String(_) => tishlang_runtime::string_last_index_of(&_obj, &{}, &{}), _ => Value::Number(-1.0) }} }}",
6803
- obj_expr, search, position
6986
+ "tishlang_runtime::array_copy_within(&{}, &{}, &{}, &{})",
6987
+ obj_expr, t, s, e
6804
6988
  ));
6805
6989
  }
6806
6990
  "includes" => {
@@ -6821,6 +7005,9 @@ impl Codegen {
6821
7005
  "reverse" => {
6822
7006
  return Ok(format!("tishlang_runtime::array_reverse(&{})", obj_expr));
6823
7007
  }
7008
+ "toReversed" => {
7009
+ return Ok(format!("tishlang_runtime::array_to_reversed(&{})", obj_expr));
7010
+ }
6824
7011
  "fill" => {
6825
7012
  let value = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
6826
7013
  let start = arg_exprs.get(1).cloned().unwrap_or_else(|| "Value::Null".to_string());
@@ -6879,6 +7066,26 @@ impl Codegen {
6879
7066
  "trim" => {
6880
7067
  return Ok(format!("tishlang_runtime::string_trim(&{})", obj_expr));
6881
7068
  }
7069
+ "trimStart" => {
7070
+ return Ok(format!("tishlang_runtime::string_trim_start(&{})", obj_expr));
7071
+ }
7072
+ "trimEnd" => {
7073
+ return Ok(format!("tishlang_runtime::string_trim_end(&{})", obj_expr));
7074
+ }
7075
+ "normalize" => {
7076
+ let form = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
7077
+ return Ok(format!(
7078
+ "tishlang_runtime::string_normalize(&{}, &{})",
7079
+ obj_expr, form
7080
+ ));
7081
+ }
7082
+ "codePointAt" => {
7083
+ let idx = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
7084
+ return Ok(format!(
7085
+ "tishlang_runtime::string_code_point_at(&{}, &{})",
7086
+ obj_expr, idx
7087
+ ));
7088
+ }
6882
7089
  "toUpperCase" => {
6883
7090
  return Ok(format!("tishlang_runtime::string_to_upper_case(&{})", obj_expr));
6884
7091
  }
@@ -6887,16 +7094,24 @@ impl Codegen {
6887
7094
  }
6888
7095
  "startsWith" => {
6889
7096
  let search = arg_exprs.first().cloned().unwrap_or_else(|| "Value::String(\"\".into())".to_string());
7097
+ let pos = match arg_exprs.get(1) {
7098
+ Some(e) => format!("Some(&{})", e),
7099
+ None => "None".to_string(),
7100
+ };
6890
7101
  return Ok(format!(
6891
- "tishlang_runtime::string_starts_with(&{}, &{})",
6892
- obj_expr, search
7102
+ "tishlang_runtime::string_starts_with(&{}, &{}, {})",
7103
+ obj_expr, search, pos
6893
7104
  ));
6894
7105
  }
6895
7106
  "endsWith" => {
6896
7107
  let search = arg_exprs.first().cloned().unwrap_or_else(|| "Value::String(\"\".into())".to_string());
7108
+ let pos = match arg_exprs.get(1) {
7109
+ Some(e) => format!("Some(&{})", e),
7110
+ None => "None".to_string(),
7111
+ };
6897
7112
  return Ok(format!(
6898
- "tishlang_runtime::string_ends_with(&{}, &{})",
6899
- obj_expr, search
7113
+ "tishlang_runtime::string_ends_with(&{}, &{}, {})",
7114
+ obj_expr, search, pos
6900
7115
  ));
6901
7116
  }
6902
7117
  "replace" => {
@@ -6926,6 +7141,13 @@ impl Codegen {
6926
7141
  obj_expr, regexp
6927
7142
  ));
6928
7143
  }
7144
+ "matchAll" if self.has_feature("regex") => {
7145
+ let regexp = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
7146
+ return Ok(format!(
7147
+ "tishlang_runtime::string_match_all_regex(&{}, &{})",
7148
+ obj_expr, regexp
7149
+ ));
7150
+ }
6929
7151
  "search" if self.has_feature("regex") => {
6930
7152
  let regexp = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
6931
7153
  return Ok(format!(
@@ -6987,6 +7209,20 @@ impl Codegen {
6987
7209
  obj_expr, digits
6988
7210
  ));
6989
7211
  }
7212
+ "toExponential" => {
7213
+ let digits = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
7214
+ return Ok(format!(
7215
+ "tishlang_runtime::number_to_exponential(&{}, &{})",
7216
+ obj_expr, digits
7217
+ ));
7218
+ }
7219
+ "toPrecision" => {
7220
+ let precision = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
7221
+ return Ok(format!(
7222
+ "tishlang_runtime::number_to_precision(&{}, &{})",
7223
+ obj_expr, precision
7224
+ ));
7225
+ }
6990
7226
  "toString" => {
6991
7227
  let radix = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
6992
7228
  return Ok(format!(
@@ -7024,11 +7260,41 @@ impl Codegen {
7024
7260
  }
7025
7261
  }
7026
7262
  let callback = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
7263
+ // `array_reduce` takes `Option<&Value>` init: `None` = no initial value, so an
7264
+ // empty array with no init throws (per JS) instead of returning `null`.
7265
+ let init_arg = match arg_exprs.get(1) {
7266
+ Some(e) => format!("Some(&{})", e),
7267
+ None => "None".to_string(),
7268
+ };
7269
+ return Ok(format!(
7270
+ "tishlang_runtime::array_reduce(&{}, &{}, {})",
7271
+ obj_expr, callback, init_arg
7272
+ ));
7273
+ }
7274
+ "reduceRight" => {
7275
+ let callback = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
7276
+ let init_arg = match arg_exprs.get(1) {
7277
+ Some(e) => format!("Some(&{})", e),
7278
+ None => "None".to_string(),
7279
+ };
7027
7280
  return Ok(format!(
7028
- "tishlang_runtime::array_reduce(&{}, &{}, &{})",
7029
- obj_expr, callback, initial
7281
+ "tishlang_runtime::array_reduce_right(&{}, &{}, {})",
7282
+ obj_expr, callback, init_arg
7030
7283
  ));
7031
7284
  }
7285
+ // keys/values/entries exist on BOTH arrays and Map/Set. Arrays have no runtime
7286
+ // method table, so they need this fast-path; a known Map/Set local instead
7287
+ // falls through to the generic bound-method call (its get_prop resolves them).
7288
+ // (#456 regression — the unguarded arms hijacked `m.keys()`/`s.values()`.)
7289
+ "keys" if !self.kve_defers_to_generic(object.as_ref()) => {
7290
+ return Ok(format!("tishlang_runtime::array_keys(&{})", obj_expr));
7291
+ }
7292
+ "values" if !self.kve_defers_to_generic(object.as_ref()) => {
7293
+ return Ok(format!("tishlang_runtime::array_values(&{})", obj_expr));
7294
+ }
7295
+ "entries" if !self.kve_defers_to_generic(object.as_ref()) => {
7296
+ return Ok(format!("tishlang_runtime::array_entries(&{})", obj_expr));
7297
+ }
7032
7298
  "forEach" => {
7033
7299
  let callback = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
7034
7300
  return Ok(format!(
@@ -7133,6 +7399,38 @@ impl Codegen {
7133
7399
  obj_expr, start, delete_count, items
7134
7400
  ));
7135
7401
  }
7402
+ "toSpliced" => {
7403
+ let start = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Number(0.0)".to_string());
7404
+ let delete_count = arg_exprs.get(1).map(|d| format!("Some(&{})", d)).unwrap_or_else(|| "None".to_string());
7405
+ let items = if arg_exprs.len() > 2 {
7406
+ let items_vec = arg_exprs[2..].iter()
7407
+ .map(|a| format!("{}.clone()", a))
7408
+ .collect::<Vec<_>>()
7409
+ .join(", ");
7410
+ format!("&[{}]", items_vec)
7411
+ } else {
7412
+ "&[]".to_string()
7413
+ };
7414
+ return Ok(format!(
7415
+ "tishlang_runtime::array_to_spliced(&{}, &{}, {}, {})",
7416
+ obj_expr, start, delete_count, items
7417
+ ));
7418
+ }
7419
+ "toSorted" => {
7420
+ let comparator = arg_exprs.first().map(|c| format!("Some(&{})", c)).unwrap_or_else(|| "None".to_string());
7421
+ return Ok(format!(
7422
+ "tishlang_runtime::array_to_sorted(&{}, {})",
7423
+ obj_expr, comparator
7424
+ ));
7425
+ }
7426
+ "with" => {
7427
+ let index = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
7428
+ let value = arg_exprs.get(1).cloned().unwrap_or_else(|| "Value::Null".to_string());
7429
+ return Ok(format!(
7430
+ "tishlang_runtime::array_with(&{}, &{}, &{})",
7431
+ obj_expr, index, value
7432
+ ));
7433
+ }
7136
7434
  "flat" => {
7137
7435
  let depth = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Number(1.0)".to_string());
7138
7436
  return Ok(format!(
@@ -8175,7 +8473,12 @@ impl Codegen {
8175
8473
  }
8176
8474
  Self::collect_assigned_idents_in_stmt(body, names);
8177
8475
  }
8178
- Statement::ForOf { iterable, body, .. } => {
8476
+ Statement::ForOf { iterable, body, .. }
8477
+ | Statement::ForIn {
8478
+ object: iterable,
8479
+ body,
8480
+ ..
8481
+ } => {
8179
8482
  Self::collect_assigned_idents_in_expr(iterable, names);
8180
8483
  Self::collect_assigned_idents_in_stmt(body, names);
8181
8484
  }
@@ -8311,7 +8614,12 @@ impl Codegen {
8311
8614
  || update.as_ref().is_some_and(|u| Self::expr_has_length_changer(u))
8312
8615
  || Self::stmt_has_length_changer(body)
8313
8616
  }
8314
- Statement::ForOf { iterable, body, .. } => {
8617
+ Statement::ForOf { iterable, body, .. }
8618
+ | Statement::ForIn {
8619
+ object: iterable,
8620
+ body,
8621
+ ..
8622
+ } => {
8315
8623
  Self::expr_has_length_changer(iterable) || Self::stmt_has_length_changer(body)
8316
8624
  }
8317
8625
  Statement::While { cond, body, .. } | Statement::DoWhile { body, cond, .. } => {
@@ -8338,6 +8646,7 @@ impl Codegen {
8338
8646
  | "normalize" | "localeCompare" | "map" | "filter" | "reduce" | "reduceRight"
8339
8647
  | "forEach" | "find" | "findIndex" | "findLast" | "findLastIndex" | "some" | "every"
8340
8648
  | "join" | "flat" | "flatMap" | "keys" | "values" | "entries"
8649
+ | "toReversed" | "toSorted" | "with" | "toSpliced"
8341
8650
  )
8342
8651
  }
8343
8652
 
@@ -8448,7 +8757,12 @@ impl Codegen {
8448
8757
  || update.as_ref().is_some_and(|u| Self::expr_changes_base_len(u, base))
8449
8758
  || Self::stmt_changes_base_len(body, base)
8450
8759
  }
8451
- Statement::ForOf { iterable, body, .. } => {
8760
+ Statement::ForOf { iterable, body, .. }
8761
+ | Statement::ForIn {
8762
+ object: iterable,
8763
+ body,
8764
+ ..
8765
+ } => {
8452
8766
  Self::expr_changes_base_len(iterable, base) || Self::stmt_changes_base_len(body, base)
8453
8767
  }
8454
8768
  Statement::While { cond, body, .. } | Statement::DoWhile { body, cond, .. } => {
@@ -8857,7 +9171,12 @@ impl Codegen {
8857
9171
  }
8858
9172
  Self::collect_captured_block_vars_from_statements(body, block_vars, result);
8859
9173
  }
8860
- Statement::ForOf { iterable, body, .. } => {
9174
+ Statement::ForOf { iterable, body, .. }
9175
+ | Statement::ForIn {
9176
+ object: iterable,
9177
+ body,
9178
+ ..
9179
+ } => {
8861
9180
  Self::collect_captured_block_vars_from_expr(iterable, block_vars, result);
8862
9181
  Self::collect_captured_block_vars_from_statements(body, block_vars, result);
8863
9182
  }
@@ -8995,7 +9314,8 @@ impl Codegen {
8995
9314
  }
8996
9315
  Self::collect_local_var_names(body, names);
8997
9316
  }
8998
- Statement::ForOf { body, .. } => Self::collect_local_var_names(body, names),
9317
+ Statement::ForOf { body, .. }
9318
+ | Statement::ForIn { body, .. } => Self::collect_local_var_names(body, names),
8999
9319
  Statement::While { body, .. } | Statement::DoWhile { body, .. } => {
9000
9320
  Self::collect_local_var_names(body, names);
9001
9321
  }
@@ -9261,7 +9581,12 @@ impl Codegen {
9261
9581
  }
9262
9582
  Self::collect_stmt_idents(body, idents);
9263
9583
  }
9264
- Statement::ForOf { iterable, body, .. } => {
9584
+ Statement::ForOf { iterable, body, .. }
9585
+ | Statement::ForIn {
9586
+ object: iterable,
9587
+ body,
9588
+ ..
9589
+ } => {
9265
9590
  Self::collect_expr_idents(iterable, idents);
9266
9591
  Self::collect_stmt_idents(body, idents);
9267
9592
  }
@@ -12150,6 +12475,18 @@ impl Codegen {
12150
12475
  }
12151
12476
  _ => false,
12152
12477
  },
12478
+ // `Math.imul(a, b)` — an exact int32-domain node (emits an i32 `wrapping_mul`, see
12479
+ // `emit_int32_operand`), so it participates in an int32 chain when both operands are
12480
+ // themselves int32-lowerable. Lets `h = Math.imul(h, K) >>> 0` keep `h` in a register
12481
+ // across the loop instead of narrowing/widening `h` on every op.
12482
+ Expr::Call { callee, args, .. } => {
12483
+ matches!(callee.as_ref(),
12484
+ Expr::Member { object, prop: MemberProp::Name { name: m, .. }, .. }
12485
+ if m.as_ref() == "imul"
12486
+ && matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math"))
12487
+ && matches!(args.as_slice(), [CallArg::Expr(a0), CallArg::Expr(a1)]
12488
+ if self.i32_chain_lowerable(a0, var) && self.i32_chain_lowerable(a1, var))
12489
+ }
12153
12490
  _ => false,
12154
12491
  }
12155
12492
  }
@@ -12391,6 +12728,7 @@ impl Codegen {
12391
12728
  }
12392
12729
  Statement::For { body, .. }
12393
12730
  | Statement::ForOf { body, .. }
12731
+ | Statement::ForIn { body, .. }
12394
12732
  | Statement::While { body, .. }
12395
12733
  | Statement::DoWhile { body, .. } => {
12396
12734
  Self::collect_ordered_assigns_to(body, var, out)
@@ -12415,6 +12753,7 @@ impl Codegen {
12415
12753
  }
12416
12754
  Statement::For { body, .. }
12417
12755
  | Statement::ForOf { body, .. }
12756
+ | Statement::ForIn { body, .. }
12418
12757
  | Statement::While { body, .. }
12419
12758
  | Statement::DoWhile { body, .. }
12420
12759
  | Statement::FunDecl { body, .. } => f(std::slice::from_ref(body)),
@@ -12480,7 +12819,12 @@ impl Codegen {
12480
12819
  }
12481
12820
  Self::for_each_stmt_expr(body, f);
12482
12821
  }
12483
- Statement::ForOf { iterable, body, .. } => {
12822
+ Statement::ForOf { iterable, body, .. }
12823
+ | Statement::ForIn {
12824
+ object: iterable,
12825
+ body,
12826
+ ..
12827
+ } => {
12484
12828
  f(iterable);
12485
12829
  Self::for_each_stmt_expr(body, f);
12486
12830
  }
@@ -12893,7 +13237,11 @@ impl Codegen {
12893
13237
  Self::collect_annotated_types(std::slice::from_ref(e), aliases, env);
12894
13238
  }
12895
13239
  }
12896
- Statement::While { body, .. } | Statement::DoWhile { body, .. } => {
13240
+ // for-in's loop var is always a string key, so it gets no Vec-element type binding —
13241
+ // it behaves like while/do-while here (recurse the body only).
13242
+ Statement::While { body, .. }
13243
+ | Statement::DoWhile { body, .. }
13244
+ | Statement::ForIn { body, .. } => {
12897
13245
  Self::collect_annotated_types(std::slice::from_ref(body), aliases, env)
12898
13246
  }
12899
13247
  Statement::ForOf {
@@ -13037,7 +13385,12 @@ impl Codegen {
13037
13385
  }
13038
13386
  Self::collect_reassignments_stmt(body, out);
13039
13387
  }
13040
- Statement::ForOf { iterable, body, .. } => {
13388
+ Statement::ForOf { iterable, body, .. }
13389
+ | Statement::ForIn {
13390
+ object: iterable,
13391
+ body,
13392
+ ..
13393
+ } => {
13041
13394
  Self::collect_reassignments_expr(iterable, out);
13042
13395
  Self::collect_reassignments_stmt(body, out);
13043
13396
  }
@@ -13299,6 +13652,22 @@ impl Codegen {
13299
13652
  }
13300
13653
  }
13301
13654
  }
13655
+ // Two-arg `Math.imul(a, b)` → an exact 32-bit product (a number). Modelled so a value
13656
+ // fed by it (`h = Math.imul(h, K) >>> 0`) is not demoted to a boxed `Value`.
13657
+ if let [CallArg::Expr(_), CallArg::Expr(_)] = args.as_slice() {
13658
+ if let Expr::Member {
13659
+ object,
13660
+ prop: MemberProp::Name { name: method, .. },
13661
+ ..
13662
+ } = callee.as_ref()
13663
+ {
13664
+ if method.as_ref() == "imul"
13665
+ && matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
13666
+ {
13667
+ return RustType::F64;
13668
+ }
13669
+ }
13670
+ }
13302
13671
  // #169: a fused native `Vec<f64>` reduce produces an `f64` (the emitter lowers it via
13303
13672
  // `native_vec_hof_for_call`). Model it here so an accumulator fed by `xs.reduce(...)`
13304
13673
  // is not wrongly demoted to a boxed `Value`. Conservative: any miss → `Value`.
@@ -13984,7 +14353,16 @@ impl Codegen {
13984
14353
  self.emit_native_fn_body(&statements[i])?;
13985
14354
  } else {
13986
14355
  self.emit_statement(&statements[i])?;
13987
- if Self::stmt_may_throw(&statements[i]) && !Self::is_terminator_stmt(&statements[i]) {
14356
+ // Same guard as the primary emit site above: a native-typed frame
14357
+ // (`-> f64`/`-> Vec<..>`/`-> ()`) can't type the `Err`/`Value::Null` check, so
14358
+ // suppress it there — the parked throw still surfaces at the first Value/Result
14359
+ // frame up the chain. Without this guard the strict-`<` fast-path emitted an
14360
+ // untypeable checkpoint after any may-throw stmt (e.g. an index read post-#425),
14361
+ // breaking pure-numeric fixtures like fannkuch.
14362
+ if Self::stmt_may_throw(&statements[i])
14363
+ && !Self::is_terminator_stmt(&statements[i])
14364
+ && (self.try_closure_depth > 0 || !self.in_native_typed_frame())
14365
+ {
13988
14366
  self.emit_pending_throw_check();
13989
14367
  }
13990
14368
  }
@@ -14115,7 +14493,8 @@ impl Codegen {
14115
14493
  }
14116
14494
  Self::walk_stmt_for_module_const_disqualifiers(body, g, bad);
14117
14495
  }
14118
- Statement::ForOf { body, .. } => {
14496
+ Statement::ForOf { body, .. }
14497
+ | Statement::ForIn { body, .. } => {
14119
14498
  Self::walk_stmt_for_module_const_disqualifiers(body, g, bad);
14120
14499
  }
14121
14500
  Statement::Switch {
@@ -14536,6 +14915,32 @@ impl Codegen {
14536
14915
  )
14537
14916
  }
14538
14917
 
14918
+ fn expr_is_set_construct(expr: &Expr) -> bool {
14919
+ matches!(
14920
+ expr,
14921
+ Expr::New {
14922
+ callee,
14923
+ ..
14924
+ } if matches!(callee.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Set")
14925
+ )
14926
+ }
14927
+
14928
+ /// #456 regression guard: should `<object>.keys()/.values()/.entries()` DEFER to the generic
14929
+ /// bound-method call instead of the array fast-path? True when `object` is not an array:
14930
+ /// - the `Object` global (`Object.keys/values/entries` are STATICS → `tish_object_*`),
14931
+ /// - a known `new Map()`/`new Set()` local, or a direct `new Map(…)`/`new Set(…)` receiver
14932
+ /// (their keys/values/entries resolve as bound methods via `get_prop`).
14933
+ /// Arrays (everything else) keep the fast-path — they have no runtime method table.
14934
+ fn kve_defers_to_generic(&self, object: &Expr) -> bool {
14935
+ match object {
14936
+ Expr::Ident { name, .. } => {
14937
+ let n = name.as_ref();
14938
+ n == "Object" || self.collection_instance_locals.contains(n)
14939
+ }
14940
+ _ => Self::expr_is_map_construct(object) || Self::expr_is_set_construct(object),
14941
+ }
14942
+ }
14943
+
14539
14944
  /// #176 — classify top-level `let x = <number-literal>` bindings whose every use is a numeric
14540
14945
  /// read or a whole-binding assign with a numeric-shaped RHS (fasta `seed`).
14541
14946
  fn collect_native_numeric_globals(stmts: &[Statement]) -> HashMap<String, f64> {
@@ -14865,7 +15270,8 @@ impl Codegen {
14865
15270
  Statement::While { body, .. }
14866
15271
  | Statement::DoWhile { body, .. }
14867
15272
  | Statement::For { body, .. }
14868
- | Statement::ForOf { body, .. } => {
15273
+ | Statement::ForOf { body, .. }
15274
+ | Statement::ForIn { body, .. } => {
14869
15275
  Self::walk_stmt_for_global_disqualifiers(body, g, bad);
14870
15276
  }
14871
15277
  Statement::Switch {
@@ -15105,7 +15511,12 @@ impl Codegen {
15105
15511
  f(cond);
15106
15512
  Self::for_each_expr_root_stmt(body, f);
15107
15513
  }
15108
- Statement::ForOf { iterable, body, .. } => {
15514
+ Statement::ForOf { iterable, body, .. }
15515
+ | Statement::ForIn {
15516
+ object: iterable,
15517
+ body,
15518
+ ..
15519
+ } => {
15109
15520
  f(iterable);
15110
15521
  Self::for_each_expr_root_stmt(body, f);
15111
15522
  }
@@ -15717,12 +16128,16 @@ impl Codegen {
15717
16128
  }
15718
16129
  Expr::Member { object, prop: MemberProp::Name { name: m, .. }, .. } => {
15719
16130
  matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
15720
- && args.len() == 1
15721
- && matches!(
15722
- m.as_ref(),
15723
- "sqrt" | "sin" | "cos" | "tan" | "abs" | "floor" | "ceil" | "exp"
15724
- | "trunc" | "log"
15725
- )
16131
+ && ((args.len() == 1
16132
+ && matches!(
16133
+ m.as_ref(),
16134
+ "sqrt" | "sin" | "cos" | "tan" | "abs" | "floor" | "ceil"
16135
+ | "exp" | "trunc" | "log"
16136
+ ))
16137
+ // 2-arg `Math.imul` inlines to an exact i32 wrapping multiply — a
16138
+ // native-safe numeric primitive (no dispatch), so a fn using it (FNV
16139
+ // with imul, xmur3/mulberry32 PRNGs) stays eligible for native typing.
16140
+ || (args.len() == 2 && m.as_ref() == "imul"))
15726
16141
  }
15727
16142
  _ => false,
15728
16143
  }
@@ -18325,7 +18740,8 @@ impl Codegen {
18325
18740
  Statement::While { body, .. }
18326
18741
  | Statement::DoWhile { body, .. }
18327
18742
  | Statement::For { body, .. }
18328
- | Statement::ForOf { body, .. } => Self::rec_collect_returns(body, out),
18743
+ | Statement::ForOf { body, .. }
18744
+ | Statement::ForIn { body, .. } => Self::rec_collect_returns(body, out),
18329
18745
  _ => {}
18330
18746
  }
18331
18747
  }
@@ -18469,7 +18885,8 @@ impl Codegen {
18469
18885
  }
18470
18886
  Statement::While { body, .. }
18471
18887
  | Statement::DoWhile { body, .. }
18472
- | Statement::ForOf { body, .. } => Self::rec_orch_binds_node(body, builders),
18888
+ | Statement::ForOf { body, .. }
18889
+ | Statement::ForIn { body, .. } => Self::rec_orch_binds_node(body, builders),
18473
18890
  Statement::For { init, body, .. } => {
18474
18891
  init.as_ref()
18475
18892
  .is_some_and(|i| Self::rec_orch_binds_node(i, builders))
@@ -20553,7 +20970,8 @@ impl Codegen {
20553
20970
  }
20554
20971
  Statement::While { body, .. }
20555
20972
  | Statement::DoWhile { body, .. }
20556
- | Statement::ForOf { body, .. } => Self::agg_collect_aliases(body, p, out),
20973
+ | Statement::ForOf { body, .. }
20974
+ | Statement::ForIn { body, .. } => Self::agg_collect_aliases(body, p, out),
20557
20975
  _ => {}
20558
20976
  }
20559
20977
  }
@@ -20581,7 +20999,8 @@ impl Codegen {
20581
20999
  Statement::For { body, .. }
20582
21000
  | Statement::While { body, .. }
20583
21001
  | Statement::DoWhile { body, .. }
20584
- | Statement::ForOf { body, .. } => Self::agg_stmt_writes(body, p, aliases),
21002
+ | Statement::ForOf { body, .. }
21003
+ | Statement::ForIn { body, .. } => Self::agg_stmt_writes(body, p, aliases),
20585
21004
  _ => false,
20586
21005
  }
20587
21006
  }
@@ -20705,7 +21124,8 @@ impl Codegen {
20705
21124
  Statement::For { body, .. }
20706
21125
  | Statement::While { body, .. }
20707
21126
  | Statement::DoWhile { body, .. }
20708
- | Statement::ForOf { body, .. } => Self::stmt_returns_value(body),
21127
+ | Statement::ForOf { body, .. }
21128
+ | Statement::ForIn { body, .. } => Self::stmt_returns_value(body),
20709
21129
  _ => false,
20710
21130
  }
20711
21131
  }
@@ -20764,6 +21184,30 @@ impl Codegen {
20764
21184
  return Ok(Some(code));
20765
21185
  }
20766
21186
  }
21187
+ // `Math.imul(a, b)` in an int32 context → the raw i32 `wrapping_mul` (no f64 excursion, no
21188
+ // `to_int32` round-trip on the product), when both operands are themselves int32-lowerable.
21189
+ // JS ES6 semantics (ToInt32 both operands, wrapping 32-bit multiply, signed result) are carried
21190
+ // by the operands' own int32 lowering. Keeps `h = Math.imul(h, K) >>> 0` fully register-domain.
21191
+ if let Expr::Call { callee, args, .. } = e {
21192
+ if let [CallArg::Expr(a0), CallArg::Expr(a1)] = args.as_slice() {
21193
+ if let Expr::Member {
21194
+ object,
21195
+ prop: MemberProp::Name { name: m, .. },
21196
+ ..
21197
+ } = callee.as_ref()
21198
+ {
21199
+ if m.as_ref() == "imul"
21200
+ && matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
21201
+ {
21202
+ let ai = self.emit_int32_operand(a0)?;
21203
+ let bi = self.emit_int32_operand(a1)?;
21204
+ if let (Some(ai), Some(bi)) = (ai, bi) {
21205
+ return Ok(Some(format!("(({}).wrapping_mul({}))", ai, bi)));
21206
+ }
21207
+ }
21208
+ }
21209
+ }
21210
+ }
20767
21211
  // Integer-literal leaf: `ToInt32(<int literal>)` is a compile-time constant — emit it
20768
21212
  // directly (`v as i32` = modulo 2^32 = JS ToInt32 of the integer) instead of a runtime
20769
21213
  // `to_int32(1_f64)` call. Removes the constant round-trip in masks/shift-counts
@@ -21334,6 +21778,35 @@ impl Codegen {
21334
21778
  }
21335
21779
  }
21336
21780
  }
21781
+ // `Math.imul(a, b)` → an inline exact 32-bit wrapping multiply (ES6: ToInt32 both
21782
+ // operands, wrapping i32 multiply, signed int32 result). Both args go through
21783
+ // `emit_int32_operand` — the SAME ToInt32 lowering the bitwise path uses — so this is
21784
+ // bit-for-bit identical to the boxed `tish_math_imul` and to V8: typed==boxed==node
21785
+ // holds. It eliminates the per-call arg boxing + `value_call` a hot hashing/PRNG loop
21786
+ // (xmur3, mulberry32, FNV-with-imul) would otherwise pay. A general primitive (fires
21787
+ // for any `Math.imul`), not a fixture-specific kernel emission (cf. #317). Falls
21788
+ // through to the boxed call when an operand isn't int32-lowerable.
21789
+ if let [CallArg::Expr(a0), CallArg::Expr(a1)] = args.as_slice() {
21790
+ if let Expr::Member {
21791
+ object,
21792
+ prop: MemberProp::Name { name: method, .. },
21793
+ ..
21794
+ } = callee.as_ref()
21795
+ {
21796
+ if method.as_ref() == "imul"
21797
+ && matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
21798
+ {
21799
+ let ai = self.emit_int32_operand(a0)?;
21800
+ let bi = self.emit_int32_operand(a1)?;
21801
+ if let (Some(ai), Some(bi)) = (ai, bi) {
21802
+ return Ok((
21803
+ format!("(({}).wrapping_mul({}) as f64)", ai, bi),
21804
+ RustType::F64,
21805
+ ));
21806
+ }
21807
+ }
21808
+ }
21809
+ }
21337
21810
  if let [CallArg::Expr(arg_expr)] = args.as_slice() {
21338
21811
  if let Expr::Member {
21339
21812
  object,