@tishlang/tish 1.13.2 → 2.0.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 (106) hide show
  1. package/Cargo.toml +2 -0
  2. package/bin/tish +0 -0
  3. package/crates/js_to_tish/src/transform/expr.rs +1 -0
  4. package/crates/tish/Cargo.toml +11 -3
  5. package/crates/tish/build.rs +21 -0
  6. package/crates/tish/src/cli_help.rs +15 -4
  7. package/crates/tish/src/main.rs +93 -21
  8. package/crates/tish/src/repl_completion.rs +0 -1
  9. package/crates/tish/tests/error_source_location.rs +36 -0
  10. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  11. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  12. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  13. package/crates/tish/tests/integration_test.rs +402 -91
  14. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  15. package/crates/tish/tests/tty_capability.rs +43 -0
  16. package/crates/tish_ast/src/ast.rs +37 -8
  17. package/crates/tish_builtins/Cargo.toml +2 -0
  18. package/crates/tish_builtins/src/array.rs +375 -13
  19. package/crates/tish_builtins/src/collections.rs +481 -0
  20. package/crates/tish_builtins/src/construct.rs +59 -19
  21. package/crates/tish_builtins/src/date.rs +538 -0
  22. package/crates/tish_builtins/src/globals.rs +86 -6
  23. package/crates/tish_builtins/src/iterator.rs +129 -0
  24. package/crates/tish_builtins/src/lib.rs +5 -0
  25. package/crates/tish_builtins/src/number.rs +96 -0
  26. package/crates/tish_builtins/src/object.rs +2 -2
  27. package/crates/tish_builtins/src/string.rs +19 -20
  28. package/crates/tish_builtins/src/symbol.rs +1 -1
  29. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  30. package/crates/tish_bytecode/src/chunk.rs +69 -1
  31. package/crates/tish_bytecode/src/compiler.rs +933 -89
  32. package/crates/tish_bytecode/src/encoding.rs +2 -0
  33. package/crates/tish_bytecode/src/lib.rs +2 -1
  34. package/crates/tish_bytecode/src/opcode.rs +47 -4
  35. package/crates/tish_bytecode/src/serialize.rs +31 -1
  36. package/crates/tish_compile/Cargo.toml +1 -0
  37. package/crates/tish_compile/src/check.rs +774 -0
  38. package/crates/tish_compile/src/codegen.rs +2334 -349
  39. package/crates/tish_compile/src/infer.rs +1395 -6
  40. package/crates/tish_compile/src/lib.rs +50 -8
  41. package/crates/tish_compile/src/resolve.rs +584 -21
  42. package/crates/tish_compile/src/types.rs +106 -2
  43. package/crates/tish_compile_js/src/codegen.rs +67 -0
  44. package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
  45. package/crates/tish_core/Cargo.toml +7 -1
  46. package/crates/tish_core/src/console_style.rs +11 -1
  47. package/crates/tish_core/src/json.rs +81 -38
  48. package/crates/tish_core/src/lib.rs +3 -0
  49. package/crates/tish_core/src/shape.rs +85 -0
  50. package/crates/tish_core/src/value.rs +679 -25
  51. package/crates/tish_core/src/vmref.rs +13 -8
  52. package/crates/tish_cranelift/src/link.rs +17 -4
  53. package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
  54. package/crates/tish_eval/Cargo.toml +6 -0
  55. package/crates/tish_eval/src/eval.rs +665 -117
  56. package/crates/tish_eval/src/http.rs +4 -1
  57. package/crates/tish_eval/src/natives.rs +165 -13
  58. package/crates/tish_eval/src/value.rs +31 -13
  59. package/crates/tish_eval/src/value_convert.rs +10 -4
  60. package/crates/tish_ffi/Cargo.toml +26 -0
  61. package/crates/tish_ffi/src/lib.rs +518 -0
  62. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  63. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  64. package/crates/tish_ffi/tests/loader.rs +65 -0
  65. package/crates/tish_fmt/src/lib.rs +43 -5
  66. package/crates/tish_lexer/src/lib.rs +397 -9
  67. package/crates/tish_lexer/src/token.rs +7 -0
  68. package/crates/tish_lint/src/lib.rs +2 -10
  69. package/crates/tish_lsp/src/import_goto.rs +2 -0
  70. package/crates/tish_lsp/src/main.rs +439 -26
  71. package/crates/tish_native/src/build.rs +55 -1
  72. package/crates/tish_opt/src/lib.rs +126 -23
  73. package/crates/tish_parser/src/lib.rs +55 -1
  74. package/crates/tish_parser/src/parser.rs +456 -34
  75. package/crates/tish_pg/src/lib.rs +3 -3
  76. package/crates/tish_resolve/src/lib.rs +99 -59
  77. package/crates/tish_runtime/Cargo.toml +4 -0
  78. package/crates/tish_runtime/src/http.rs +66 -17
  79. package/crates/tish_runtime/src/http_fetch.rs +29 -8
  80. package/crates/tish_runtime/src/http_hyper.rs +25 -2
  81. package/crates/tish_runtime/src/lib.rs +299 -44
  82. package/crates/tish_runtime/src/promise.rs +328 -18
  83. package/crates/tish_runtime/src/timers.rs +13 -7
  84. package/crates/tish_runtime/src/tty.rs +226 -0
  85. package/crates/tish_runtime/src/ws.rs +35 -18
  86. package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
  87. package/crates/tish_ui/src/jsx.rs +10 -0
  88. package/crates/tish_ui/src/runtime/hooks.rs +19 -15
  89. package/crates/tish_ui/src/runtime/mod.rs +15 -12
  90. package/crates/tish_vm/Cargo.toml +14 -1
  91. package/crates/tish_vm/src/jit.rs +1050 -0
  92. package/crates/tish_vm/src/lib.rs +2 -0
  93. package/crates/tish_vm/src/vm.rs +1546 -202
  94. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  95. package/crates/tish_wasm/src/lib.rs +6 -2
  96. package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
  97. package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
  98. package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
  99. package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
  100. package/justfile +8 -0
  101. package/package.json +1 -1
  102. package/platform/darwin-arm64/tish +0 -0
  103. package/platform/darwin-x64/tish +0 -0
  104. package/platform/linux-arm64/tish +0 -0
  105. package/platform/linux-x64/tish +0 -0
  106. package/platform/win32-x64/tish.exe +0 -0
@@ -57,7 +57,9 @@ impl UsageAnalyzer {
57
57
  self.analyze_statement(e);
58
58
  }
59
59
  }
60
- Statement::Block { statements, .. } => self.analyze_statements(statements),
60
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
61
+ self.analyze_statements(statements)
62
+ }
61
63
  Statement::For {
62
64
  init,
63
65
  cond,
@@ -202,6 +204,7 @@ impl UsageAnalyzer {
202
204
  self.analyze_expr(right);
203
205
  }
204
206
  Expr::TypeOf { operand, .. } => self.analyze_expr(operand),
207
+ Expr::Delete { target, .. } => self.analyze_expr(target),
205
208
  Expr::TemplateLiteral { exprs, .. } => {
206
209
  for e in exprs {
207
210
  self.analyze_expr(e);
@@ -310,7 +313,9 @@ fn program_uses_async(program: &Program) -> bool {
310
313
  fn stmt_has_async(s: &Statement) -> bool {
311
314
  match s {
312
315
  Statement::FunDecl { async_, .. } if *async_ => true,
313
- Statement::Block { statements, .. } => statements.iter().any(stmt_has_async),
316
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
317
+ statements.iter().any(stmt_has_async)
318
+ }
314
319
  Statement::If {
315
320
  then_branch,
316
321
  else_branch,
@@ -427,7 +432,9 @@ fn program_uses_async(program: &Program) -> bool {
427
432
  }
428
433
  fn stmt_has_await(s: &Statement) -> bool {
429
434
  match s {
430
- Statement::Block { statements, .. } => statements.iter().any(stmt_has_await),
435
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
436
+ statements.iter().any(stmt_has_await)
437
+ }
431
438
  Statement::VarDecl { init, .. } => init.as_ref().is_some_and(expr_has_await),
432
439
  Statement::VarDeclDestructure { init, .. } => expr_has_await(init),
433
440
  Statement::ExprStmt { expr, .. } => expr_has_await(expr),
@@ -652,6 +659,34 @@ pub fn compile_with_native_modules(
652
659
  )
653
660
  }
654
661
 
662
+ /// Opt-in gradual type check. `TISH_CHECK=1`/`warn` prints provable annotation violations to stderr
663
+ /// as warnings; `TISH_CHECK=error` also fails the build. Unset/`0` → no-op (default builds are
664
+ /// unaffected). The checker is gradual (see `check.rs`): it never flags code it can't prove wrong.
665
+ fn run_type_check(program: &Program) -> Result<(), CompileError> {
666
+ let mode = std::env::var("TISH_CHECK").unwrap_or_default();
667
+ if mode.is_empty() || mode == "0" {
668
+ return Ok(());
669
+ }
670
+ let diags = crate::check::check_program(program);
671
+ if diags.is_empty() {
672
+ return Ok(());
673
+ }
674
+ let kind = if mode == "error" { "error" } else { "warning" };
675
+ for d in &diags {
676
+ eprintln!(
677
+ "tish type {}: {}:{}: {}",
678
+ kind, d.span.start.0, d.span.start.1, d.message
679
+ );
680
+ }
681
+ if mode == "error" {
682
+ return Err(CompileError::new(
683
+ format!("type checking failed: {} error(s)", diags.len()),
684
+ Some(diags[0].span),
685
+ ));
686
+ }
687
+ Ok(())
688
+ }
689
+
655
690
  pub fn compile_with_native_modules_emit(
656
691
  program: &Program,
657
692
  project_root: Option<&Path>,
@@ -666,6 +701,10 @@ pub fn compile_with_native_modules_emit(
666
701
  } else {
667
702
  program.clone()
668
703
  };
704
+ // Gradual type check (opt-in via `TISH_CHECK`): `=1`/`=warn` prints provable annotation
705
+ // violations as warnings; `=error` blocks the build. Off by default — never affects the
706
+ // standard build. Run on the optimized, pre-inference program (real user annotations only).
707
+ run_type_check(&program)?;
669
708
  // Type-inference pass: fills in `type_ann` on unannotated VarDecl nodes where
670
709
  // the type is unambiguous (literals, arithmetic of typed vars, etc.).
671
710
  let program = crate::infer::infer_program(&program);
@@ -703,7 +742,6 @@ struct Codegen {
703
742
  indent: usize,
704
743
  loop_label_index: usize,
705
744
  is_async: bool,
706
- project_root: Option<std::path::PathBuf>,
707
745
  /// Requested features (http, process, fs, regex, polars). When non-empty, used instead of #[cfg].
708
746
  features: std::collections::HashSet<String>,
709
747
  /// spec -> native init strategy (legacy adapter object vs generated `generated_native` wrapper)
@@ -711,6 +749,15 @@ struct Codegen {
711
749
  /// Stack: true = async Rust context (run body), false = sync closure (Tish fn body)
712
750
  async_context_stack: Vec<bool>,
713
751
  loop_stack: Vec<(String, Option<String>)>, // (break_label, continue_update) for innermost loop
752
+ /// Break targets for innermost breakable construct — loops AND switches (JS `break` exits the
753
+ /// nearest loop OR switch; `continue` uses loop_stack). Loops push to both; switches push here only.
754
+ break_stack: Vec<String>,
755
+ /// How many enclosing `try`-body closures we're currently emitting inside (within the current
756
+ /// function). A try body compiles to `(|| -> Result<Option<Value>, _> { … })()` — a *completion*
757
+ /// closure: `Ok(None)`=normal, `Ok(Some(v))`=pending `return v`, `Err(Throw)`=pending throw. When
758
+ /// depth>0, `return`/`throw` emit the closure-escaping completion form so they unwind through
759
+ /// `finally`; at depth 0 they're a plain `return`/panic (the fast path is untouched).
760
+ try_closure_depth: u32,
714
761
  /// Stack of scopes, each containing function names declared in that scope
715
762
  /// Used to capture sibling functions for mutual recursion
716
763
  function_scope_stack: Vec<Vec<String>>,
@@ -723,6 +770,18 @@ struct Codegen {
723
770
  /// Variables currently wrapped in Rc<RefCell<Value>> for mutable capture in closures
724
771
  /// These need special handling: reads via .borrow().clone(), writes via *var.borrow_mut()
725
772
  refcell_wrapped_vars: std::collections::HashSet<String>,
773
+ /// M5 (dark-shipped behind `TISH_NATIVE_FN`): top-level functions eligible for a parallel
774
+ /// free `fn f_native(f64,..)->f64` (all params `: number`, returns `number`, native-safe
775
+ /// body). Direct calls to these route to the native fn, bypassing the boxed `value_call`.
776
+ native_fns: std::collections::HashSet<String>,
777
+ /// Names of `number`-typed locals demoted to a boxed `Value` because some reassignment can
778
+ /// store a non-number — e.g. `let s = 0; s = s + arr[i]` where `arr` is a boxed Value: `+` is
779
+ /// JS string concat, so `s` may become a `String`. Lowering `s` to a native `f64` would panic
780
+ /// at the store's `from_value_expr(F64)` coercion (`_ => panic!("expected number")`). Computed
781
+ /// once in `emit_program` (after type aliases + `native_fns`), consulted at `VarDecl` to force
782
+ /// `RustType::Value`. This is the rust-AOT analogue of the VM array-JIT bailing to the
783
+ /// interpreter on a non-numeric element. See `collect_demoted_numeric_locals`.
784
+ demoted_numeric_locals: std::collections::HashSet<String>,
726
785
  /// Scopes of names whose Rust binding is actually `Rc<RefCell<_>>` (emitted at VarDecl).
727
786
  /// `refcell_wrapped_vars` alone is insufficient: it is set by prepasses before decl may run.
728
787
  rc_cell_storage_scopes: Vec<std::collections::HashSet<String>>,
@@ -753,7 +812,9 @@ struct Codegen {
753
812
 
754
813
  impl Codegen {
755
814
  fn new_with_native_modules(
756
- project_root: Option<&Path>,
815
+ // `project_root` is no longer needed by codegen (the only consumer, a Polars-specific
816
+ // `read_csv` compile-time embed, was removed — crate-specific codegen belongs in that crate).
817
+ _project_root: Option<&Path>,
757
818
  features: &[String],
758
819
  native_module_init: std::collections::HashMap<String, crate::resolve::NativeModuleInit>,
759
820
  ) -> Self {
@@ -763,15 +824,18 @@ impl Codegen {
763
824
  indent: 0,
764
825
  loop_label_index: 0,
765
826
  is_async: false,
766
- project_root: project_root.map(|p| p.to_path_buf()),
767
827
  features,
768
828
  native_module_init,
769
829
  async_context_stack: Vec::new(),
770
830
  loop_stack: Vec::new(),
831
+ break_stack: Vec::new(),
832
+ try_closure_depth: 0,
771
833
  function_scope_stack: vec![Vec::new()], // Start with global scope
772
834
  outer_params_stack: Vec::new(),
773
835
  outer_vars_stack: vec![Vec::new()], // Start with module-level scope
774
836
  refcell_wrapped_vars: std::collections::HashSet::new(),
837
+ native_fns: std::collections::HashSet::new(),
838
+ demoted_numeric_locals: std::collections::HashSet::new(),
775
839
  rc_cell_storage_scopes: vec![std::collections::HashSet::new()],
776
840
  usage_analyzer: None,
777
841
  type_context: TypeContext::new(),
@@ -834,7 +898,9 @@ impl Codegen {
834
898
  Statement::TypeAlias { name, ty, .. } => {
835
899
  out.push((name.to_string(), ty));
836
900
  }
837
- Statement::Block { statements, .. } => Self::walk_type_aliases(statements, out),
901
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
902
+ Self::walk_type_aliases(statements, out)
903
+ }
838
904
  Statement::If {
839
905
  then_branch,
840
906
  else_branch,
@@ -884,7 +950,7 @@ impl Codegen {
884
950
  = &ty
885
951
  {
886
952
  let struct_name = crate::types::named_struct_ident(&name);
887
- self.write(&format!("#[derive(Clone, Debug, Default)]\n"));
953
+ self.write("#[derive(Clone, Debug, Default)]\n");
888
954
  self.write("#[allow(non_snake_case, non_camel_case_types)]\n");
889
955
  self.write(&format!("pub struct {} {{\n", struct_name));
890
956
  for (k, t) in fields {
@@ -955,9 +1021,14 @@ impl Codegen {
955
1021
  self.write(" buf.push(']');\n");
956
1022
  }
957
1023
  _ => {
958
- // Fallback: convert the field to a Value and
959
- // delegate to the dynamic stringifier.
960
- let v_expr = t.to_value_expr(&access);
1024
+ // Fallback: convert the field to a Value and delegate to the dynamic
1025
+ // stringifier. A `Value` field (e.g. a generic struct's `Box<T>` field)
1026
+ // is behind `&self` and not `Copy`, so clone it.
1027
+ let v_expr = if matches!(t, crate::types::RustType::Value) {
1028
+ format!("{}.clone()", access)
1029
+ } else {
1030
+ t.to_value_expr(&access)
1031
+ };
961
1032
  self.write(&format!(
962
1033
  " let _v: Value = {}; tishlang_runtime::json::stringify_into(buf, &_v);\n",
963
1034
  v_expr
@@ -1034,7 +1105,7 @@ impl Codegen {
1034
1105
  // latter dispatches into `http_serve_per_worker`, which
1035
1106
  // calls onWorker once per accept thread to build that
1036
1107
  // thread's handler.
1037
- "serve" => Some("Value::native(|args: &[Value]| { let handler = args.get(1).cloned().unwrap_or(Value::Null); match handler { Value::Function(f) => tish_http_serve(args, move |req_args| f(req_args)), Value::Object(ref opts) => { let factory = opts.borrow().strings.get(\"onWorker\").cloned().unwrap_or(Value::Null); tishlang_runtime::http_serve_per_worker(args, factory) }, _ => Value::Null } })"),
1108
+ "serve" => Some("Value::native(|args: &[Value]| { let handler = args.get(1).cloned().unwrap_or(Value::Null); match handler { Value::Function(f) => tish_http_serve(args, move |req_args| f.call(req_args)), Value::Object(ref opts) => { let factory = opts.borrow().strings.get(\"onWorker\").cloned().unwrap_or(Value::Null); tishlang_runtime::http_serve_per_worker(args, factory) }, _ => Value::Null } })"),
1038
1109
  "Promise" => Some("tish_promise_object()"),
1039
1110
  "Symbol" => Some("tish_symbol_object()"),
1040
1111
  _ => None,
@@ -1062,6 +1133,16 @@ impl Codegen {
1062
1133
  "wsBroadcast" => Some("Value::native(|args: &[Value]| tishlang_runtime::ws_broadcast_native(args))"),
1063
1134
  _ => None,
1064
1135
  },
1136
+ "tish:tty" if self.has_feature("tty") => match export_name {
1137
+ "size" => Some("Value::native(|args: &[Value]| tishlang_runtime::tty_size(args))"),
1138
+ "isTTY" => Some("Value::native(|args: &[Value]| tishlang_runtime::tty_is_tty(args))"),
1139
+ "setRawMode" => Some("Value::native(|args: &[Value]| tishlang_runtime::tty_set_raw_mode(args))"),
1140
+ "enterAltScreen" => Some("Value::native(|args: &[Value]| tishlang_runtime::tty_enter_alt_screen(args))"),
1141
+ "leaveAltScreen" => Some("Value::native(|args: &[Value]| tishlang_runtime::tty_leave_alt_screen(args))"),
1142
+ "read" => Some("Value::native(|args: &[Value]| tishlang_runtime::tty_read(args))"),
1143
+ "readLine" => Some("Value::native(|args: &[Value]| tishlang_runtime::tty_read_line(args))"),
1144
+ _ => None,
1145
+ },
1065
1146
  _ => return None,
1066
1147
  };
1067
1148
  init.map(String::from)
@@ -1069,7 +1150,10 @@ impl Codegen {
1069
1150
 
1070
1151
  fn has_feature(&self, name: &str) -> bool {
1071
1152
  if self.features.contains("full") {
1072
- matches!(name, "http" | "timers" | "fs" | "process" | "regex" | "ws")
1153
+ matches!(
1154
+ name,
1155
+ "http" | "timers" | "fs" | "process" | "regex" | "ws" | "tty"
1156
+ )
1073
1157
  } else {
1074
1158
  self.features.contains(name)
1075
1159
  }
@@ -1098,6 +1182,17 @@ impl Codegen {
1098
1182
  }
1099
1183
 
1100
1184
  /// Escape Rust reserved keywords by prefixing with r#
1185
+ /// Binding keyword that stays valid for the wildcard `_`. A `_` binding cannot be `mut`
1186
+ /// (`error: mut must be followed by a named binding`) and is never reassigned, so it always
1187
+ /// takes a plain `let`. `base` is the keyword for a normal binding here (e.g. `"let mut"`).
1188
+ fn mut_kw_for<'a>(name: &str, base: &'a str) -> &'a str {
1189
+ if name == "_" {
1190
+ "let"
1191
+ } else {
1192
+ base
1193
+ }
1194
+ }
1195
+
1101
1196
  fn escape_ident(name: &str) -> Cow<'_, str> {
1102
1197
  // Rust standard library macros that conflict with variable names
1103
1198
  const RUST_MACROS: &[&str] = &[
@@ -1227,15 +1322,46 @@ impl Codegen {
1227
1322
  }
1228
1323
  }
1229
1324
 
1230
- /// Generate code for a bitwise binary operation.
1325
+ /// Emit a valid Rust `f64` expression for `n`, handling non-finite values. Constant-folding can
1326
+ /// produce Infinity/NaN (e.g. `5/0` → `f64::INFINITY`, `0/0` → `f64::NAN`), which the plain
1327
+ /// `format!("{}_f64", n)` would render as the INVALID Rust `inf_f64` / `NaN_f64`. Finite values
1328
+ /// keep the literal `{n}_f64` form.
1329
+ fn f64_lit(n: f64) -> String {
1330
+ if n.is_nan() {
1331
+ "f64::NAN".to_string()
1332
+ } else if n.is_infinite() {
1333
+ if n > 0.0 {
1334
+ "f64::INFINITY".to_string()
1335
+ } else {
1336
+ "f64::NEG_INFINITY".to_string()
1337
+ }
1338
+ } else {
1339
+ format!("{}_f64", n)
1340
+ }
1341
+ }
1342
+
1343
+ /// Generate code for a bitwise binary operation (`& | ^`). `to_int32` is JS ToInt32
1344
+ /// (modulo 2³², NaN/±Infinity → 0) — out-of-range operands wrap, not saturate.
1231
1345
  fn emit_bitwise_binop(l: &str, r: &str, op: &str) -> String {
1232
1346
  format!(
1233
1347
  "Value::Number({{ let Value::Number(a) = &({}) else {{ panic!() }}; \
1234
- let Value::Number(b) = &({}) else {{ panic!() }}; ((*a as i32) {} (*b as i32)) as f64 }})",
1348
+ let Value::Number(b) = &({}) else {{ panic!() }}; (tishlang_runtime::to_int32(*a) {} tishlang_runtime::to_int32(*b)) as f64 }})",
1235
1349
  l, r, op
1236
1350
  )
1237
1351
  }
1238
1352
 
1353
+ /// Generate code for a shift (`<< >> >>>`). `a_to` is the left-operand coercion
1354
+ /// (`to_int32` signed, `to_uint32` for the logical `>>>`); `method` is the `wrapping_sh*`
1355
+ /// call. Counts go through `to_uint32` then mask to 5 bits — exact JS semantics, panic-free.
1356
+ fn emit_shift_binop(l: &str, r: &str, a_to: &str, method: &str) -> String {
1357
+ format!(
1358
+ "Value::Number({{ let Value::Number(a) = &({}) else {{ panic!() }}; \
1359
+ let Value::Number(b) = &({}) else {{ panic!() }}; \
1360
+ tishlang_runtime::{}(*a).{}(tishlang_runtime::to_uint32(*b)) as f64 }})",
1361
+ l, r, a_to, method
1362
+ )
1363
+ }
1364
+
1239
1365
  fn write(&mut self, s: &str) {
1240
1366
  self.output.push_str(s);
1241
1367
  }
@@ -1308,7 +1434,7 @@ impl Codegen {
1308
1434
  self.write("use std::cell::RefCell;\n");
1309
1435
  self.write("use std::rc::Rc;\n");
1310
1436
  self.write("use std::sync::Arc;\n");
1311
- 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, date_now as tish_date_now, array_is_array as tish_array_is_array, string_from_char_code as tish_string_from_char_code, 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_uint8_array_constructor, tish_audio_context_constructor, register_static_route as tish_register_static_route, ObjectMap, TishError, Value, VmRef};\n");
1437
+ 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, 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, 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, TishError, Value, VmRef};\n");
1312
1438
  if self.program_has_jsx {
1313
1439
  self.write("use tishlang_ui::{fragment_value, install_thread_local_host, native_create_root, native_use_state, ui_h, ui_text, HeadlessHost};\n");
1314
1440
  }
@@ -1319,8 +1445,11 @@ impl Codegen {
1319
1445
  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");
1320
1446
  }
1321
1447
  if self.has_feature("http") {
1448
+ // `register_static_route` is http-gated in the runtime; emit its import only when http is
1449
+ // linked, else a non-http `tish build --feature …` fails with an unresolved import.
1450
+ self.write("use tishlang_runtime::register_static_route as tish_register_static_route;\n");
1322
1451
  if self.is_async {
1323
- 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, promise_object as tish_promise_object, await_promise as tish_await_promise};\n");
1452
+ 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, promise_object as tish_promise_object, await_promise as tish_await_promise, await_promise_throw as tish_await_promise_throw};\n");
1324
1453
  } else {
1325
1454
  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");
1326
1455
  }
@@ -1374,6 +1503,21 @@ impl Codegen {
1374
1503
  self.writeln("}");
1375
1504
  self.writeln("");
1376
1505
  }
1506
+ // M5 (dark-shipped behind TISH_NATIVE_FN): emit a parallel native `fn f_native` for each
1507
+ // eligible top-level numeric fn at top level; direct calls route to it in emit_typed_expr.
1508
+ if std::env::var("TISH_NATIVE_FN").map(|v| v != "0").unwrap_or(false) {
1509
+ self.native_fns = Self::collect_native_fns(&program.statements);
1510
+ if !self.native_fns.is_empty() {
1511
+ self.emit_native_fns(&program.statements)?;
1512
+ self.writeln("");
1513
+ }
1514
+ }
1515
+ // Soundness pass — must run after type aliases + `native_fns` are known (both feed the
1516
+ // native-type oracle): find `number`-typed locals a reassignment can turn non-numeric so
1517
+ // `VarDecl` lowers them as boxed `Value` rather than native `f64` (else the store coerces
1518
+ // and panics on a JS string-concat result like `s = s + arr[i]`). See
1519
+ // `collect_demoted_numeric_locals` / `demoted_numeric_locals`.
1520
+ self.demoted_numeric_locals = self.collect_demoted_numeric_locals(&program.statements);
1377
1521
  if self.is_async {
1378
1522
  self.writeln("async fn run() -> Result<(), Box<dyn std::error::Error>> {");
1379
1523
  } else if self.emit_mode == crate::NativeEmitMode::EmbeddedLib {
@@ -1398,9 +1542,13 @@ impl Codegen {
1398
1542
  self.writeln("let parseFloat = Value::native(|args: &[Value]| tish_parse_float(args));");
1399
1543
  self.writeln("let decodeURI = Value::native(|args: &[Value]| tish_decode_uri(args));");
1400
1544
  self.writeln("let encodeURI = Value::native(|args: &[Value]| tish_encode_uri(args));");
1401
- self.writeln(
1402
- r#"let registerStaticRoute = Value::native(|args: &[Value]| { let path = match args.get(0) { Some(Value::String(s)) => s.to_string(), _ => return Value::Null }; let body = match args.get(1) { Some(Value::String(s)) => s.as_bytes().to_vec(), _ => return Value::Null }; let ct = match args.get(2) { Some(Value::String(s)) => s.to_string(), _ => "application/octet-stream".to_string() }; tish_register_static_route(&path, &body, &ct); Value::Null });"#,
1403
- );
1545
+ // `registerStaticRoute` calls the http-gated runtime fn, so only bind it when http is linked
1546
+ // (matches the conditional `use` above; otherwise non-http builds fail to resolve it).
1547
+ if self.has_feature("http") {
1548
+ self.writeln(
1549
+ r#"let registerStaticRoute = Value::native(|args: &[Value]| { let path = match args.get(0) { Some(Value::String(s)) => s.to_string(), _ => return Value::Null }; let body = match args.get(1) { Some(Value::String(s)) => s.as_bytes().to_vec(), _ => return Value::Null }; let ct = match args.get(2) { Some(Value::String(s)) => s.to_string(), _ => "application/octet-stream".to_string() }; tish_register_static_route(&path, &body, &ct); Value::Null });"#,
1550
+ );
1551
+ }
1404
1552
  self.writeln(
1405
1553
  "let htmlEscape = Value::native(|args: &[Value]| tish_escape_html(args.first().unwrap_or(&Value::Null)));",
1406
1554
  );
@@ -1443,6 +1591,22 @@ impl Codegen {
1443
1591
  self.writeln(
1444
1592
  "(Arc::from(\"imul\"), Value::native(|args: &[Value]| tish_math_imul(args))),",
1445
1593
  );
1594
+ // Hyperbolic / inverse-hyperbolic / cbrt / base-2/10 logs (issue #61).
1595
+ for (name, func) in [
1596
+ ("sinh", "tish_math_sinh"),
1597
+ ("cosh", "tish_math_cosh"),
1598
+ ("tanh", "tish_math_tanh"),
1599
+ ("asinh", "tish_math_asinh"),
1600
+ ("acosh", "tish_math_acosh"),
1601
+ ("atanh", "tish_math_atanh"),
1602
+ ("cbrt", "tish_math_cbrt"),
1603
+ ("log2", "tish_math_log2"),
1604
+ ("log10", "tish_math_log10"),
1605
+ ] {
1606
+ self.writeln(&format!(
1607
+ "(Arc::from(\"{name}\"), Value::native(|args: &[Value]| {func}(args))),"
1608
+ ));
1609
+ }
1446
1610
  self.writeln("(Arc::from(\"PI\"), Value::Number(std::f64::consts::PI)),");
1447
1611
  self.writeln("(Arc::from(\"E\"), Value::Number(std::f64::consts::E)),");
1448
1612
  self.indent -= 1;
@@ -1461,21 +1625,32 @@ impl Codegen {
1461
1625
  self.writeln(
1462
1626
  "(Arc::from(\"isArray\"), Value::native(|args: &[Value]| tish_array_is_array(args))),",
1463
1627
  );
1628
+ // `Array(n)` / `new Array(n)` constructor (issue #72); `__call` covers both forms.
1629
+ self.writeln(
1630
+ "(Arc::from(\"__call\"), Value::native(|args: &[Value]| tish_array_construct(args))),",
1631
+ );
1464
1632
  self.indent -= 1;
1465
1633
  self.writeln("]));");
1466
1634
 
1467
1635
  self.writeln("let String = Value::object(ObjectMap::from([");
1468
1636
  self.indent += 1;
1469
1637
  self.writeln("(Arc::from(\"fromCharCode\"), Value::native(|args: &[Value]| tish_string_from_char_code(args))),");
1638
+ // `String(value)` callable: `value_call` dispatches objects via `__call`, like `Symbol`.
1639
+ self.writeln("(Arc::from(\"__call\"), Value::native(|args: &[Value]| tish_string_convert(args))),");
1470
1640
  self.indent -= 1;
1471
1641
  self.writeln("]));");
1472
1642
 
1473
- self.writeln("let Date = Value::object(ObjectMap::from([");
1643
+ // `Number(value)` coercion callable (issue #36).
1644
+ self.writeln("let Number = Value::object(ObjectMap::from([");
1474
1645
  self.indent += 1;
1475
- self.writeln("(Arc::from(\"now\"), Value::native(|args: &[Value]| tish_date_now(args))),");
1646
+ self.writeln("(Arc::from(\"__call\"), Value::native(|args: &[Value]| tish_number_convert(args))),");
1476
1647
  self.indent -= 1;
1477
1648
  self.writeln("]));");
1478
1649
 
1650
+ self.writeln("let Date = tish_date_constructor();");
1651
+ self.writeln("let Set = tish_set_constructor();");
1652
+ self.writeln("let Map = tish_map_constructor();");
1653
+
1479
1654
  self.writeln("let Symbol = tish_symbol_object();");
1480
1655
 
1481
1656
  self.writeln("let Object = Value::object(ObjectMap::from([");
@@ -1496,8 +1671,20 @@ impl Codegen {
1496
1671
  self.indent -= 1;
1497
1672
  self.writeln("]));");
1498
1673
 
1674
+ self.writeln("let Float64Array = tish_float64_array_constructor();");
1675
+ self.writeln("let Float32Array = tish_float32_array_constructor();");
1676
+ self.writeln("let Int8Array = tish_int8_array_constructor();");
1499
1677
  self.writeln("let Uint8Array = tish_uint8_array_constructor();");
1678
+ self.writeln("let Uint8ClampedArray = tish_uint8_clamped_array_constructor();");
1679
+ self.writeln("let Int16Array = tish_int16_array_constructor();");
1680
+ self.writeln("let Uint16Array = tish_uint16_array_constructor();");
1681
+ self.writeln("let Int32Array = tish_int32_array_constructor();");
1682
+ self.writeln("let Uint32Array = tish_uint32_array_constructor();");
1500
1683
  self.writeln("let AudioContext = tish_audio_context_constructor();");
1684
+ // Error constructors (issue #60): `new Error(msg)` / `Error(msg)` → `{ name, message }`.
1685
+ for name in ["Error", "TypeError", "RangeError", "SyntaxError"] {
1686
+ self.writeln(&format!("let {name} = tish_error_constructor({name:?});"));
1687
+ }
1501
1688
  if self.program_uses_document {
1502
1689
  self.writeln("let document = VmRef::new(tish_canvas_document());");
1503
1690
  self.refcell_wrapped_vars.insert("document".to_string());
@@ -1560,7 +1747,7 @@ impl Codegen {
1560
1747
  self.writeln("match handler {");
1561
1748
  self.indent += 1;
1562
1749
  self.writeln(
1563
- "Value::Function(f) => tish_http_serve(args, move |req_args| f(req_args)),",
1750
+ "Value::Function(f) => tish_http_serve(args, move |req_args| f.call(req_args)),",
1564
1751
  );
1565
1752
  self.writeln("Value::Object(ref opts) => {");
1566
1753
  self.indent += 1;
@@ -1620,7 +1807,7 @@ impl Codegen {
1620
1807
  self.usage_analyzer = Some(analyzer);
1621
1808
 
1622
1809
  // Prepass: vars mutated by nested closures must be RefCell from the start (top-level)
1623
- let top_level_mutated = Self::collect_vars_mutated_by_nested_closures(&program.statements);
1810
+ let top_level_mutated = Self::collect_vars_needing_capture_cell(&program.statements);
1624
1811
  for v in &top_level_mutated {
1625
1812
  self.refcell_wrapped_vars.insert(v.clone());
1626
1813
  }
@@ -1635,6 +1822,14 @@ impl Codegen {
1635
1822
  self.async_context_stack.pop();
1636
1823
  }
1637
1824
 
1825
+ // Run pending timers to completion before exiting — the JS event loop drains the
1826
+ // timer queue after top-level code finishes. Without this the rust backend drops
1827
+ // `setTimeout(cb, 0)` callbacks that never coincided with a blocking-op drain,
1828
+ // diverging from interp/vm/cranelift/wasi (which drain at end-of-program).
1829
+ if self.has_feature("timers") {
1830
+ self.writeln("tishlang_runtime::drain_timers();");
1831
+ }
1832
+
1638
1833
  self.writeln("Ok(())");
1639
1834
  self.indent -= 1;
1640
1835
  self.writeln("}");
@@ -1655,6 +1850,122 @@ impl Codegen {
1655
1850
  Ok(())
1656
1851
  }
1657
1852
 
1853
+ /// Emit an expression in **statement position** (its value is discarded). For a native
1854
+ /// assignment this emits only the side-effect — NOT the boxed `Value::Number(..)` that the
1855
+ /// expression form returns (JS "assignment yields its value"). In a hot loop that boxed
1856
+ /// value was constructed + dropped every iteration, and because `Value` has a non-trivial
1857
+ /// `Drop` (other variants hold `Rc`/`Arc`) LLVM couldn't prove it dead — so it could not
1858
+ /// vectorize/fold the loop. Falls back to `emit_expr` for everything else (whose trailing
1859
+ /// value is simply dropped by the `;`).
1860
+ fn emit_expr_discard(&mut self, expr: &Expr) -> Result<String, CompileError> {
1861
+ match expr {
1862
+ Expr::Assign { name, value, .. } => {
1863
+ let rust_type = self.type_context.get_type(name.as_ref());
1864
+ // String self-append `s = s + rhs` -> in-place push_str (amortized O(1)). The
1865
+ // general path boxes via `ops::add(Value::String(s.clone()), ...)` which clones
1866
+ // the whole string per concat -> O(n^2) string building. rhs must be String-typed.
1867
+ if rust_type == RustType::String {
1868
+ if let Expr::Binary {
1869
+ left,
1870
+ op: BinOp::Add,
1871
+ right,
1872
+ ..
1873
+ } = value.as_ref()
1874
+ {
1875
+ if matches!(left.as_ref(), Expr::Ident { name: ln, .. } if ln.as_ref() == name.as_ref())
1876
+ {
1877
+ let (rhs_code, rhs_ty) = self.emit_typed_expr(right.as_ref())?;
1878
+ if rhs_ty == RustType::String {
1879
+ let escaped = Self::escape_ident(name.as_ref());
1880
+ if self.refcell_wrapped_vars.contains(name.as_ref()) {
1881
+ return Ok(format!(
1882
+ "{{ let _r = {}; {}.borrow_mut().push_str(&_r); }}",
1883
+ rhs_code, escaped
1884
+ ));
1885
+ }
1886
+ return Ok(format!(
1887
+ "{{ let _r = {}; {}.push_str(&_r); }}",
1888
+ rhs_code, escaped
1889
+ ));
1890
+ }
1891
+ }
1892
+ }
1893
+ }
1894
+ if matches!(rust_type, RustType::F64 | RustType::Bool | RustType::String) {
1895
+ let escaped = Self::escape_ident(name.as_ref());
1896
+ let is_ref = self.refcell_wrapped_vars.contains(name.as_ref());
1897
+ let (val_code, val_ty) = self.emit_typed_expr(value)?;
1898
+ let native_val = if val_ty == RustType::Value {
1899
+ rust_type.from_value_expr(&val_code)
1900
+ } else {
1901
+ val_code
1902
+ };
1903
+ if is_ref {
1904
+ return Ok(format!(
1905
+ "{{ let _assign_tmp = {}; *{}.borrow_mut() = _assign_tmp; }}",
1906
+ native_val, escaped
1907
+ ));
1908
+ }
1909
+ return Ok(format!("{} = {}", escaped, native_val));
1910
+ }
1911
+ }
1912
+ // `i++` / `++i` / `i--` / `--i` in statement position (incl. for-loop update):
1913
+ // emit just the native increment, no boxed `Value::Number(_prev)`.
1914
+ Expr::PostfixInc { name, .. } | Expr::PrefixInc { name, .. } => {
1915
+ if self.type_context.get_type(name.as_ref()) == RustType::F64 {
1916
+ let n = Self::escape_ident(name.as_ref());
1917
+ if self.refcell_wrapped_vars.contains(name.as_ref()) {
1918
+ return Ok(format!("*{}.borrow_mut() += 1.0_f64", n));
1919
+ }
1920
+ return Ok(format!("{} += 1.0_f64", n));
1921
+ }
1922
+ }
1923
+ Expr::PostfixDec { name, .. } | Expr::PrefixDec { name, .. } => {
1924
+ if self.type_context.get_type(name.as_ref()) == RustType::F64 {
1925
+ let n = Self::escape_ident(name.as_ref());
1926
+ if self.refcell_wrapped_vars.contains(name.as_ref()) {
1927
+ return Ok(format!("*{}.borrow_mut() -= 1.0_f64", n));
1928
+ }
1929
+ return Ok(format!("{} -= 1.0_f64", n));
1930
+ }
1931
+ }
1932
+ // `s += x` etc. in statement position: native f64 compound op, no boxed return.
1933
+ Expr::CompoundAssign { name, op, value, .. } => {
1934
+ if self.type_context.get_type(name.as_ref()) == RustType::F64 {
1935
+ let n = Self::escape_ident(name.as_ref());
1936
+ let is_refcell = self.refcell_wrapped_vars.contains(name.as_ref());
1937
+ let (rhs_code, rhs_ty) = self.emit_typed_expr(value)?;
1938
+ let rhs_f64 = if rhs_ty == RustType::F64 {
1939
+ rhs_code
1940
+ } else {
1941
+ let rhs_val = if rhs_ty.is_native() {
1942
+ rhs_ty.to_value_expr(&rhs_code)
1943
+ } else {
1944
+ rhs_code
1945
+ };
1946
+ format!("(match &({}) {{ Value::Number(n) => *n, v => panic!(\"compound assign: expected number, got {{:?}}\", v) }})", rhs_val)
1947
+ };
1948
+ let op_str = match op {
1949
+ CompoundOp::Add => "+=",
1950
+ CompoundOp::Sub => "-=",
1951
+ CompoundOp::Mul => "*=",
1952
+ CompoundOp::Div => "/=",
1953
+ CompoundOp::Mod => "%=",
1954
+ };
1955
+ if is_refcell {
1956
+ return Ok(format!(
1957
+ "{{ let _op_rhs = {}; *{}.borrow_mut() {} _op_rhs; }}",
1958
+ rhs_f64, n, op_str
1959
+ ));
1960
+ }
1961
+ return Ok(format!("{} {} {}", n, op_str, rhs_f64));
1962
+ }
1963
+ }
1964
+ _ => {}
1965
+ }
1966
+ self.emit_expr(expr)
1967
+ }
1968
+
1658
1969
  fn emit_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
1659
1970
  match stmt {
1660
1971
  Statement::Block { statements, .. } => {
@@ -1666,7 +1977,7 @@ impl Codegen {
1666
1977
  .push(std::collections::HashSet::new());
1667
1978
  // Prepass: vars that must be RefCell because nested closures capture and mutate them
1668
1979
  let vars_mutated_by_nested =
1669
- Self::collect_vars_mutated_by_nested_closures(statements);
1980
+ Self::collect_vars_needing_capture_cell(statements);
1670
1981
  for v in &vars_mutated_by_nested {
1671
1982
  self.refcell_wrapped_vars.insert(v.clone());
1672
1983
  }
@@ -1694,6 +2005,13 @@ impl Codegen {
1694
2005
  self.indent -= 1;
1695
2006
  self.writeln("}");
1696
2007
  }
2008
+ // Comma-declarators: emit each declarator into the *current* Rust scope
2009
+ // (no wrapping `{}`), so the bindings stay visible to later statements.
2010
+ Statement::Multi { statements, .. } => {
2011
+ for s in statements {
2012
+ self.emit_statement(s)?;
2013
+ }
2014
+ }
1697
2015
  Statement::VarDecl {
1698
2016
  name,
1699
2017
  mutable,
@@ -1705,13 +2023,22 @@ impl Codegen {
1705
2023
  // user-declared `type` aliases so a `let x: World = ...`
1706
2024
  // resolves to `RustType::Named { name: "World", fields }`
1707
2025
  // and we can emit a struct move instead of a Value box.
1708
- let rust_type = type_ann
2026
+ let mut rust_type = type_ann
1709
2027
  .as_ref()
1710
2028
  .map(|t| {
1711
2029
  crate::types::RustType::from_annotation_with_aliases(t, &self.type_aliases)
1712
2030
  })
1713
2031
  .unwrap_or(RustType::Value);
1714
2032
 
2033
+ // Soundness: a `number` local that a reassignment can turn non-numeric (e.g.
2034
+ // `s = s + arr[i]`, JS string concat) must stay a boxed `Value` — a native-f64
2035
+ // store would panic at the `from_value_expr(F64)` coercion. See
2036
+ // `demoted_numeric_locals`.
2037
+ if rust_type == RustType::F64 && self.demoted_numeric_locals.contains(name.as_ref())
2038
+ {
2039
+ rust_type = RustType::Value;
2040
+ }
2041
+
1715
2042
  // Track the variable type
1716
2043
  self.type_context.define(name.as_ref(), rust_type.clone());
1717
2044
 
@@ -1789,7 +2116,7 @@ impl Codegen {
1789
2116
  self.register_destruct_pattern_outer_vars(pattern);
1790
2117
  }
1791
2118
  Statement::ExprStmt { expr, .. } => {
1792
- let e = self.emit_expr(expr)?;
2119
+ let e = self.emit_expr_discard(expr)?;
1793
2120
  self.writeln(&format!("{};", e));
1794
2121
  }
1795
2122
  Statement::If {
@@ -1816,9 +2143,11 @@ impl Codegen {
1816
2143
  let label = format!("'while_loop_{}", self.loop_label_index);
1817
2144
  self.loop_label_index += 1;
1818
2145
  self.loop_stack.push((label.clone(), None));
2146
+ self.break_stack.push(label.clone());
1819
2147
  self.write(&format!("{}: while {} {{\n", label, c));
1820
2148
  self.indent += 1;
1821
2149
  self.emit_statement(body)?;
2150
+ self.break_stack.pop();
1822
2151
  self.loop_stack.pop();
1823
2152
  self.indent -= 1;
1824
2153
  self.writeln("}");
@@ -1829,42 +2158,102 @@ impl Codegen {
1829
2158
  body,
1830
2159
  ..
1831
2160
  } => {
1832
- let iter_expr = self.emit_expr(iterable)?;
1833
- self.writeln(&format!("{{ let _fof = ({}).clone();", iter_expr));
1834
- self.indent += 1;
1835
- self.writeln("match &_fof {");
1836
- self.indent += 1;
1837
- self.writeln("Value::Array(ref _arr) => {");
1838
- self.indent += 1;
1839
- self.writeln("for _v in _arr.borrow().iter() {");
1840
- self.indent += 1;
1841
- self.writeln(&format!(
1842
- "let {} = _v.clone();",
1843
- Self::escape_ident(name.as_ref())
1844
- ));
1845
- self.emit_statement(body)?;
1846
- self.indent -= 1;
1847
- self.writeln("}");
1848
- self.indent -= 1;
1849
- self.writeln("}");
1850
- self.writeln("Value::String(ref _s) => {");
1851
- self.indent += 1;
1852
- self.writeln("for _ch in _s.chars() {");
1853
- self.indent += 1;
1854
- self.writeln(&format!(
1855
- "let {} = Value::String(std::sync::Arc::from(_ch.to_string()));",
1856
- Self::escape_ident(name.as_ref())
1857
- ));
1858
- self.emit_statement(body)?;
1859
- self.indent -= 1;
1860
- self.writeln("}");
1861
- self.indent -= 1;
1862
- self.writeln("}");
1863
- self.writeln("_ => panic!(\"for-of requires array or string\"),");
1864
- self.indent -= 1;
1865
- self.writeln("}");
1866
- self.indent -= 1;
1867
- self.writeln("}");
2161
+ // M3 native fast path: the iterable is a `Vec<elem>` local with a native element
2162
+ // type (e.g. `let xs: number[]` -> `Vec<f64>`) and the body never mentions it (so
2163
+ // iterating by reference can't alias a mutation). Bind the loop var as `elem` so the
2164
+ // body lowers natively — no per-element `Value::clone`, and accumulators stay f64.
2165
+ let mut emitted_native = false;
2166
+ if let Expr::Ident { name: it_name, .. } = iterable {
2167
+ if let RustType::Vec(elem) = self.type_context.get_type(it_name.as_ref()) {
2168
+ if elem.is_native() {
2169
+ let mut body_idents = std::collections::HashSet::new();
2170
+ Self::collect_stmt_idents(body, &mut body_idents);
2171
+ if !body_idents.contains(it_name.as_ref()) {
2172
+ let esc_it = Self::escape_ident(it_name.as_ref()).into_owned();
2173
+ let esc_name = Self::escape_ident(name.as_ref()).into_owned();
2174
+ // Index-based iteration (not `.iter().cloned()`, which rustc fails to
2175
+ // tighten here): `0..len` indexing of a `Vec<f64>` matches a hand-
2176
+ // written C-style loop. Unique counter names keep nested ForOf sound.
2177
+ let idx = self.loop_label_index;
2178
+ self.loop_label_index += 1;
2179
+ let copy_elem = matches!(*elem, RustType::F64 | RustType::Bool);
2180
+ let bind = if copy_elem {
2181
+ format!("let {} = {}[_fof_i{}];", esc_name, esc_it, idx)
2182
+ } else {
2183
+ format!("let {} = {}[_fof_i{}].clone();", esc_name, esc_it, idx)
2184
+ };
2185
+ self.writeln(&format!("for _fof_i{} in 0..{}.len() {{", idx, esc_it));
2186
+ self.indent += 1;
2187
+ self.writeln(&bind);
2188
+ self.type_context.push_scope();
2189
+ self.type_context.define(name.as_ref(), *elem);
2190
+ self.emit_statement(body)?;
2191
+ self.type_context.pop_scope();
2192
+ self.indent -= 1;
2193
+ self.writeln("}");
2194
+ emitted_native = true;
2195
+ }
2196
+ }
2197
+ }
2198
+ }
2199
+ if !emitted_native {
2200
+ let iter_expr = self.emit_expr(iterable)?;
2201
+ // `normalize_for_of` drains a JS iterator object (Map/Set `.values()` etc.)
2202
+ // into an array; arrays/strings/everything else pass through unchanged.
2203
+ self.writeln(&format!(
2204
+ "{{ let _fof = tishlang_runtime::normalize_for_of(({}).clone());",
2205
+ iter_expr
2206
+ ));
2207
+ self.indent += 1;
2208
+ self.writeln("match &_fof {");
2209
+ self.indent += 1;
2210
+ self.writeln("Value::Array(ref _arr) => {");
2211
+ self.indent += 1;
2212
+ self.writeln("for _v in _arr.borrow().iter() {");
2213
+ self.indent += 1;
2214
+ self.writeln(&format!(
2215
+ "let {} = _v.clone();",
2216
+ Self::escape_ident(name.as_ref())
2217
+ ));
2218
+ self.emit_statement(body)?;
2219
+ self.indent -= 1;
2220
+ self.writeln("}");
2221
+ self.indent -= 1;
2222
+ self.writeln("}");
2223
+ // Packed `Float64Array` (`TISH_PACKED_ARRAYS`): iterate the `Vec<f64>` directly,
2224
+ // re-boxing each element to `Value::Number` for the loop body.
2225
+ self.writeln("Value::NumberArray(ref _arr) => {");
2226
+ self.indent += 1;
2227
+ self.writeln("for _v in _arr.borrow().iter() {");
2228
+ self.indent += 1;
2229
+ self.writeln(&format!(
2230
+ "let {} = Value::Number(*_v);",
2231
+ Self::escape_ident(name.as_ref())
2232
+ ));
2233
+ self.emit_statement(body)?;
2234
+ self.indent -= 1;
2235
+ self.writeln("}");
2236
+ self.indent -= 1;
2237
+ self.writeln("}");
2238
+ self.writeln("Value::String(ref _s) => {");
2239
+ self.indent += 1;
2240
+ self.writeln("for _ch in _s.chars() {");
2241
+ self.indent += 1;
2242
+ self.writeln(&format!(
2243
+ "let {} = Value::String(tishlang_runtime::ArcStr::from(_ch.to_string()));",
2244
+ Self::escape_ident(name.as_ref())
2245
+ ));
2246
+ self.emit_statement(body)?;
2247
+ self.indent -= 1;
2248
+ self.writeln("}");
2249
+ self.indent -= 1;
2250
+ self.writeln("}");
2251
+ self.writeln("_ => panic!(\"for-of requires array or string\"),");
2252
+ self.indent -= 1;
2253
+ self.writeln("}");
2254
+ self.indent -= 1;
2255
+ self.writeln("}");
2256
+ }
1868
2257
  }
1869
2258
  Statement::For {
1870
2259
  init,
@@ -1885,18 +2274,20 @@ impl Codegen {
1885
2274
  .map(|c| self.emit_cond_expr(c).unwrap())
1886
2275
  .unwrap_or_else(|| "true".to_string());
1887
2276
  let update_code = update.as_ref().map(|u| {
1888
- let ue = self.emit_expr(u).unwrap();
2277
+ let ue = self.emit_expr_discard(u).unwrap();
1889
2278
  format!("{};", ue)
1890
2279
  });
1891
2280
  self.loop_stack.push((label.clone(), update_code));
2281
+ self.break_stack.push(label.clone());
1892
2282
  self.write(&format!("{}: loop {{\n", label));
1893
2283
  self.indent += 1;
1894
2284
  self.writeln(&format!("if !{} {{ break; }}", cond_expr));
1895
2285
  self.emit_statement(body)?;
1896
2286
  if let Some(u) = update {
1897
- let ue = self.emit_expr(u)?;
2287
+ let ue = self.emit_expr_discard(u)?;
1898
2288
  self.writeln(&format!("{};", ue));
1899
2289
  }
2290
+ self.break_stack.pop();
1900
2291
  self.loop_stack.pop();
1901
2292
  self.indent -= 1;
1902
2293
  self.writeln("}");
@@ -1909,10 +2300,18 @@ impl Codegen {
1909
2300
  .map(|e| self.emit_expr(e))
1910
2301
  .transpose()?
1911
2302
  .unwrap_or_else(|| "Value::Null".to_string());
1912
- self.writeln(&format!("return {};", v));
2303
+ if self.try_closure_depth > 0 {
2304
+ // Inside a try-body closure: escape it as a pending-return completion so any
2305
+ // enclosing `finally` runs on the way out to the function boundary.
2306
+ self.writeln(&format!("return Ok(Some({}));", v));
2307
+ } else {
2308
+ self.writeln(&format!("return {};", v));
2309
+ }
1913
2310
  }
1914
2311
  Statement::Break { .. } => {
1915
- if let Some((label, _)) = self.loop_stack.last() {
2312
+ // `break` exits the innermost loop OR switch (break_stack), not necessarily the
2313
+ // innermost loop. A switch pushes a label here so its `break` stays switch-local.
2314
+ if let Some(label) = self.break_stack.last() {
1916
2315
  self.writeln(&format!("break {};", label));
1917
2316
  } else {
1918
2317
  self.writeln("break;");
@@ -1949,6 +2348,13 @@ impl Codegen {
1949
2348
  } => {
1950
2349
  let e = self.emit_expr(expr)?;
1951
2350
  self.writeln(&format!("let _sv = {};", e));
2351
+ // Wrap in a labeled block so `break` inside a case exits the SWITCH, not an
2352
+ // enclosing loop. tish switch has no fall-through (match's first-arm semantics).
2353
+ let sw_label = format!("'switch_{}", self.loop_label_index);
2354
+ self.loop_label_index += 1;
2355
+ self.break_stack.push(sw_label.clone());
2356
+ self.write(&format!("{}: {{\n", sw_label));
2357
+ self.indent += 1;
1952
2358
  self.writeln("match () {");
1953
2359
  self.indent += 1;
1954
2360
  for (case_expr, body) in cases {
@@ -1978,30 +2384,39 @@ impl Codegen {
1978
2384
  }
1979
2385
  self.indent -= 1;
1980
2386
  self.writeln("}");
2387
+ self.indent -= 1;
2388
+ self.writeln("}");
2389
+ self.break_stack.pop();
1981
2390
  }
1982
2391
  Statement::DoWhile { body, cond, .. } => {
1983
2392
  let c = self.emit_cond_expr(cond)?;
1984
2393
  let label = format!("'dowhile_loop_{}", self.loop_label_index);
1985
2394
  self.loop_label_index += 1;
1986
2395
  self.loop_stack.push((label.clone(), None));
2396
+ self.break_stack.push(label.clone());
1987
2397
  self.write(&format!("{}: loop {{\n", label));
1988
2398
  self.indent += 1;
1989
2399
  self.emit_statement(body)?;
1990
2400
  self.write(&format!("if !{} {{ break; }}\n", c));
2401
+ self.break_stack.pop();
1991
2402
  self.loop_stack.pop();
1992
2403
  self.indent -= 1;
1993
2404
  self.writeln("}");
1994
2405
  }
1995
2406
  Statement::Throw { value, .. } => {
1996
2407
  let v = self.emit_expr(value)?;
1997
- if self.value_fn_depth > 0 {
2408
+ if self.try_closure_depth > 0 || self.value_fn_depth == 0 {
2409
+ // Inside a try-body closure (so `catch`/`finally` can see it) or at top level
2410
+ // (run() returns a Result): a catchable error completion.
1998
2411
  self.writeln(&format!(
1999
- "{{ let _th = {}; panic!(\"uncaught throw: {{}}\", _th.to_display_string()); }}",
2412
+ "return Err(Box::new(tishlang_runtime::TishError::Throw({})) as Box<dyn std::error::Error>);",
2000
2413
  v
2001
2414
  ));
2002
2415
  } else {
2416
+ // Top of a value-fn body with no enclosing try: there is no error channel
2417
+ // across the native-fn ABI, so an uncaught throw aborts (matches prior behavior).
2003
2418
  self.writeln(&format!(
2004
- "return Err(Box::new(tishlang_runtime::TishError::Throw({})) as Box<dyn std::error::Error>);",
2419
+ "{{ let _th = {}; panic!(\"uncaught throw: {{}}\", _th.to_display_string()); }}",
2005
2420
  v
2006
2421
  ));
2007
2422
  }
@@ -2013,58 +2428,81 @@ impl Codegen {
2013
2428
  finally_body,
2014
2429
  ..
2015
2430
  } => {
2016
- self.writeln("let _try_result: Result<Value, Box<dyn std::error::Error>> = (|| {");
2431
+ // The try body runs in a completion closure:
2432
+ // Ok(None) = ran to the end normally
2433
+ // Ok(Some(v)) = a `return v` is pending (must run finally, then return)
2434
+ // Err(Throw) = a `throw` is pending (catchable; else runs finally then re-raises)
2435
+ // `return`/`throw` inside the body emit the closure-escaping form (try_closure_depth).
2436
+ self.writeln(
2437
+ "let mut _flow: Result<Option<Value>, Box<dyn std::error::Error>> = (|| {",
2438
+ );
2017
2439
  self.indent += 1;
2440
+ self.try_closure_depth += 1;
2018
2441
  self.emit_statement(body)?;
2019
- self.writeln("Ok(Value::Null)");
2442
+ self.try_closure_depth -= 1;
2443
+ self.writeln("Ok(None)");
2020
2444
  self.indent -= 1;
2021
2445
  self.writeln("})();");
2022
2446
 
2023
2447
  if let Some(catch_stmt) = catch_body {
2448
+ // Only a `throw` is catchable; a pending `return` (Ok(Some)) bypasses catch.
2449
+ self.writeln("_flow = match _flow {");
2450
+ self.indent += 1;
2451
+ self.writeln("Err(_e) => match _e.downcast::<tishlang_runtime::TishError>() {");
2452
+ self.indent += 1;
2453
+ self.writeln("Ok(_te) => match *_te {");
2454
+ self.indent += 1;
2455
+ self.writeln("tishlang_runtime::TishError::Throw(_tv) => {");
2456
+ self.indent += 1;
2024
2457
  if let Some(param) = catch_param {
2025
- self.writeln("if let Err(e) = _try_result {");
2026
- self.indent += 1;
2027
- self.writeln("match e.downcast::<tishlang_runtime::TishError>() {");
2028
- self.indent += 1;
2029
- self.writeln("Ok(tish_err) => {");
2030
- self.indent += 1;
2031
- self.writeln("if let tishlang_runtime::TishError::Throw(v) = *tish_err {");
2032
- self.writeln(&format!(
2033
- "let {} = v.clone();",
2034
- Self::escape_ident(param.as_ref())
2035
- ));
2036
- self.emit_statement(catch_stmt)?;
2037
- if self.value_fn_depth > 0 {
2038
- self.writeln(
2039
- "} else { panic!(\"unhandled error in native Tish: {:?}\", *tish_err); }",
2040
- );
2041
- } else {
2042
- self.writeln("} else { return Err(Box::new(tish_err)); }");
2043
- }
2044
- self.indent -= 1;
2045
- self.writeln("}");
2046
- if self.value_fn_depth > 0 {
2047
- self.writeln(
2048
- "Err(orig) => panic!(\"non-Tish error in native Tish: {:?}\", orig),",
2049
- );
2050
- } else {
2051
- self.writeln("Err(orig) => return Err(orig),");
2052
- }
2053
- self.indent -= 1;
2054
- self.writeln("}");
2055
- self.indent -= 1;
2056
- } else {
2057
- self.writeln("if let Err(_e) = _try_result {");
2058
- self.indent += 1;
2059
- self.emit_statement(catch_stmt)?;
2060
- self.indent -= 1;
2458
+ self.writeln(&format!("let {} = _tv;", Self::escape_ident(param.as_ref())));
2061
2459
  }
2460
+ self.writeln(
2461
+ "(|| -> Result<Option<Value>, Box<dyn std::error::Error>> {",
2462
+ );
2463
+ self.indent += 1;
2464
+ self.try_closure_depth += 1;
2465
+ self.emit_statement(catch_stmt)?;
2466
+ self.try_closure_depth -= 1;
2467
+ self.writeln("Ok(None)");
2468
+ self.indent -= 1;
2469
+ self.writeln("})()");
2470
+ self.indent -= 1;
2062
2471
  self.writeln("}");
2472
+ self.writeln("_other => Err(Box::new(_other)),");
2473
+ self.indent -= 1;
2474
+ self.writeln("},");
2475
+ self.writeln("Err(_orig) => Err(_orig),");
2476
+ self.indent -= 1;
2477
+ self.writeln("},");
2478
+ self.writeln("_ok => _ok,");
2479
+ self.indent -= 1;
2480
+ self.writeln("};");
2063
2481
  }
2064
2482
 
2065
2483
  if let Some(finally_stmt) = finally_body {
2066
2484
  self.emit_statement(finally_stmt)?;
2067
2485
  }
2486
+
2487
+ // After finally, propagate any pending completion in the form the enclosing context
2488
+ // expects (an outer try-closure / a value-fn body / top-level run()).
2489
+ self.writeln("match _flow {");
2490
+ self.indent += 1;
2491
+ if self.try_closure_depth > 0 {
2492
+ self.writeln("Ok(Some(_rv)) => return Ok(Some(_rv)),");
2493
+ self.writeln("Err(_e) => return Err(_e),");
2494
+ } else if self.value_fn_depth > 0 {
2495
+ self.writeln("Ok(Some(_rv)) => return _rv,");
2496
+ self.writeln("Err(_e) => return tishlang_runtime::fn_unwind(_e),");
2497
+ } else {
2498
+ // Top level (run() -> Result<(), _>): a top-level `return value` just ends the
2499
+ // script (the value is unobservable); an uncaught throw propagates out of run().
2500
+ self.writeln("Ok(Some(_)) => return Ok(()),");
2501
+ self.writeln("Err(_e) => return Err(_e),");
2502
+ }
2503
+ self.writeln("Ok(None) => {}");
2504
+ self.indent -= 1;
2505
+ self.writeln("}");
2068
2506
  }
2069
2507
  Statement::FunDecl {
2070
2508
  name,
@@ -2127,6 +2565,8 @@ impl Codegen {
2127
2565
  "Math",
2128
2566
  "JSON",
2129
2567
  "Date",
2568
+ "Set",
2569
+ "Map",
2130
2570
  "Object",
2131
2571
  "process",
2132
2572
  "setTimeout",
@@ -2142,18 +2582,22 @@ impl Codegen {
2142
2582
  })
2143
2583
  .collect();
2144
2584
 
2145
- // Outer vars that are assigned in the body need RefCell (capture cell, add to refcell_wrapped_vars).
2146
- // Read-only outer vars get a Value binding to avoid nested_complex param-shadow issues.
2585
+ // Live cell capture: assigned in this body, or already a shared
2586
+ // `VmRef` cell in a parent scope (so a closure that only READS the
2587
+ // var still sees later mutations through the shared cell, instead
2588
+ // of snapshotting it by value at creation time). Truly read-only,
2589
+ // non-cell vars get a Value snapshot (avoids param-shadow issues).
2590
+ // Mirrors `emit_arrow_function`.
2147
2591
  let mut assigned_in_body = HashSet::new();
2148
2592
  Self::collect_assigned_idents_in_stmt(body, &mut assigned_in_body);
2149
2593
  let mutable_outer_vars: Vec<String> = outer_vars
2150
2594
  .iter()
2151
- .filter(|v| assigned_in_body.contains(*v))
2595
+ .filter(|v| assigned_in_body.contains(*v) || self.rc_cell_storage_contains(v))
2152
2596
  .cloned()
2153
2597
  .collect();
2154
2598
  let read_only_outer_vars: Vec<String> = outer_vars
2155
2599
  .iter()
2156
- .filter(|v| !assigned_in_body.contains(*v))
2600
+ .filter(|v| !assigned_in_body.contains(*v) && !self.rc_cell_storage_contains(v))
2157
2601
  .cloned()
2158
2602
  .collect();
2159
2603
 
@@ -2226,8 +2670,18 @@ impl Codegen {
2226
2670
  "Math",
2227
2671
  "JSON",
2228
2672
  "Date",
2673
+ "Set",
2674
+ "Map",
2229
2675
  "Object",
2676
+ "Float64Array",
2677
+ "Float32Array",
2678
+ "Int8Array",
2230
2679
  "Uint8Array",
2680
+ "Uint8ClampedArray",
2681
+ "Int16Array",
2682
+ "Uint16Array",
2683
+ "Int32Array",
2684
+ "Uint32Array",
2231
2685
  "AudioContext",
2232
2686
  "process",
2233
2687
  "setTimeout",
@@ -2300,14 +2754,82 @@ impl Codegen {
2300
2754
  .map(|n| n.to_string())
2301
2755
  .collect();
2302
2756
  let formal_span = *span;
2757
+ // M1 (keystone, dark-shipped behind TISH_PARAM_NATIVE): a typed scalar param
2758
+ // normally arrives boxed (`args.get(i).cloned()`), which poisons native math in
2759
+ // the body (e.g. `i*N+k` boxes). Bind a *native shadow* — coerce once to f64/
2760
+ // bool/String — so the body lowers it like a native local. Conservative: only
2761
+ // simple params, native-scalar annotation, no default value.
2762
+ let param_native =
2763
+ std::env::var("TISH_PARAM_NATIVE").map(|v| v != "0").unwrap_or(false);
2764
+ // A param referenced by ANY sibling default expr (e.g. `(a, b = a + 1)`) must NOT
2765
+ // get a native f64 shadow: the default binding is emitted on the boxed Value path
2766
+ // (`ops::add(&a, …)` expects `&Value`), so a native `a: f64` would mistype the
2767
+ // generated Rust. Keep such params boxed — correctness over the M1 optimization;
2768
+ // defaults referencing params are rare in hot code. Also covers the M4 case where
2769
+ // an unannotated param (e.g. `dependent(a, b = a + 1)`) is inferred numeric.
2770
+ let mut default_referenced: std::collections::HashSet<String> =
2771
+ std::collections::HashSet::new();
2772
+ for p in params {
2773
+ if let FunParam::Simple(tp) = p {
2774
+ if let Some(d) = &tp.default {
2775
+ Self::collect_expr_idents(d, &mut default_referenced);
2776
+ }
2777
+ }
2778
+ }
2779
+ let mut native_params: Vec<(String, RustType)> = Vec::new();
2303
2780
  for (i, p) in params.iter().enumerate() {
2304
2781
  match p {
2305
2782
  FunParam::Simple(tp) => {
2306
- self.writeln(&format!(
2307
- "let mut {} = args.get({}).cloned().unwrap_or(Value::Null);",
2308
- Self::escape_ident(tp.name.as_ref()),
2309
- i
2310
- ));
2783
+ let native_ty = if param_native
2784
+ && tp.default.is_none()
2785
+ && !default_referenced.contains(tp.name.as_ref())
2786
+ {
2787
+ tp.type_ann
2788
+ .as_ref()
2789
+ .map(RustType::from_annotation)
2790
+ .filter(|t| {
2791
+ matches!(
2792
+ t,
2793
+ RustType::F64 | RustType::Bool | RustType::String
2794
+ )
2795
+ })
2796
+ } else {
2797
+ None
2798
+ };
2799
+ if let Some(nt) = native_ty {
2800
+ let coercion = nt.from_value_expr(&format!(
2801
+ "args.get({}).cloned().unwrap_or(Value::Null)",
2802
+ i
2803
+ ));
2804
+ self.writeln(&format!(
2805
+ "{} {} = {};",
2806
+ Self::mut_kw_for(tp.name.as_ref(), "let mut"),
2807
+ Self::escape_ident(tp.name.as_ref()),
2808
+ coercion
2809
+ ));
2810
+ native_params.push((tp.name.to_string(), nt));
2811
+ } else if let Some(default_expr) = &tp.default {
2812
+ // Default applies only when the positional arg is MISSING
2813
+ // (`args.get(i) == None`), matching the interpreter + bytecode VM.
2814
+ // An explicit `null` argument is "supplied" and keeps the null.
2815
+ // Earlier params are already bound above, so a default may
2816
+ // reference them, e.g. `(a, b = a + 1)`.
2817
+ let default_str = self.emit_expr(default_expr)?;
2818
+ self.writeln(&format!(
2819
+ "{} {} = match args.get({}) {{ Some(v) => v.clone(), None => {} }};",
2820
+ Self::mut_kw_for(tp.name.as_ref(), "let mut"),
2821
+ Self::escape_ident(tp.name.as_ref()),
2822
+ i,
2823
+ default_str
2824
+ ));
2825
+ } else {
2826
+ self.writeln(&format!(
2827
+ "{} {} = args.get({}).cloned().unwrap_or(Value::Null);",
2828
+ Self::mut_kw_for(tp.name.as_ref(), "let mut"),
2829
+ Self::escape_ident(tp.name.as_ref()),
2830
+ i
2831
+ ));
2832
+ }
2311
2833
  }
2312
2834
  FunParam::Destructure { pattern, .. } => {
2313
2835
  let tmp = format!("_formal_{}", i);
@@ -2319,16 +2841,47 @@ impl Codegen {
2319
2841
  }
2320
2842
  }
2321
2843
  }
2844
+ // A typed rest-param `...args: number[]` lowers to a native `Vec<elem>` (unbox each
2845
+ // trailing arg) instead of a boxed `Value::Array`, so the body iterates/indexes it
2846
+ // natively (and `for (let x of args)` keeps accumulators `f64`). Non-native element
2847
+ // types fall back to the boxed array.
2848
+ let rest_native: Option<RustType> = rest_param.as_ref().and_then(|rp| {
2849
+ rp.type_ann.as_ref().and_then(|ann| {
2850
+ match RustType::from_annotation_with_aliases(ann, &self.type_aliases) {
2851
+ RustType::Vec(elem) if elem.is_native() => Some(RustType::Vec(elem)),
2852
+ _ => None,
2853
+ }
2854
+ })
2855
+ });
2322
2856
  if let Some(rest) = rest_param {
2323
- self.writeln(&format!(
2324
- "let {} = Value::Array(VmRef::new(args[{}..].to_vec()));",
2325
- Self::escape_ident(rest.name.as_ref()),
2326
- params.len()
2327
- ));
2857
+ if let Some(RustType::Vec(elem)) = &rest_native {
2858
+ self.writeln(&format!(
2859
+ "let {}: Vec<{}> = args[{}..].iter().map(|v| {}).collect();",
2860
+ Self::escape_ident(rest.name.as_ref()),
2861
+ elem.to_rust_type_str(),
2862
+ params.len(),
2863
+ elem.from_value_expr("v")
2864
+ ));
2865
+ } else {
2866
+ self.writeln(&format!(
2867
+ "let {} = Value::Array(VmRef::new(args[{}..].to_vec()));",
2868
+ Self::escape_ident(rest.name.as_ref()),
2869
+ params.len()
2870
+ ));
2871
+ }
2328
2872
  }
2329
2873
 
2330
2874
  self.type_context
2331
2875
  .push_fun_param_scope(params, rest_param.as_ref());
2876
+ // Register native-shadowed params (bound above) with their native type so the
2877
+ // body lowers them exactly like native locals (binops, indices, etc.).
2878
+ for (pname, pty) in &native_params {
2879
+ self.type_context.define(pname, pty.clone());
2880
+ }
2881
+ // A native `Vec` rest-param: register so the body iterates/indexes it natively.
2882
+ if let (Some(rest), Some(rt)) = (rest_param.as_ref(), rest_native.as_ref()) {
2883
+ self.type_context.define(rest.name.as_ref(), rt.clone());
2884
+ }
2332
2885
 
2333
2886
  let fun_body_res: Result<(), CompileError> = (|| -> Result<(), CompileError> {
2334
2887
  // Push current params to stack for nested functions
@@ -2362,9 +2915,22 @@ impl Codegen {
2362
2915
  escaped
2363
2916
  ));
2364
2917
  }
2918
+ // Vars declared in this body that a nested closure captures
2919
+ // and that are assigned somewhere in the body must be shared
2920
+ // `VmRef` cells (e.g. `let t=0; let f=()=>t; t=100`). Block
2921
+ // scopes get this via emit_statement(Block); a function body
2922
+ // is iterated directly, so run the same prepass here.
2923
+ let body_cell_vars =
2924
+ Self::collect_vars_needing_capture_cell(statements);
2925
+ for v in &body_cell_vars {
2926
+ self.refcell_wrapped_vars.insert(v.clone());
2927
+ }
2365
2928
  for s in statements {
2366
2929
  self.emit_statement(s)?;
2367
2930
  }
2931
+ for v in &body_cell_vars {
2932
+ self.refcell_wrapped_vars.remove(v);
2933
+ }
2368
2934
  self.function_scope_stack.pop();
2369
2935
  self.outer_vars_stack.pop();
2370
2936
  self.rc_cell_storage_scopes.pop();
@@ -2435,7 +3001,7 @@ impl Codegen {
2435
3001
  }
2436
3002
  CallArg::Spread(e) => {
2437
3003
  let val = self.emit_expr(e)?;
2438
- parts.push(format!("if let Value::Array(ref _spread) = {} {{ _args.extend(_spread.borrow().iter().cloned()); }}", val));
3004
+ parts.push(format!("if let Value::Array(ref _spread) = tishlang_runtime::normalize_for_of(({}).clone()) {{ _args.extend(_spread.borrow().iter().cloned()); }}", val));
2439
3005
  }
2440
3006
  }
2441
3007
  }
@@ -2480,7 +3046,7 @@ impl Codegen {
2480
3046
  DestructElement::Ident(name, _) => {
2481
3047
  self.writeln(&format!(
2482
3048
  "{} {} = match &({}) {{ Value::Array(ref _a) => _a.borrow().get({}).cloned().unwrap_or(Value::Null), _ => Value::Null }};",
2483
- mutability,
3049
+ Self::mut_kw_for(name.as_ref(), mutability),
2484
3050
  Self::escape_ident(name.as_ref()),
2485
3051
  value_expr,
2486
3052
  i
@@ -2497,7 +3063,7 @@ impl Codegen {
2497
3063
  DestructElement::Rest(name, _) => {
2498
3064
  self.writeln(&format!(
2499
3065
  "{} {} = match &({}) {{ Value::Array(ref _a) => {{ let _b = _a.borrow(); Value::Array(VmRef::new(_b.iter().skip({}).cloned().collect())) }}, _ => Value::Array(VmRef::new(Vec::new())) }};",
2500
- mutability,
3066
+ Self::mut_kw_for(name.as_ref(), mutability),
2501
3067
  Self::escape_ident(name.as_ref()),
2502
3068
  value_expr,
2503
3069
  i
@@ -2514,7 +3080,7 @@ impl Codegen {
2514
3080
  DestructElement::Ident(name, _) => {
2515
3081
  self.writeln(&format!(
2516
3082
  "{} {} = match &({}) {{ Value::Object(ref _o) => _o.borrow().strings.get({:?}).cloned().unwrap_or(Value::Null), _ => Value::Null }};",
2517
- mutability,
3083
+ Self::mut_kw_for(name.as_ref(), mutability),
2518
3084
  Self::escape_ident(name.as_ref()),
2519
3085
  value_expr,
2520
3086
  key
@@ -2585,7 +3151,7 @@ impl Codegen {
2585
3151
  fn emit_expr(&mut self, expr: &Expr) -> Result<String, CompileError> {
2586
3152
  Ok(match expr {
2587
3153
  Expr::Literal { value, .. } => match value {
2588
- Literal::Number(n) => format!("Value::Number({}_f64)", n),
3154
+ Literal::Number(n) => format!("Value::Number({})", Self::f64_lit(*n)),
2589
3155
  Literal::String(s) => format!("Value::String({:?}.into())", s.as_ref()),
2590
3156
  Literal::Bool(b) => format!("Value::Bool({})", b),
2591
3157
  Literal::Null => "Value::Null".to_string(),
@@ -2633,7 +3199,7 @@ impl Codegen {
2633
3199
  o
2634
3200
  ),
2635
3201
  UnaryOp::BitNot => format!(
2636
- "Value::Number({{ let Value::Number(n) = &({}) else {{ panic!(\"Expected number\") }}; (!(*n as i32)) as f64 }})",
3202
+ "Value::Number({{ let Value::Number(n) = &({}) else {{ panic!(\"Expected number\") }}; (!tishlang_runtime::to_int32(*n)) as f64 }})",
2637
3203
  o
2638
3204
  ),
2639
3205
  UnaryOp::Void => format!("{{ {}; Value::Null }}", o),
@@ -2656,8 +3222,7 @@ impl Codegen {
2656
3222
  {
2657
3223
  if method_name.as_ref() == "stringify"
2658
3224
  && matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "JSON")
2659
- {
2660
- if args.len() == 1 {
3225
+ && args.len() == 1 {
2661
3226
  if let CallArg::Expr(arg) = &args[0] {
2662
3227
  let (arg_code, arg_ty) = self.emit_typed_expr(arg)?;
2663
3228
  match &arg_ty {
@@ -2682,52 +3247,6 @@ impl Codegen {
2682
3247
  }
2683
3248
  }
2684
3249
  }
2685
- }
2686
- }
2687
-
2688
- // Compile-time embed: Polars.read_csv("<literal path>") when file exists
2689
- if let Some(init) = self.native_module_init.get("tish:polars") {
2690
- let crate_name = match init {
2691
- crate::resolve::NativeModuleInit::Legacy { crate_name, .. } => {
2692
- crate_name.as_str()
2693
- }
2694
- crate::resolve::NativeModuleInit::Generated { shim_crate, .. } => {
2695
- shim_crate.as_str()
2696
- }
2697
- };
2698
- if let (Some(root), Some(CallArg::Expr(first_arg))) =
2699
- (self.project_root.as_ref(), args.first())
2700
- {
2701
- if let Expr::Member {
2702
- object,
2703
- prop: MemberProp::Name { name: ref method_name, .. },
2704
- ..
2705
- } = callee.as_ref()
2706
- {
2707
- if method_name.as_ref() == "read_csv"
2708
- && matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Polars")
2709
- {
2710
- if let Expr::Literal {
2711
- value: Literal::String(ref path),
2712
- ..
2713
- } = first_arg
2714
- {
2715
- let path_str = path.as_ref();
2716
- let normalized = path_str.trim_start_matches("./");
2717
- let full_path = root.join(normalized);
2718
- if full_path.exists() {
2719
- if let Ok(content) = std::fs::read_to_string(&full_path) {
2720
- let escaped = format!("{:?}", content);
2721
- return Ok(format!(
2722
- "{}::polars_read_csv_from_string_runtime({})",
2723
- crate_name, escaped
2724
- ));
2725
- }
2726
- }
2727
- }
2728
- }
2729
- }
2730
- }
2731
3250
  }
2732
3251
 
2733
3252
  // Check for built-in method calls on arrays/strings
@@ -2839,6 +3358,15 @@ impl Codegen {
2839
3358
  "reverse" => {
2840
3359
  return Ok(format!("tishlang_runtime::array_reverse(&{})", obj_expr));
2841
3360
  }
3361
+ "fill" => {
3362
+ let value = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
3363
+ let start = arg_exprs.get(1).cloned().unwrap_or_else(|| "Value::Null".to_string());
3364
+ let end = arg_exprs.get(2).cloned().unwrap_or_else(|| "Value::Null".to_string());
3365
+ return Ok(format!(
3366
+ "tishlang_runtime::array_fill(&{}, &{}, &{}, &{})",
3367
+ obj_expr, value, start, end
3368
+ ));
3369
+ }
2842
3370
  "shuffle" => {
2843
3371
  return Ok(format!("tishlang_runtime::array_shuffle(&{})", obj_expr));
2844
3372
  }
@@ -2923,13 +3451,24 @@ impl Codegen {
2923
3451
  obj_expr, search, replacement
2924
3452
  ));
2925
3453
  }
2926
- "match" if cfg!(feature = "regex") => {
3454
+ // Gate on the *requested* feature (has_feature), not tish_compile's own
3455
+ // cfg!(feature="regex") — the generated binary links the runtime's regex
3456
+ // impls when the build requests regex, regardless of how tish_compile was
3457
+ // compiled. Falls through to a generic call (no-regex builds) otherwise.
3458
+ "match" if self.has_feature("regex") => {
2927
3459
  let regexp = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
2928
3460
  return Ok(format!(
2929
3461
  "tishlang_runtime::string_match_regex(&{}, &{})",
2930
3462
  obj_expr, regexp
2931
3463
  ));
2932
3464
  }
3465
+ "search" if self.has_feature("regex") => {
3466
+ let regexp = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
3467
+ return Ok(format!(
3468
+ "tishlang_runtime::string_search_regex(&{}, &{})",
3469
+ obj_expr, regexp
3470
+ ));
3471
+ }
2933
3472
  "charAt" => {
2934
3473
  let idx = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Number(0.0)".to_string());
2935
3474
  return Ok(format!(
@@ -2975,6 +3514,13 @@ impl Codegen {
2975
3514
  obj_expr, digits
2976
3515
  ));
2977
3516
  }
3517
+ "toString" => {
3518
+ let radix = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
3519
+ return Ok(format!(
3520
+ "tishlang_runtime::number_to_string(&{}, &{})",
3521
+ obj_expr, radix
3522
+ ));
3523
+ }
2978
3524
  // Higher-order array methods
2979
3525
  "map" => {
2980
3526
  let callback = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
@@ -2991,8 +3537,20 @@ impl Codegen {
2991
3537
  ));
2992
3538
  }
2993
3539
  "reduce" => {
2994
- let callback = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
2995
3540
  let initial = arg_exprs.get(1).cloned().unwrap_or_else(|| "Value::Null".to_string());
3541
+ // Fused reduce (TISH_FUSED_HOF): `arr.reduce((acc, x) => acc OP x, init)`
3542
+ // with a plain binop of the two params → a native fold using the SAME
3543
+ // runtime Value op the closure body would, eliminating the per-element
3544
+ // `value_call`. Sound (identical Value semantics, incl. string `+`).
3545
+ // Requires an explicit init; anything else falls back to array_reduce.
3546
+ if std::env::var("TISH_FUSED_HOF").is_ok() && args.len() == 2 {
3547
+ if let Some(fold) =
3548
+ self.try_fused_reduce(args, &obj_expr, &initial)?
3549
+ {
3550
+ return Ok(fold);
3551
+ }
3552
+ }
3553
+ let callback = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
2996
3554
  return Ok(format!(
2997
3555
  "tishlang_runtime::array_reduce(&{}, &{}, &{})",
2998
3556
  obj_expr, callback, initial
@@ -3161,6 +3719,27 @@ impl Codegen {
3161
3719
  }
3162
3720
  }
3163
3721
  }
3722
+ // Generalize the typed struct-field fast path to `xs[i].field` (array-of-structs):
3723
+ // when `object` indexes a `Vec<Named>`, do native struct field access.
3724
+ if !optional {
3725
+ if let (Expr::Index { .. }, MemberProp::Name { name: prop_name, .. }) =
3726
+ (object.as_ref(), prop)
3727
+ {
3728
+ let (obj_code, obj_ty) = self.emit_typed_expr(object)?;
3729
+ if let RustType::Named { fields, .. } = &obj_ty {
3730
+ if let Some((_, field_ty)) =
3731
+ fields.iter().find(|(k, _)| k.as_ref() == prop_name.as_ref())
3732
+ {
3733
+ let access = format!(
3734
+ "({}).{}",
3735
+ obj_code,
3736
+ crate::types::field_ident(prop_name.as_ref())
3737
+ );
3738
+ return Ok(field_ty.to_value_expr(&access));
3739
+ }
3740
+ }
3741
+ }
3742
+ }
3164
3743
  let obj = self.emit_expr(object)?;
3165
3744
  let key = match prop {
3166
3745
  MemberProp::Name { name, .. } => format!("{:?}", name.as_ref()),
@@ -3233,7 +3812,7 @@ impl Codegen {
3233
3812
  }
3234
3813
  ArrayElement::Spread(e) => {
3235
3814
  let val = self.emit_expr(e)?;
3236
- parts.push(format!("if let Value::Array(ref _spread) = {} {{ _arr.extend(_spread.borrow().iter().cloned()); }}", val));
3815
+ parts.push(format!("if let Value::Array(ref _spread) = tishlang_runtime::normalize_for_of(({}).clone()) {{ _arr.extend(_spread.borrow().iter().cloned()); }}", val));
3237
3816
  }
3238
3817
  }
3239
3818
  }
@@ -3294,10 +3873,9 @@ impl Codegen {
3294
3873
  }
3295
3874
  }
3296
3875
  }
3297
- format!(
3298
- "Value::object(ObjectMap::from([{}]))",
3299
- parts.join(", ")
3300
- )
3876
+ // Build the PropMap directly (no intermediate AHashMap) — one
3877
+ // inline allocation for small objects (the common case).
3878
+ format!("Value::object_from_pairs([{}])", parts.join(", "))
3301
3879
  }
3302
3880
  }
3303
3881
  Expr::Assign { name, value, .. } => {
@@ -3359,26 +3937,36 @@ impl Codegen {
3359
3937
  #[cfg(feature = "http")]
3360
3938
  if self.is_async {
3361
3939
  let _in_async = self.async_context_stack.last().copied().unwrap_or(false);
3940
+ // A rejected awaited promise must THROW (so a surrounding try/catch fires).
3941
+ // Use the throwing `?`-variant wherever an error channel exists — the SAME
3942
+ // condition `throw` uses to emit `return Err(..)` (inside a try body, or the
3943
+ // top-level run()). Elsewhere there is no channel, so fall back to the
3944
+ // value-returning variant (matches the existing uncaught-throw limitation).
3945
+ let (awaiter, q) = if self.try_closure_depth > 0 || self.value_fn_depth == 0 {
3946
+ ("tish_await_promise_throw", "?")
3947
+ } else {
3948
+ ("tish_await_promise", "")
3949
+ };
3362
3950
  if let Expr::Call { callee, args, .. } = operand.as_ref() {
3363
3951
  if let Expr::Ident { name, .. } = callee.as_ref() {
3364
3952
  let args_code = self.emit_call_args(args)?;
3365
3953
  return Ok(match name.as_ref() {
3366
3954
  "fetch" => {
3367
- format!("tish_await_promise(tish_fetch_promise({}))", args_code)
3955
+ format!("{}(tish_fetch_promise({})){}", awaiter, args_code, q)
3368
3956
  }
3369
3957
  "fetchAll" => {
3370
- format!("tish_await_promise(tish_fetch_all_promise({}))", args_code)
3958
+ format!("{}(tish_fetch_all_promise({})){}", awaiter, args_code, q)
3371
3959
  }
3372
3960
  _ => {
3373
3961
  let o = self.emit_expr(operand)?;
3374
- return Ok(format!("tish_await_promise({})", o));
3962
+ return Ok(format!("{}({}){}", awaiter, o, q));
3375
3963
  }
3376
3964
  });
3377
3965
  }
3378
3966
  }
3379
3967
  // await Call with non-Ident callee, or await Promise value: wrap in await_promise
3380
3968
  let o = self.emit_expr(operand)?;
3381
- return Ok(format!("tish_await_promise({})", o));
3969
+ return Ok(format!("{}({}){}", awaiter, o, q));
3382
3970
  }
3383
3971
  // Fallback: emit operand as sync call (no real .await in our model)
3384
3972
  let o = self.emit_expr(operand)?;
@@ -3396,6 +3984,27 @@ impl Codegen {
3396
3984
  o
3397
3985
  )
3398
3986
  }
3987
+ Expr::Delete { target, .. } => match target.as_ref() {
3988
+ Expr::Member { object, prop: MemberProp::Name { name, .. }, .. } => {
3989
+ let obj = self.emit_expr(object)?;
3990
+ format!(
3991
+ "tishlang_runtime::delete_property(&{}, &Value::String({:?}.into()))",
3992
+ obj,
3993
+ name.as_ref()
3994
+ )
3995
+ }
3996
+ Expr::Member { object, prop: MemberProp::Expr(key), .. } => {
3997
+ let obj = self.emit_expr(object)?;
3998
+ let k = self.emit_expr(key)?;
3999
+ format!("tishlang_runtime::delete_property(&{}, &{})", obj, k)
4000
+ }
4001
+ Expr::Index { object, index, .. } => {
4002
+ let obj = self.emit_expr(object)?;
4003
+ let idx = self.emit_expr(index)?;
4004
+ format!("tishlang_runtime::delete_property(&{}, &{})", obj, idx)
4005
+ }
4006
+ _ => "Value::Bool(true)".to_string(),
4007
+ },
3399
4008
  Expr::PostfixInc { name, .. } => self.emit_inc_dec(name.as_ref(), false, "+ 1.0", "++"),
3400
4009
  Expr::PostfixDec { name, .. } => self.emit_inc_dec(name.as_ref(), false, "- 1.0", "--"),
3401
4010
  Expr::PrefixInc { name, .. } => self.emit_inc_dec(name.as_ref(), true, "+ 1.0", "++"),
@@ -3473,7 +4082,7 @@ impl Codegen {
3473
4082
  Value::Number(n) => {{ let i = *n as i64; if (*n - i as f64).abs() < f64::EPSILON {{ i.to_string() }} else {{ n.to_string() }} }}, \
3474
4083
  Value::Bool(b) => b.to_string(), \
3475
4084
  Value::Null => \"null\".to_string(), \
3476
- other => format!(\"{{:?}}\", other) }}",
4085
+ other => other.to_js_string() }}",
3477
4086
  rhs_val
3478
4087
  )
3479
4088
  };
@@ -3500,7 +4109,7 @@ impl Codegen {
3500
4109
  Value::Number(n) => {{ let i = *n as i64; if (*n - i as f64).abs() < f64::EPSILON {{ i.to_string() }} else {{ n.to_string() }} }}, \
3501
4110
  Value::Bool(b) => b.to_string(), \
3502
4111
  Value::Null => \"null\".to_string(), \
3503
- other => format!(\"{{:?}}\", other) }}",
4112
+ other => other.to_js_string() }}",
3504
4113
  rhs_val
3505
4114
  )
3506
4115
  };
@@ -3682,10 +4291,25 @@ impl Codegen {
3682
4291
  // both native but different type — best effort
3683
4292
  val_code
3684
4293
  };
3685
- return Ok(format!(
3686
- "{{ {}[{}] = {}; Value::Null }}",
3687
- esc_obj, idx_usize, native_val
3688
- ));
4294
+ // OOB-safe write for numeric/bool Vecs: JS `a[i] = x` past the end
4295
+ // grows the array (holes read back as `undefined` → NaN/false), it does
4296
+ // not panic. In-bounds is the same direct store. Other element types keep
4297
+ // the direct store (their OOB semantics aren't a native-inference target).
4298
+ let assign = match elem_type.as_ref() {
4299
+ RustType::F64 | RustType::Bool => {
4300
+ let pad = if matches!(elem_type.as_ref(), RustType::F64) {
4301
+ "f64::NAN"
4302
+ } else {
4303
+ "false"
4304
+ };
4305
+ format!(
4306
+ "{{ let _idx = {}; if _idx >= {}.len() {{ {}.resize(_idx + 1, {}); }} {}[_idx] = {}; Value::Null }}",
4307
+ idx_usize, esc_obj, esc_obj, pad, esc_obj, native_val
4308
+ )
4309
+ }
4310
+ _ => format!("{{ {}[{}] = {}; Value::Null }}", esc_obj, idx_usize, native_val),
4311
+ };
4312
+ return Ok(assign);
3689
4313
  }
3690
4314
  }
3691
4315
  }
@@ -3712,7 +4336,7 @@ impl Codegen {
3712
4336
  parts.push(format!("\"{}\"", escaped));
3713
4337
  if i < exprs.len() {
3714
4338
  let expr_code = self.emit_expr(&exprs[i])?;
3715
- parts.push(format!("&({}).to_display_string()", expr_code));
4339
+ parts.push(format!("&({}).to_js_string()", expr_code));
3716
4340
  }
3717
4341
  }
3718
4342
  format!("Value::String([{}].concat().into())", parts.join(", "))
@@ -3727,6 +4351,37 @@ impl Codegen {
3727
4351
  .map_err(|m| CompileError::new(m, None))?
3728
4352
  }
3729
4353
  Expr::New { callee, args, .. } => {
4354
+ // Packed-native fast path: `new Float64Array(...)` lowers to a packed
4355
+ // `Value::NumberArray` (`Vec<f64>`) instead of the boxed `Value::Array` the generic
4356
+ // `tish_construct` builds — `Float64Array` is the one view whose element type *is*
4357
+ // f64, so it needs no coercion and avoids the per-element `Value` boxing. The helper
4358
+ // falls back to the identical boxed value when `TISH_PACKED_ARRAYS` is off, so default
4359
+ // builds stay byte-for-byte unchanged. The other typed-array views have no packed
4360
+ // `Value` variant (would need `Vec<f32>`/`Vec<i32>`/… + the 24-byte size assertion and
4361
+ // every exhaustive match), so they keep the generic path. Native-only: interp/VM value
4362
+ // bridges carry no `NumberArray`, so only the native runtime grew the support. Keyed on
4363
+ // the callee ident like the existing `JSON.`/`Polars.` special-cases.
4364
+ if matches!(callee.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Float64Array")
4365
+ {
4366
+ if args.iter().any(|a| matches!(a, CallArg::Spread(_))) {
4367
+ let args_code = self.emit_call_args(args)?;
4368
+ return Ok(format!(
4369
+ "{{ let _spread_args = {}; tishlang_runtime::float64_array_packed(&_spread_args[..]) }}",
4370
+ args_code
4371
+ ));
4372
+ }
4373
+ let arg_exprs: Result<Vec<_>, _> =
4374
+ args.iter().map(|a| self.emit_call_arg(a)).collect();
4375
+ let args_vec = arg_exprs?
4376
+ .iter()
4377
+ .map(|a| format!("{}.clone()", a))
4378
+ .collect::<Vec<_>>()
4379
+ .join(", ");
4380
+ return Ok(format!(
4381
+ "tishlang_runtime::float64_array_packed(&[{}])",
4382
+ args_vec
4383
+ ));
4384
+ }
3730
4385
  let callee_expr = self.emit_expr(callee)?;
3731
4386
  let has_spread = args.iter().any(|a| matches!(a, CallArg::Spread(_)));
3732
4387
  if has_spread {
@@ -3795,6 +4450,7 @@ impl Codegen {
3795
4450
  Self::collect_expr_idents(right, idents);
3796
4451
  }
3797
4452
  Expr::Unary { operand, .. } => Self::collect_expr_idents(operand, idents),
4453
+ Expr::Delete { target, .. } => Self::collect_expr_idents(target, idents),
3798
4454
  Expr::Call { callee, args, .. } => {
3799
4455
  Self::collect_expr_idents(callee, idents);
3800
4456
  for arg in args {
@@ -3931,8 +4587,19 @@ impl Codegen {
3931
4587
  fn collect_assigned_idents_in_stmt(stmt: &Statement, names: &mut HashSet<String>) {
3932
4588
  match stmt {
3933
4589
  Statement::ExprStmt { expr, .. } => Self::collect_assigned_idents_in_expr(expr, names),
3934
- Statement::VarDecl { .. } | Statement::VarDeclDestructure { .. } => {}
3935
- Statement::Block { statements, .. } => {
4590
+ // Descend into initializers: an assignment may live inside a closure
4591
+ // stored in a `let`/`const` (e.g. `let inc = () => { count = count + 1 }`).
4592
+ // The declared name itself is a binding, not an assignment, so it is
4593
+ // not added here. Closing this gap also closes it for arrow-block
4594
+ // bodies, which are scanned via collect_assigned_idents_in_expr.
4595
+ Statement::VarDecl { init: Some(e), .. } => {
4596
+ Self::collect_assigned_idents_in_expr(e, names)
4597
+ }
4598
+ Statement::VarDecl { init: None, .. } => {}
4599
+ Statement::VarDeclDestructure { init, .. } => {
4600
+ Self::collect_assigned_idents_in_expr(init, names)
4601
+ }
4602
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
3936
4603
  for s in statements {
3937
4604
  Self::collect_assigned_idents_in_stmt(s, names);
3938
4605
  }
@@ -4066,6 +4733,7 @@ impl Codegen {
4066
4733
  Self::collect_assigned_idents_in_expr(right, names);
4067
4734
  }
4068
4735
  Expr::Unary { operand, .. } => Self::collect_assigned_idents_in_expr(operand, names),
4736
+ Expr::Delete { target, .. } => Self::collect_assigned_idents_in_expr(target, names),
4069
4737
  Expr::Call { callee, args, .. } => {
4070
4738
  Self::collect_assigned_idents_in_expr(callee, names);
4071
4739
  for arg in args {
@@ -4197,9 +4865,11 @@ impl Codegen {
4197
4865
  }
4198
4866
  }
4199
4867
 
4200
- /// Collect variable names that are both captured and mutated by a closure body.
4201
- /// block_vars: vars declared in the enclosing block (candidates for mutation).
4202
- fn collect_mutated_captures_from_closure(
4868
+ /// Collect block vars captured (referenced) by this closure and any nested
4869
+ /// closures. block_vars: vars declared in the enclosing block. The caller
4870
+ /// (`collect_vars_needing_capture_cell`) further restricts to vars that are
4871
+ /// also assigned somewhere in the defining scope.
4872
+ fn collect_captured_block_vars_from_closure(
4203
4873
  params: &[FunParam],
4204
4874
  body: &Statement,
4205
4875
  block_vars: &HashSet<String>,
@@ -4214,8 +4884,6 @@ impl Codegen {
4214
4884
  Self::collect_local_var_names(body, &mut local_var_names);
4215
4885
  let mut referenced = HashSet::new();
4216
4886
  Self::collect_stmt_idents(body, &mut referenced);
4217
- let mut assigned = HashSet::new();
4218
- Self::collect_assigned_idents_in_stmt(body, &mut assigned);
4219
4887
  let outer_captured: HashSet<String> = referenced
4220
4888
  .difference(&param_names)
4221
4889
  .cloned()
@@ -4223,16 +4891,18 @@ impl Codegen {
4223
4891
  .difference(&local_var_names)
4224
4892
  .cloned()
4225
4893
  .collect();
4226
- for v in outer_captured.intersection(&assigned) {
4894
+ // Every block var this closure captures is a candidate; the caller keeps
4895
+ // only those also assigned somewhere in the defining scope.
4896
+ for v in &outer_captured {
4227
4897
  if block_vars.contains(v) {
4228
4898
  result.insert(v.clone());
4229
4899
  }
4230
4900
  }
4231
4901
  // Recurse into nested fns
4232
- Self::collect_mutated_captures_from_statements(body, block_vars, result);
4902
+ Self::collect_captured_block_vars_from_statements(body, block_vars, result);
4233
4903
  }
4234
4904
 
4235
- fn collect_mutated_captures_from_arrow(
4905
+ fn collect_captured_block_vars_from_arrow(
4236
4906
  params: &[FunParam],
4237
4907
  body: &ArrowBody,
4238
4908
  block_vars: &HashSet<String>,
@@ -4253,11 +4923,6 @@ impl Codegen {
4253
4923
  ArrowBody::Expr(e) => Self::collect_expr_idents(e, &mut referenced),
4254
4924
  ArrowBody::Block(s) => Self::collect_stmt_idents(s, &mut referenced),
4255
4925
  }
4256
- let mut assigned = HashSet::new();
4257
- match body {
4258
- ArrowBody::Expr(e) => Self::collect_assigned_idents_in_expr(e, &mut assigned),
4259
- ArrowBody::Block(s) => Self::collect_assigned_idents_in_stmt(s, &mut assigned),
4260
- }
4261
4926
  let outer_captured: HashSet<String> = referenced
4262
4927
  .difference(&param_names)
4263
4928
  .cloned()
@@ -4265,52 +4930,52 @@ impl Codegen {
4265
4930
  .difference(&local_var_names)
4266
4931
  .cloned()
4267
4932
  .collect();
4268
- for v in outer_captured.intersection(&assigned) {
4933
+ for v in &outer_captured {
4269
4934
  if block_vars.contains(v) {
4270
4935
  result.insert(v.clone());
4271
4936
  }
4272
4937
  }
4273
4938
  match body {
4274
- ArrowBody::Expr(e) => Self::collect_mutated_captures_from_expr(e, block_vars, result),
4939
+ ArrowBody::Expr(e) => Self::collect_captured_block_vars_from_expr(e, block_vars, result),
4275
4940
  ArrowBody::Block(s) => {
4276
- Self::collect_mutated_captures_from_statements(s, block_vars, result)
4941
+ Self::collect_captured_block_vars_from_statements(s, block_vars, result)
4277
4942
  }
4278
4943
  }
4279
4944
  }
4280
4945
 
4281
- fn collect_mutated_captures_from_expr(
4946
+ fn collect_captured_block_vars_from_expr(
4282
4947
  expr: &Expr,
4283
4948
  block_vars: &HashSet<String>,
4284
4949
  result: &mut HashSet<String>,
4285
4950
  ) {
4286
4951
  match expr {
4287
4952
  Expr::ArrowFunction { params, body, .. } => {
4288
- Self::collect_mutated_captures_from_arrow(params, body, block_vars, result);
4953
+ Self::collect_captured_block_vars_from_arrow(params, body, block_vars, result);
4289
4954
  }
4290
4955
  Expr::Call { callee, args, .. } => {
4291
- Self::collect_mutated_captures_from_expr(callee, block_vars, result);
4956
+ Self::collect_captured_block_vars_from_expr(callee, block_vars, result);
4292
4957
  for arg in args {
4293
4958
  match arg {
4294
4959
  CallArg::Expr(e) | CallArg::Spread(e) => {
4295
- Self::collect_mutated_captures_from_expr(e, block_vars, result);
4960
+ Self::collect_captured_block_vars_from_expr(e, block_vars, result);
4296
4961
  }
4297
4962
  }
4298
4963
  }
4299
4964
  }
4300
4965
  Expr::New { callee, args, .. } => {
4301
- Self::collect_mutated_captures_from_expr(callee, block_vars, result);
4966
+ Self::collect_captured_block_vars_from_expr(callee, block_vars, result);
4302
4967
  for arg in args {
4303
4968
  match arg {
4304
4969
  CallArg::Expr(e) | CallArg::Spread(e) => {
4305
- Self::collect_mutated_captures_from_expr(e, block_vars, result);
4970
+ Self::collect_captured_block_vars_from_expr(e, block_vars, result);
4306
4971
  }
4307
4972
  }
4308
4973
  }
4309
4974
  }
4310
4975
  Expr::Member { object, prop, .. } => {
4311
- Self::collect_mutated_captures_from_expr(object, block_vars, result);
4976
+ Self::collect_captured_block_vars_from_expr(object, block_vars, result);
4312
4977
  if let MemberProp::Expr(e) = prop {
4313
- Self::collect_mutated_captures_from_expr(e, block_vars, result);
4978
+ Self::collect_captured_block_vars_from_expr(e, block_vars, result);
4314
4979
  }
4315
4980
  }
4316
4981
  Expr::Conditional {
@@ -4319,19 +4984,19 @@ impl Codegen {
4319
4984
  else_branch,
4320
4985
  ..
4321
4986
  } => {
4322
- Self::collect_mutated_captures_from_expr(cond, block_vars, result);
4323
- Self::collect_mutated_captures_from_expr(then_branch, block_vars, result);
4324
- Self::collect_mutated_captures_from_expr(else_branch, block_vars, result);
4987
+ Self::collect_captured_block_vars_from_expr(cond, block_vars, result);
4988
+ Self::collect_captured_block_vars_from_expr(then_branch, block_vars, result);
4989
+ Self::collect_captured_block_vars_from_expr(else_branch, block_vars, result);
4325
4990
  }
4326
4991
  Expr::Binary { left, right, .. } | Expr::NullishCoalesce { left, right, .. } => {
4327
- Self::collect_mutated_captures_from_expr(left, block_vars, result);
4328
- Self::collect_mutated_captures_from_expr(right, block_vars, result);
4992
+ Self::collect_captured_block_vars_from_expr(left, block_vars, result);
4993
+ Self::collect_captured_block_vars_from_expr(right, block_vars, result);
4329
4994
  }
4330
4995
  Expr::Array { elements, .. } => {
4331
4996
  for el in elements {
4332
4997
  match el {
4333
4998
  ArrayElement::Expr(e) | ArrayElement::Spread(e) => {
4334
- Self::collect_mutated_captures_from_expr(e, block_vars, result);
4999
+ Self::collect_captured_block_vars_from_expr(e, block_vars, result);
4335
5000
  }
4336
5001
  }
4337
5002
  }
@@ -4340,7 +5005,7 @@ impl Codegen {
4340
5005
  for prop in props {
4341
5006
  match prop {
4342
5007
  ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => {
4343
- Self::collect_mutated_captures_from_expr(e, block_vars, result);
5008
+ Self::collect_captured_block_vars_from_expr(e, block_vars, result);
4344
5009
  }
4345
5010
  }
4346
5011
  }
@@ -4349,21 +5014,21 @@ impl Codegen {
4349
5014
  }
4350
5015
  }
4351
5016
 
4352
- fn collect_mutated_captures_from_statements(
5017
+ fn collect_captured_block_vars_from_statements(
4353
5018
  stmt: &Statement,
4354
5019
  block_vars: &HashSet<String>,
4355
5020
  result: &mut HashSet<String>,
4356
5021
  ) {
4357
5022
  match stmt {
4358
5023
  Statement::FunDecl { params, body, .. } => {
4359
- Self::collect_mutated_captures_from_closure(params, body, block_vars, result);
5024
+ Self::collect_captured_block_vars_from_closure(params, body, block_vars, result);
4360
5025
  }
4361
5026
  Statement::ExprStmt { expr, .. } => {
4362
- Self::collect_mutated_captures_from_expr(expr, block_vars, result);
5027
+ Self::collect_captured_block_vars_from_expr(expr, block_vars, result);
4363
5028
  }
4364
- Statement::Block { statements, .. } => {
5029
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
4365
5030
  for s in statements {
4366
- Self::collect_mutated_captures_from_statements(s, block_vars, result);
5031
+ Self::collect_captured_block_vars_from_statements(s, block_vars, result);
4367
5032
  }
4368
5033
  }
4369
5034
  Statement::If {
@@ -4372,10 +5037,10 @@ impl Codegen {
4372
5037
  else_branch,
4373
5038
  ..
4374
5039
  } => {
4375
- Self::collect_mutated_captures_from_expr(cond, block_vars, result);
4376
- Self::collect_mutated_captures_from_statements(then_branch, block_vars, result);
5040
+ Self::collect_captured_block_vars_from_expr(cond, block_vars, result);
5041
+ Self::collect_captured_block_vars_from_statements(then_branch, block_vars, result);
4377
5042
  if let Some(eb) = else_branch {
4378
- Self::collect_mutated_captures_from_statements(eb, block_vars, result);
5043
+ Self::collect_captured_block_vars_from_statements(eb, block_vars, result);
4379
5044
  }
4380
5045
  }
4381
5046
  Statement::For {
@@ -4386,23 +5051,23 @@ impl Codegen {
4386
5051
  ..
4387
5052
  } => {
4388
5053
  if let Some(i) = init {
4389
- Self::collect_mutated_captures_from_statements(i, block_vars, result);
5054
+ Self::collect_captured_block_vars_from_statements(i, block_vars, result);
4390
5055
  }
4391
5056
  if let Some(c) = cond {
4392
- Self::collect_mutated_captures_from_expr(c, block_vars, result);
5057
+ Self::collect_captured_block_vars_from_expr(c, block_vars, result);
4393
5058
  }
4394
5059
  if let Some(u) = update {
4395
- Self::collect_mutated_captures_from_expr(u, block_vars, result);
5060
+ Self::collect_captured_block_vars_from_expr(u, block_vars, result);
4396
5061
  }
4397
- Self::collect_mutated_captures_from_statements(body, block_vars, result);
5062
+ Self::collect_captured_block_vars_from_statements(body, block_vars, result);
4398
5063
  }
4399
5064
  Statement::ForOf { iterable, body, .. } => {
4400
- Self::collect_mutated_captures_from_expr(iterable, block_vars, result);
4401
- Self::collect_mutated_captures_from_statements(body, block_vars, result);
5065
+ Self::collect_captured_block_vars_from_expr(iterable, block_vars, result);
5066
+ Self::collect_captured_block_vars_from_statements(body, block_vars, result);
4402
5067
  }
4403
5068
  Statement::While { cond, body, .. } | Statement::DoWhile { body, cond, .. } => {
4404
- Self::collect_mutated_captures_from_expr(cond, block_vars, result);
4405
- Self::collect_mutated_captures_from_statements(body, block_vars, result);
5069
+ Self::collect_captured_block_vars_from_expr(cond, block_vars, result);
5070
+ Self::collect_captured_block_vars_from_statements(body, block_vars, result);
4406
5071
  }
4407
5072
  Statement::Switch {
4408
5073
  expr,
@@ -4410,18 +5075,18 @@ impl Codegen {
4410
5075
  default_body,
4411
5076
  ..
4412
5077
  } => {
4413
- Self::collect_mutated_captures_from_expr(expr, block_vars, result);
5078
+ Self::collect_captured_block_vars_from_expr(expr, block_vars, result);
4414
5079
  for (ce, stmts) in cases {
4415
5080
  if let Some(e) = ce {
4416
- Self::collect_mutated_captures_from_expr(e, block_vars, result);
5081
+ Self::collect_captured_block_vars_from_expr(e, block_vars, result);
4417
5082
  }
4418
5083
  for s in stmts {
4419
- Self::collect_mutated_captures_from_statements(s, block_vars, result);
5084
+ Self::collect_captured_block_vars_from_statements(s, block_vars, result);
4420
5085
  }
4421
5086
  }
4422
5087
  if let Some(stmts) = default_body {
4423
5088
  for s in stmts {
4424
- Self::collect_mutated_captures_from_statements(s, block_vars, result);
5089
+ Self::collect_captured_block_vars_from_statements(s, block_vars, result);
4425
5090
  }
4426
5091
  }
4427
5092
  }
@@ -4431,39 +5096,77 @@ impl Codegen {
4431
5096
  finally_body,
4432
5097
  ..
4433
5098
  } => {
4434
- Self::collect_mutated_captures_from_statements(body, block_vars, result);
5099
+ Self::collect_captured_block_vars_from_statements(body, block_vars, result);
4435
5100
  if let Some(c) = catch_body {
4436
- Self::collect_mutated_captures_from_statements(c, block_vars, result);
5101
+ Self::collect_captured_block_vars_from_statements(c, block_vars, result);
4437
5102
  }
4438
5103
  if let Some(f) = finally_body {
4439
- Self::collect_mutated_captures_from_statements(f, block_vars, result);
5104
+ Self::collect_captured_block_vars_from_statements(f, block_vars, result);
4440
5105
  }
4441
5106
  }
4442
5107
  Statement::VarDecl { init: Some(e), .. } => {
4443
- Self::collect_mutated_captures_from_expr(e, block_vars, result);
5108
+ Self::collect_captured_block_vars_from_expr(e, block_vars, result);
4444
5109
  }
4445
5110
  Statement::VarDeclDestructure { init, .. } => {
4446
- Self::collect_mutated_captures_from_expr(init, block_vars, result);
5111
+ Self::collect_captured_block_vars_from_expr(init, block_vars, result);
4447
5112
  }
4448
5113
  Statement::Return { value: Some(e), .. } => {
4449
- Self::collect_mutated_captures_from_expr(e, block_vars, result);
5114
+ Self::collect_captured_block_vars_from_expr(e, block_vars, result);
4450
5115
  }
4451
5116
  Statement::Throw { value, .. } => {
4452
- Self::collect_mutated_captures_from_expr(value, block_vars, result)
5117
+ Self::collect_captured_block_vars_from_expr(value, block_vars, result)
4453
5118
  }
4454
5119
  _ => {}
4455
5120
  }
4456
5121
  }
4457
5122
 
4458
- /// For a block, return var names that must be RefCell (captured and mutated by nested closures).
4459
- fn collect_vars_mutated_by_nested_closures(statements: &[Statement]) -> HashSet<String> {
5123
+ /// For a block, return the names of block-scoped vars that must live in a
5124
+ /// shared `VmRef` cell because a nested closure captures them by reference.
5125
+ ///
5126
+ /// A var needs a cell when it is BOTH (a) captured (referenced) by some nested
5127
+ /// closure AND (b) assigned somewhere in the defining scope. The assignment may
5128
+ /// be inside a closure (`counter()`, sibling `inc`/`get`) or in the enclosing
5129
+ /// scope — including AFTER the closure is created (`let t = 0; let f = () => t;
5130
+ /// t = 100`). Capture alone is not enough: a never-mutated var can be snapshot
5131
+ /// by value. The previous rule (captured AND mutated *inside* a closure) was too
5132
+ /// narrow — it snapshotted capture-then-mutate vars by value, so the rust backend
5133
+ /// returned the stale value and diverged from node/vm/interp/cranelift.
5134
+ fn collect_vars_needing_capture_cell(statements: &[Statement]) -> HashSet<String> {
4460
5135
  let mut block_vars = HashSet::new();
4461
5136
  Self::collect_block_var_names(statements, &mut block_vars);
4462
- let mut result = HashSet::new();
5137
+ // (a) Block vars captured by any nested closure.
5138
+ let mut captured = HashSet::new();
5139
+ for s in statements {
5140
+ Self::collect_captured_block_vars_from_statements(s, &block_vars, &mut captured);
5141
+ }
5142
+ // (b) Idents assigned anywhere in this scope (incl. inside closures).
5143
+ let mut assigned_in_scope = HashSet::new();
5144
+ for s in statements {
5145
+ Self::collect_assigned_idents_in_stmt(s, &mut assigned_in_scope);
5146
+ }
5147
+ captured.retain(|v| assigned_in_scope.contains(v));
5148
+ // A `for (let i = 0; …; i++)` counter is declared ONCE in the header but is a
5149
+ // per-iteration `let` in JS: a closure in the body must snapshot THIS iteration's
5150
+ // value, not share one cell across all iterations. The loop's own `i++` would
5151
+ // otherwise pull it in here. (for-of vars are not block vars, and body-`let`s are
5152
+ // re-declared each iteration so they get a fresh cell regardless — only header
5153
+ // counters, declared once, must be excluded.) See loop_let_capture.tish.
5154
+ let mut for_counters = HashSet::new();
4463
5155
  for s in statements {
4464
- Self::collect_mutated_captures_from_statements(s, &block_vars, &mut result);
5156
+ if let Statement::For { init: Some(i), .. } = s {
5157
+ match i.as_ref() {
5158
+ Statement::VarDecl { name, .. } => {
5159
+ for_counters.insert(name.to_string());
5160
+ }
5161
+ Statement::VarDeclDestructure { pattern, .. } => {
5162
+ Self::collect_destruct_names(pattern, &mut for_counters);
5163
+ }
5164
+ _ => {}
5165
+ }
5166
+ }
4465
5167
  }
4466
- result
5168
+ captured.retain(|v| !for_counters.contains(v));
5169
+ captured
4467
5170
  }
4468
5171
 
4469
5172
  /// Collect variable names declared in a statement (VarDecl, Destructure, For init).
@@ -4475,7 +5178,7 @@ impl Codegen {
4475
5178
  Statement::VarDeclDestructure { pattern, .. } => {
4476
5179
  Self::collect_destruct_names(pattern, names);
4477
5180
  }
4478
- Statement::Block { statements, .. } => {
5181
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
4479
5182
  for s in statements {
4480
5183
  Self::collect_local_var_names(s, names);
4481
5184
  }
@@ -4572,7 +5275,7 @@ impl Codegen {
4572
5275
  }
4573
5276
  }
4574
5277
  Statement::VarDeclDestructure { init, .. } => Self::collect_expr_idents(init, idents),
4575
- Statement::Block { statements, .. } => {
5278
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
4576
5279
  for s in statements {
4577
5280
  Self::collect_stmt_idents(s, idents);
4578
5281
  }
@@ -4718,6 +5421,8 @@ impl Codegen {
4718
5421
  "Math",
4719
5422
  "JSON",
4720
5423
  "Date",
5424
+ "Set",
5425
+ "Map",
4721
5426
  "Object",
4722
5427
  "process",
4723
5428
  "setTimeout",
@@ -4743,12 +5448,12 @@ impl Codegen {
4743
5448
  // (cleanups may only read `timer2` but must see updates from nested callbacks).
4744
5449
  let cell_capture_outer_vars: Vec<String> = outer_vars
4745
5450
  .iter()
4746
- .filter(|v| assigned_in_body.contains(*v) || self.rc_cell_storage_contains(*v))
5451
+ .filter(|v| assigned_in_body.contains(*v) || self.rc_cell_storage_contains(v))
4747
5452
  .cloned()
4748
5453
  .collect();
4749
5454
  let read_only_outer_vars: Vec<String> = outer_vars
4750
5455
  .iter()
4751
- .filter(|v| !assigned_in_body.contains(*v) && !self.rc_cell_storage_contains(*v))
5456
+ .filter(|v| !assigned_in_body.contains(*v) && !self.rc_cell_storage_contains(v))
4752
5457
  .cloned()
4753
5458
  .collect();
4754
5459
 
@@ -4790,8 +5495,18 @@ impl Codegen {
4790
5495
  "Math",
4791
5496
  "JSON",
4792
5497
  "Date",
5498
+ "Set",
5499
+ "Map",
4793
5500
  "Object",
5501
+ "Float64Array",
5502
+ "Float32Array",
5503
+ "Int8Array",
4794
5504
  "Uint8Array",
5505
+ "Uint8ClampedArray",
5506
+ "Int16Array",
5507
+ "Uint16Array",
5508
+ "Int32Array",
5509
+ "Uint32Array",
4795
5510
  "AudioContext",
4796
5511
  "process",
4797
5512
  "setTimeout",
@@ -4916,11 +5631,29 @@ impl Codegen {
4916
5631
  for (i, p) in params.iter().enumerate() {
4917
5632
  match p {
4918
5633
  FunParam::Simple(tp) => {
4919
- code.push_str(&format!(
4920
- " let mut {} = args.get({}).cloned().unwrap_or(Value::Null);\n",
4921
- Self::escape_ident(tp.name.as_ref()),
4922
- i
4923
- ));
5634
+ if let Some(default_expr) = &tp.default {
5635
+ // Default applies only for a MISSING positional arg (matches interp + VM);
5636
+ // an explicit `null` keeps the null. emit_expr captured like the destructure
5637
+ // path below so any prelude lands in `code`, not the outer output buffer.
5638
+ let saved = std::mem::take(&mut self.output);
5639
+ let default_str = self.emit_expr(default_expr)?;
5640
+ let prelude = std::mem::replace(&mut self.output, saved);
5641
+ code.push_str(&prelude);
5642
+ code.push_str(&format!(
5643
+ " {} {} = match args.get({}) {{ Some(v) => v.clone(), None => {} }};\n",
5644
+ Self::mut_kw_for(tp.name.as_ref(), "let mut"),
5645
+ Self::escape_ident(tp.name.as_ref()),
5646
+ i,
5647
+ default_str
5648
+ ));
5649
+ } else {
5650
+ code.push_str(&format!(
5651
+ " {} {} = args.get({}).cloned().unwrap_or(Value::Null);\n",
5652
+ Self::mut_kw_for(tp.name.as_ref(), "let mut"),
5653
+ Self::escape_ident(tp.name.as_ref()),
5654
+ i
5655
+ ));
5656
+ }
4924
5657
  }
4925
5658
  FunParam::Destructure { pattern, .. } => {
4926
5659
  let tmp = format!("_formal_{}", i);
@@ -4961,7 +5694,14 @@ impl Codegen {
4961
5694
  let arrow_body_res: Result<(), CompileError> = match body {
4962
5695
  tishlang_ast::ArrowBody::Expr(expr) => {
4963
5696
  let expr_code = self.emit_expr(expr)?;
4964
- code.push_str(&format!(" {}\n", expr_code));
5697
+ // Bind to a temp before the closure returns: if `expr_code` reads a
5698
+ // cell-captured var its `RefCell` borrow guard is a temporary, and a
5699
+ // borrow left in tail position outlives the local cell binding —
5700
+ // which fails to compile (E0597). The `let` releases it at the `;`.
5701
+ code.push_str(&format!(
5702
+ " let __arrow_ret = {};\n __arrow_ret\n",
5703
+ expr_code
5704
+ ));
4965
5705
  Ok(())
4966
5706
  }
4967
5707
  tishlang_ast::ArrowBody::Block(block_stmt) => {
@@ -5016,7 +5756,7 @@ impl Codegen {
5016
5756
  if let Expr::Literal { value, .. } = expr {
5017
5757
  match (target_type, value) {
5018
5758
  (RustType::F64, Literal::Number(n)) => {
5019
- return Ok(format!("{}_f64", n));
5759
+ return Ok(Self::f64_lit(*n));
5020
5760
  }
5021
5761
  (RustType::String, Literal::String(s)) => {
5022
5762
  return Ok(format!("{:?}.to_string()", s.as_ref()));
@@ -5050,6 +5790,28 @@ impl Codegen {
5050
5790
  return Ok(format!("vec![{}]", items.join(", ")));
5051
5791
  }
5052
5792
 
5793
+ // Tuple literal: `[a, b]` against a `[T0, T1]` tuple type -> native Rust tuple `(a, b)`.
5794
+ if let (RustType::Tuple(elem_types), Expr::Array { elements, .. }) = (target_type, expr) {
5795
+ if elements.len() == elem_types.len()
5796
+ && elements.iter().all(|e| matches!(e, ArrayElement::Expr(_)))
5797
+ {
5798
+ let mut items = Vec::new();
5799
+ for (elem, ty) in elements.iter().zip(elem_types) {
5800
+ if let ArrayElement::Expr(e) = elem {
5801
+ items.push(self.emit_native_expr(e, ty)?);
5802
+ }
5803
+ }
5804
+ return Ok(if items.len() == 1 {
5805
+ format!("({},)", items[0])
5806
+ } else {
5807
+ format!("({})", items.join(", "))
5808
+ });
5809
+ }
5810
+ // arity/shape mismatch -> boxed fallback
5811
+ let value_expr = self.emit_expr(expr)?;
5812
+ return Ok(target_type.from_value_expr(&value_expr));
5813
+ }
5814
+
5053
5815
  // Try to emit object literals directly as a Rust struct literal
5054
5816
  // when the target is a `RustType::Named` (a user `type Foo = {...}`
5055
5817
  // alias). Each property in source order is matched to a struct
@@ -5110,6 +5872,17 @@ impl Codegen {
5110
5872
  }
5111
5873
  }
5112
5874
 
5875
+ // Native typed-array HOFs (TISH_NATIVE_HOF): `xs.reduce/map/filter/some/every(<arrow>)`
5876
+ // whose native result type matches this binding's target → emit the iterator chain
5877
+ // directly, with NO box/unbox round-trip (the per-element `value_call` is gone too).
5878
+ if let Expr::Call { callee, args, .. } = expr {
5879
+ if let Some((code, ty)) = self.native_vec_hof_for_call(callee, args)? {
5880
+ if &ty == target_type {
5881
+ return Ok(code);
5882
+ }
5883
+ }
5884
+ }
5885
+
5113
5886
  // Fall back to emit_expr + conversion
5114
5887
  let value_expr = self.emit_expr(expr)?;
5115
5888
  Ok(target_type.from_value_expr(&value_expr))
@@ -5125,95 +5898,863 @@ impl Codegen {
5125
5898
  /// through arithmetic, indexing, and assignments. For any expression this
5126
5899
  /// function cannot handle natively, it falls back to `emit_expr` and returns
5127
5900
  /// `RustType::Value`.
5128
- fn emit_typed_expr(&mut self, expr: &Expr) -> Result<(String, RustType), CompileError> {
5129
- match expr {
5130
- // ── literals ─────────────────────────────────────────────────────────
5131
- Expr::Literal { value, .. } => match value {
5132
- Literal::Number(n) => Ok((format!("{}_f64", n), RustType::F64)),
5133
- Literal::String(s) => {
5134
- Ok((format!("{:?}.to_string()", s.as_ref()), RustType::String))
5901
+ // ───────────────────────── M5: native monomorphic functions ─────────────────────────
5902
+ fn ann_is_number(ann: &TypeAnnotation) -> bool {
5903
+ RustType::from_annotation(ann) == RustType::F64
5904
+ }
5905
+
5906
+ // ── Soundness: demote `number` locals that a reassignment can turn non-numeric ──────────────
5907
+ //
5908
+ // `let s = 0` is inferred `number` → lowered to a native `f64`, and a reassignment stores into
5909
+ // it via `s = match &<rhs> { Value::Number(n) => *n, _ => panic!("expected number") }`. That
5910
+ // coercion PANICS when `<rhs>` is not a number — which `s = s + arr[i]` produces whenever
5911
+ // `arr[i]` is a String (JS `+` is string concat). Node, the interpreter, and the VM all yield
5912
+ // a string there (the VM array-JIT bails to the interpreter on a non-numeric element). The
5913
+ // fix: keep such a local a boxed `Value`, so the boxed `ops::add` — which concatenates —
5914
+ // flows through unchanged.
5915
+ //
5916
+ // A reassignment is SAFE iff its RHS lowers to a native `f64`, which is exactly what
5917
+ // `emit_typed_expr` decides. `expr_native_type` is a read-only mirror of that decision and is
5918
+ // deliberately conservative: any form it does not model → `Value` → demote (sound; at worst an
5919
+ // unnecessary box). A fixpoint propagates demotions through chains (`y = y + s` once `s` is
5920
+ // demoted). The map is name-flat across the whole program (a name demoted in one function is
5921
+ // demoted in all) — still sound, and harmless to the perf gauntlet, where each kernel is its
5922
+ // own program with unique accumulator names.
5923
+ fn collect_demoted_numeric_locals(&self, stmts: &[Statement]) -> HashSet<String> {
5924
+ // 1. Flat env: every annotated local/param name → its native `RustType`.
5925
+ let mut env: HashMap<String, RustType> = HashMap::new();
5926
+ Self::collect_annotated_types(stmts, &self.type_aliases, &mut env);
5927
+ // 2. Every reassignment `(name, rhs)` anywhere in the program (incl. nested exprs/closures).
5928
+ let mut reassigns: Vec<(String, &Expr)> = Vec::new();
5929
+ Self::collect_reassignments_stmts(stmts, &mut reassigns);
5930
+ // 3. Fixpoint: demote a `number` local whose any reassignment RHS isn't native `f64`.
5931
+ let mut demoted: HashSet<String> = HashSet::new();
5932
+ loop {
5933
+ let mut changed = false;
5934
+ for (name, rhs) in &reassigns {
5935
+ if demoted.contains(name) {
5936
+ continue;
5135
5937
  }
5136
- Literal::Bool(b) => Ok((format!("{}", b), RustType::Bool)),
5137
- Literal::Null => Ok(("Value::Null".to_string(), RustType::Value)),
5138
- },
5938
+ if env.get(name) == Some(&RustType::F64)
5939
+ && self.expr_native_type(rhs, &env) != RustType::F64
5940
+ {
5941
+ demoted.insert(name.clone());
5942
+ env.insert(name.clone(), RustType::Value);
5943
+ changed = true;
5944
+ }
5945
+ }
5946
+ if !changed {
5947
+ break;
5948
+ }
5949
+ }
5950
+ demoted
5951
+ }
5139
5952
 
5140
- // ── identifiers ──────────────────────────────────────────────────────
5141
- Expr::Ident { name, .. } => {
5142
- let escaped = Self::escape_ident(name.as_ref());
5143
- if self.refcell_wrapped_vars.contains(name.as_ref()) {
5144
- let var_type = self.type_context.get_type(name.as_ref());
5145
- if var_type.is_native() {
5146
- Ok((format!("(*{}.borrow()).clone()", escaped), var_type))
5147
- } else {
5148
- Ok((format!("(*{}.borrow()).clone()", escaped), RustType::Value))
5953
+ /// Record every annotated `VarDecl`/param name → its native `RustType`, recursing through all
5954
+ /// nested statements (loops, ifs, blocks, switch/try, function bodies). Flat; last write wins.
5955
+ fn collect_annotated_types(
5956
+ stmts: &[Statement],
5957
+ aliases: &HashMap<String, RustType>,
5958
+ env: &mut HashMap<String, RustType>,
5959
+ ) {
5960
+ for s in stmts {
5961
+ match s {
5962
+ Statement::VarDecl {
5963
+ name,
5964
+ type_ann: Some(ann),
5965
+ ..
5966
+ } => {
5967
+ env.insert(
5968
+ name.to_string(),
5969
+ RustType::from_annotation_with_aliases(ann, aliases),
5970
+ );
5971
+ }
5972
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
5973
+ Self::collect_annotated_types(statements, aliases, env)
5974
+ }
5975
+ Statement::If {
5976
+ then_branch,
5977
+ else_branch,
5978
+ ..
5979
+ } => {
5980
+ Self::collect_annotated_types(std::slice::from_ref(then_branch), aliases, env);
5981
+ if let Some(e) = else_branch {
5982
+ Self::collect_annotated_types(std::slice::from_ref(e), aliases, env);
5149
5983
  }
5150
- } else {
5151
- let var_type = self.type_context.get_type(name.as_ref());
5152
- if var_type.is_native() {
5153
- Ok((escaped.into_owned(), var_type))
5154
- } else {
5155
- Ok((escaped.into_owned(), RustType::Value))
5984
+ }
5985
+ Statement::While { body, .. } | Statement::DoWhile { body, .. } => {
5986
+ Self::collect_annotated_types(std::slice::from_ref(body), aliases, env)
5987
+ }
5988
+ Statement::ForOf {
5989
+ name,
5990
+ iterable,
5991
+ body,
5992
+ ..
5993
+ } => {
5994
+ // A loop var iterating a `Vec<elem>` local binds `elem` — so `total += n` (n the
5995
+ // loop var over a `number[]`) is seen as native f64 and `total` is NOT demoted.
5996
+ // Sound: the Vec's elements are genuinely that native type at runtime.
5997
+ if let Expr::Ident { name: it_name, .. } = iterable {
5998
+ let elem_ty = match env.get(it_name.as_ref()) {
5999
+ Some(RustType::Vec(elem)) => Some((**elem).clone()),
6000
+ _ => None,
6001
+ };
6002
+ if let Some(t) = elem_ty {
6003
+ env.insert(name.to_string(), t);
6004
+ }
5156
6005
  }
6006
+ Self::collect_annotated_types(std::slice::from_ref(body), aliases, env)
5157
6007
  }
6008
+ Statement::For { init, body, .. } => {
6009
+ if let Some(i) = init {
6010
+ Self::collect_annotated_types(std::slice::from_ref(i), aliases, env);
6011
+ }
6012
+ Self::collect_annotated_types(std::slice::from_ref(body), aliases, env);
6013
+ }
6014
+ Statement::FunDecl {
6015
+ params,
6016
+ rest_param,
6017
+ body,
6018
+ ..
6019
+ } => {
6020
+ for p in params {
6021
+ if let FunParam::Simple(tp) = p {
6022
+ if let Some(ann) = &tp.type_ann {
6023
+ env.insert(
6024
+ tp.name.to_string(),
6025
+ RustType::from_annotation_with_aliases(ann, aliases),
6026
+ );
6027
+ }
6028
+ }
6029
+ }
6030
+ // Typed rest-param `...args: number[]` -> `Vec<f64>`, so a ForOf loop var over it
6031
+ // binds the element type and accumulators stay native.
6032
+ if let Some(rp) = rest_param {
6033
+ if let Some(ann) = &rp.type_ann {
6034
+ env.insert(
6035
+ rp.name.to_string(),
6036
+ RustType::from_annotation_with_aliases(ann, aliases),
6037
+ );
6038
+ }
6039
+ }
6040
+ Self::collect_annotated_types(std::slice::from_ref(body), aliases, env);
6041
+ }
6042
+ Statement::Switch {
6043
+ cases,
6044
+ default_body,
6045
+ ..
6046
+ } => {
6047
+ for (_, body) in cases {
6048
+ Self::collect_annotated_types(body, aliases, env);
6049
+ }
6050
+ if let Some(b) = default_body {
6051
+ Self::collect_annotated_types(b, aliases, env);
6052
+ }
6053
+ }
6054
+ Statement::Try {
6055
+ body,
6056
+ catch_body,
6057
+ finally_body,
6058
+ ..
6059
+ } => {
6060
+ Self::collect_annotated_types(std::slice::from_ref(body), aliases, env);
6061
+ if let Some(b) = catch_body {
6062
+ Self::collect_annotated_types(std::slice::from_ref(b), aliases, env);
6063
+ }
6064
+ if let Some(b) = finally_body {
6065
+ Self::collect_annotated_types(std::slice::from_ref(b), aliases, env);
6066
+ }
6067
+ }
6068
+ _ => {}
5158
6069
  }
6070
+ }
6071
+ }
5159
6072
 
5160
- // ── binary expressions ───────────────────────────────────────────────
5161
- Expr::Binary {
5162
- left,
5163
- op,
5164
- right,
5165
- span,
6073
+ fn collect_reassignments_stmts<'a>(stmts: &'a [Statement], out: &mut Vec<(String, &'a Expr)>) {
6074
+ for s in stmts {
6075
+ Self::collect_reassignments_stmt(s, out);
6076
+ }
6077
+ }
6078
+
6079
+ /// Collect every `(name, rhs)` reassignment (`=`, compound `+=`, logical `||=`) reachable from
6080
+ /// `s` — descending through nested statements and expressions (including closures).
6081
+ fn collect_reassignments_stmt<'a>(s: &'a Statement, out: &mut Vec<(String, &'a Expr)>) {
6082
+ match s {
6083
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
6084
+ Self::collect_reassignments_stmts(statements, out)
6085
+ }
6086
+ Statement::VarDecl { init: Some(e), .. } => Self::collect_reassignments_expr(e, out),
6087
+ Statement::VarDeclDestructure { init, .. } => {
6088
+ Self::collect_reassignments_expr(init, out)
6089
+ }
6090
+ Statement::ExprStmt { expr, .. } => Self::collect_reassignments_expr(expr, out),
6091
+ Statement::If {
6092
+ cond,
6093
+ then_branch,
6094
+ else_branch,
5166
6095
  ..
5167
6096
  } => {
5168
- let (l, lt) = self.emit_typed_expr(left)?;
5169
- let (r, rt) = self.emit_typed_expr(right)?;
5170
-
5171
- if let Some(result_ty) = RustType::result_type_of_binop(*op, &lt, &rt) {
5172
- // Both sides are compatible native types → emit native op.
5173
- let code = match op {
5174
- BinOp::Add => format!("({} + {})", l, r),
5175
- BinOp::Sub => format!("({} - {})", l, r),
5176
- BinOp::Mul => format!("({} * {})", l, r),
5177
- BinOp::Div => format!("({} / {})", l, r),
5178
- BinOp::Mod => format!("({} % {})", l, r),
5179
- BinOp::Pow => format!("({}).powf({})", l, r),
5180
- BinOp::Lt => format!("({} < {})", l, r),
5181
- BinOp::Le => format!("({} <= {})", l, r),
5182
- BinOp::Gt => format!("({} > {})", l, r),
5183
- BinOp::Ge => format!("({} >= {})", l, r),
5184
- BinOp::StrictEq => format!("({} == {})", l, r),
5185
- BinOp::StrictNe => format!("({} != {})", l, r),
5186
- BinOp::And => format!("({} && {})", l, r),
5187
- BinOp::Or => format!("({} || {})", l, r),
5188
- _ => unreachable!("result_type_of_binop covers all handled ops"),
5189
- };
5190
- return Ok((code, result_ty));
6097
+ Self::collect_reassignments_expr(cond, out);
6098
+ Self::collect_reassignments_stmt(then_branch, out);
6099
+ if let Some(e) = else_branch {
6100
+ Self::collect_reassignments_stmt(e, out);
5191
6101
  }
5192
-
5193
- // Fall back: convert both sides to Value and use the runtime.
5194
- let lv = if lt.is_native() {
5195
- lt.to_value_expr(&l)
5196
- } else {
5197
- l
5198
- };
5199
- let rv = if rt.is_native() {
5200
- rt.to_value_expr(&r)
5201
- } else {
5202
- r
5203
- };
5204
- let result = self.emit_binop(&lv, *op, &rv, *span)?;
5205
- Ok((result, RustType::Value))
5206
6102
  }
5207
-
5208
- // ── array indexing ───────────────────────────────────────────────────
5209
- Expr::Index {
5210
- object,
5211
- index,
5212
- optional,
6103
+ Statement::While { cond, body, .. } => {
6104
+ Self::collect_reassignments_expr(cond, out);
6105
+ Self::collect_reassignments_stmt(body, out);
6106
+ }
6107
+ Statement::DoWhile { body, cond, .. } => {
6108
+ Self::collect_reassignments_stmt(body, out);
6109
+ Self::collect_reassignments_expr(cond, out);
6110
+ }
6111
+ Statement::For {
6112
+ init,
6113
+ cond,
6114
+ update,
6115
+ body,
5213
6116
  ..
5214
6117
  } => {
5215
- // Native fast path: `vec[i]` where vec is Vec<T> and i is numeric.
5216
- if !optional {
6118
+ if let Some(i) = init {
6119
+ Self::collect_reassignments_stmt(i, out);
6120
+ }
6121
+ if let Some(c) = cond {
6122
+ Self::collect_reassignments_expr(c, out);
6123
+ }
6124
+ if let Some(u) = update {
6125
+ Self::collect_reassignments_expr(u, out);
6126
+ }
6127
+ Self::collect_reassignments_stmt(body, out);
6128
+ }
6129
+ Statement::ForOf { iterable, body, .. } => {
6130
+ Self::collect_reassignments_expr(iterable, out);
6131
+ Self::collect_reassignments_stmt(body, out);
6132
+ }
6133
+ Statement::Return { value: Some(e), .. } => Self::collect_reassignments_expr(e, out),
6134
+ Statement::Throw { value, .. } => Self::collect_reassignments_expr(value, out),
6135
+ Statement::FunDecl { body, .. } => Self::collect_reassignments_stmt(body, out),
6136
+ Statement::Switch {
6137
+ expr,
6138
+ cases,
6139
+ default_body,
6140
+ ..
6141
+ } => {
6142
+ Self::collect_reassignments_expr(expr, out);
6143
+ for (g, body) in cases {
6144
+ if let Some(g) = g {
6145
+ Self::collect_reassignments_expr(g, out);
6146
+ }
6147
+ Self::collect_reassignments_stmts(body, out);
6148
+ }
6149
+ if let Some(b) = default_body {
6150
+ Self::collect_reassignments_stmts(b, out);
6151
+ }
6152
+ }
6153
+ Statement::Try {
6154
+ body,
6155
+ catch_body,
6156
+ finally_body,
6157
+ ..
6158
+ } => {
6159
+ Self::collect_reassignments_stmt(body, out);
6160
+ if let Some(b) = catch_body {
6161
+ Self::collect_reassignments_stmt(b, out);
6162
+ }
6163
+ if let Some(b) = finally_body {
6164
+ Self::collect_reassignments_stmt(b, out);
6165
+ }
6166
+ }
6167
+ _ => {}
6168
+ }
6169
+ }
6170
+
6171
+ fn collect_reassignments_expr<'a>(e: &'a Expr, out: &mut Vec<(String, &'a Expr)>) {
6172
+ match e {
6173
+ Expr::Assign { name, value, .. }
6174
+ | Expr::CompoundAssign { name, value, .. }
6175
+ | Expr::LogicalAssign { name, value, .. } => {
6176
+ out.push((name.to_string(), value.as_ref()));
6177
+ Self::collect_reassignments_expr(value, out);
6178
+ }
6179
+ Expr::Binary { left, right, .. } | Expr::NullishCoalesce { left, right, .. } => {
6180
+ Self::collect_reassignments_expr(left, out);
6181
+ Self::collect_reassignments_expr(right, out);
6182
+ }
6183
+ Expr::Unary { operand, .. }
6184
+ | Expr::TypeOf { operand, .. }
6185
+ | Expr::Await { operand, .. } => Self::collect_reassignments_expr(operand, out),
6186
+ Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
6187
+ Self::collect_reassignments_expr(callee, out);
6188
+ for a in args {
6189
+ match a {
6190
+ CallArg::Expr(x) | CallArg::Spread(x) => {
6191
+ Self::collect_reassignments_expr(x, out)
6192
+ }
6193
+ }
6194
+ }
6195
+ }
6196
+ Expr::Member { object, prop, .. } => {
6197
+ Self::collect_reassignments_expr(object, out);
6198
+ if let MemberProp::Expr(p) = prop {
6199
+ Self::collect_reassignments_expr(p, out);
6200
+ }
6201
+ }
6202
+ Expr::Index { object, index, .. } => {
6203
+ Self::collect_reassignments_expr(object, out);
6204
+ Self::collect_reassignments_expr(index, out);
6205
+ }
6206
+ Expr::Conditional {
6207
+ cond,
6208
+ then_branch,
6209
+ else_branch,
6210
+ ..
6211
+ } => {
6212
+ Self::collect_reassignments_expr(cond, out);
6213
+ Self::collect_reassignments_expr(then_branch, out);
6214
+ Self::collect_reassignments_expr(else_branch, out);
6215
+ }
6216
+ Expr::Array { elements, .. } => {
6217
+ for el in elements {
6218
+ match el {
6219
+ ArrayElement::Expr(x) | ArrayElement::Spread(x) => {
6220
+ Self::collect_reassignments_expr(x, out)
6221
+ }
6222
+ }
6223
+ }
6224
+ }
6225
+ Expr::Object { props, .. } => {
6226
+ for p in props {
6227
+ match p {
6228
+ ObjectProp::KeyValue(_, v) => Self::collect_reassignments_expr(v, out),
6229
+ ObjectProp::Spread(x) => Self::collect_reassignments_expr(x, out),
6230
+ }
6231
+ }
6232
+ }
6233
+ Expr::MemberAssign { object, value, .. } => {
6234
+ Self::collect_reassignments_expr(object, out);
6235
+ Self::collect_reassignments_expr(value, out);
6236
+ }
6237
+ Expr::IndexAssign {
6238
+ object,
6239
+ index,
6240
+ value,
6241
+ ..
6242
+ } => {
6243
+ Self::collect_reassignments_expr(object, out);
6244
+ Self::collect_reassignments_expr(index, out);
6245
+ Self::collect_reassignments_expr(value, out);
6246
+ }
6247
+ Expr::TemplateLiteral { exprs, .. } => {
6248
+ for x in exprs {
6249
+ Self::collect_reassignments_expr(x, out);
6250
+ }
6251
+ }
6252
+ Expr::ArrowFunction { body, .. } => match body {
6253
+ ArrowBody::Expr(x) => Self::collect_reassignments_expr(x, out),
6254
+ ArrowBody::Block(b) => Self::collect_reassignments_stmt(b, out),
6255
+ },
6256
+ _ => {}
6257
+ }
6258
+ }
6259
+
6260
+ /// Read-only mirror of `emit_typed_expr`'s native-type decision (no code generated), over a
6261
+ /// flat `name → RustType` env. Returns `RustType::F64` only for forms that provably lower to a
6262
+ /// native `f64`; everything else → `RustType::Value`. Conservative by construction: it never
6263
+ /// claims `F64` where `emit_typed_expr` would box, so a numeric local is never wrongly kept
6264
+ /// native (which would reintroduce the coercion panic).
6265
+ fn expr_native_type(&self, e: &Expr, env: &HashMap<String, RustType>) -> RustType {
6266
+ match e {
6267
+ Expr::Literal { value, .. } => match value {
6268
+ Literal::Number(_) => RustType::F64,
6269
+ Literal::String(_) => RustType::String,
6270
+ Literal::Bool(_) => RustType::Bool,
6271
+ Literal::Null => RustType::Value,
6272
+ },
6273
+ Expr::Ident { name, .. } => env
6274
+ .get(name.as_ref())
6275
+ .filter(|t| t.is_native())
6276
+ .cloned()
6277
+ .unwrap_or(RustType::Value),
6278
+ Expr::Binary {
6279
+ left, op, right, ..
6280
+ } => {
6281
+ let lt = self.expr_native_type(left, env);
6282
+ let rt = self.expr_native_type(right, env);
6283
+ RustType::result_type_of_binop(*op, &lt, &rt).unwrap_or(RustType::Value)
6284
+ }
6285
+ // `vec[i]` where `vec` is a `number[]` (Vec<f64>) → the element type. A `Vec<f64>`
6286
+ // can only hold numbers, so this never feeds a string into the accumulator.
6287
+ Expr::Index {
6288
+ object,
6289
+ optional: false,
6290
+ ..
6291
+ } => {
6292
+ if let Expr::Ident { name, .. } = object.as_ref() {
6293
+ if let Some(RustType::Vec(inner)) = env.get(name.as_ref()) {
6294
+ return (**inner).clone();
6295
+ }
6296
+ }
6297
+ RustType::Value
6298
+ }
6299
+ // `o.field` where `o` is a native struct local and `field` is a native field.
6300
+ Expr::Member {
6301
+ object,
6302
+ prop: MemberProp::Name { name: prop_name, .. },
6303
+ optional: false,
6304
+ ..
6305
+ } => {
6306
+ if let Expr::Ident { name: var_name, .. } = object.as_ref() {
6307
+ if let Some(RustType::Named { fields, .. }) = env.get(var_name.as_ref()) {
6308
+ if let Some((_, field_ty)) =
6309
+ fields.iter().find(|(k, _)| k.as_ref() == prop_name.as_ref())
6310
+ {
6311
+ if field_ty.is_native() {
6312
+ return field_ty.clone();
6313
+ }
6314
+ }
6315
+ }
6316
+ }
6317
+ RustType::Value
6318
+ }
6319
+ Expr::Call { callee, args, .. } => {
6320
+ // M5 native fn (`fn f_native(..) -> f64`); requires all-positional args.
6321
+ if let Expr::Ident { name: fname, .. } = callee.as_ref() {
6322
+ if self.native_fns.contains(fname.as_ref())
6323
+ && args.iter().all(|a| matches!(a, CallArg::Expr(_)))
6324
+ {
6325
+ return RustType::F64;
6326
+ }
6327
+ }
6328
+ // Single-arg `Math.<intrinsic>(x)` lowered to a direct `f64` method → number.
6329
+ if let [CallArg::Expr(_)] = args.as_slice() {
6330
+ if let Expr::Member {
6331
+ object,
6332
+ prop: MemberProp::Name { name: method, .. },
6333
+ ..
6334
+ } = callee.as_ref()
6335
+ {
6336
+ if matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
6337
+ && matches!(
6338
+ method.as_ref(),
6339
+ "sqrt" | "sin" | "cos" | "tan" | "abs" | "floor" | "ceil" | "exp"
6340
+ | "trunc" | "log"
6341
+ )
6342
+ {
6343
+ return RustType::F64;
6344
+ }
6345
+ }
6346
+ }
6347
+ RustType::Value
6348
+ }
6349
+ // Unary, Conditional, etc. are not modelled by `emit_typed_expr` (it boxes them), so a
6350
+ // store from one already coerces; treat as `Value` to match (→ demote if it feeds an
6351
+ // accumulator). Sound and consistent.
6352
+ _ => RustType::Value,
6353
+ }
6354
+ }
6355
+
6356
+ /// Names of top-level fns eligible for a parallel native `fn f_native(f64,..)->f64`:
6357
+ /// non-async, every param `: number` (no default), `: number` return, and a native-safe
6358
+ /// body (only block/if/return/expr-stmt over native exprs + calls to other eligible fns or
6359
+ /// 1-arg Math intrinsics). Conservative fixpoint — bails on anything else.
6360
+ fn collect_native_fns(statements: &[Statement]) -> std::collections::HashSet<String> {
6361
+ use std::collections::HashSet;
6362
+ let mut cand: HashSet<String> = HashSet::new();
6363
+ let mut decls: Vec<(&str, &Vec<FunParam>, &Statement)> = Vec::new();
6364
+ for s in statements {
6365
+ if let Statement::FunDecl {
6366
+ async_: false,
6367
+ name,
6368
+ params,
6369
+ rest_param: None,
6370
+ return_type,
6371
+ body,
6372
+ ..
6373
+ } = s
6374
+ {
6375
+ let params_ok = params.iter().all(|p| {
6376
+ matches!(p, FunParam::Simple(tp)
6377
+ if tp.default.is_none()
6378
+ && tp.type_ann.as_ref().map(Self::ann_is_number).unwrap_or(false))
6379
+ });
6380
+ // Return: an annotated `: number`, OR unannotated with all-numeric returns
6381
+ // (verified in the fixpoint via `returns_numeric`), so the native `-> f64` holds.
6382
+ let ret_ok = match return_type {
6383
+ Some(rt) => Self::ann_is_number(rt),
6384
+ None => true,
6385
+ };
6386
+ if ret_ok && params_ok && !params.is_empty() {
6387
+ cand.insert(name.to_string());
6388
+ decls.push((name.as_ref(), params, body));
6389
+ }
6390
+ }
6391
+ }
6392
+ loop {
6393
+ let mut remove: Vec<String> = Vec::new();
6394
+ for &(name, params, body) in &decls {
6395
+ if !cand.contains(name) {
6396
+ continue;
6397
+ }
6398
+ let pnames: HashSet<String> =
6399
+ params.iter().flat_map(|p| p.bound_names()).map(|n| n.to_string()).collect();
6400
+ if !Self::native_safe_stmt(body, &pnames, &cand)
6401
+ || !Self::returns_numeric(body, &pnames, &cand)
6402
+ {
6403
+ remove.push(name.to_string());
6404
+ }
6405
+ }
6406
+ if remove.is_empty() {
6407
+ break;
6408
+ }
6409
+ for n in remove {
6410
+ cand.remove(&n);
6411
+ }
6412
+ }
6413
+ cand
6414
+ }
6415
+
6416
+ fn native_safe_stmt(
6417
+ stmt: &Statement,
6418
+ params: &std::collections::HashSet<String>,
6419
+ cand: &std::collections::HashSet<String>,
6420
+ ) -> bool {
6421
+ match stmt {
6422
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
6423
+ statements.iter().all(|s| Self::native_safe_stmt(s, params, cand))
6424
+ }
6425
+ Statement::Return { value, .. } => {
6426
+ value.as_ref().is_some_and(|e| Self::native_safe_expr(e, params, cand))
6427
+ }
6428
+ Statement::If { cond, then_branch, else_branch, .. } => {
6429
+ Self::native_safe_expr(cond, params, cand)
6430
+ && Self::native_safe_stmt(then_branch, params, cand)
6431
+ && else_branch.as_ref().is_none_or(|e| Self::native_safe_stmt(e, params, cand))
6432
+ }
6433
+ Statement::ExprStmt { expr, .. } => Self::native_safe_expr(expr, params, cand),
6434
+ _ => false,
6435
+ }
6436
+ }
6437
+
6438
+ fn native_safe_expr(
6439
+ expr: &Expr,
6440
+ params: &std::collections::HashSet<String>,
6441
+ cand: &std::collections::HashSet<String>,
6442
+ ) -> bool {
6443
+ match expr {
6444
+ Expr::Literal { value, .. } => matches!(value, Literal::Number(_) | Literal::Bool(_)),
6445
+ Expr::Ident { name, .. } => params.contains(name.as_ref()),
6446
+ Expr::Binary { left, op, right, .. } => {
6447
+ matches!(
6448
+ op,
6449
+ BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow
6450
+ | BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge
6451
+ | BinOp::StrictEq | BinOp::StrictNe | BinOp::And | BinOp::Or
6452
+ ) && Self::native_safe_expr(left, params, cand)
6453
+ && Self::native_safe_expr(right, params, cand)
6454
+ }
6455
+ Expr::Unary { op, operand, .. } => {
6456
+ matches!(op, UnaryOp::Neg | UnaryOp::Pos | UnaryOp::Not)
6457
+ && Self::native_safe_expr(operand, params, cand)
6458
+ }
6459
+ Expr::Call { callee, args, .. } => {
6460
+ let args_ok = args
6461
+ .iter()
6462
+ .all(|a| matches!(a, CallArg::Expr(e) if Self::native_safe_expr(e, params, cand)));
6463
+ if !args_ok {
6464
+ return false;
6465
+ }
6466
+ match callee.as_ref() {
6467
+ Expr::Ident { name, .. } => cand.contains(name.as_ref()),
6468
+ Expr::Member { object, prop: MemberProp::Name { name: m, .. }, .. } => {
6469
+ matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
6470
+ && args.len() == 1
6471
+ && matches!(
6472
+ m.as_ref(),
6473
+ "sqrt" | "sin" | "cos" | "tan" | "abs" | "floor" | "ceil" | "exp"
6474
+ | "trunc" | "log"
6475
+ )
6476
+ }
6477
+ _ => false,
6478
+ }
6479
+ }
6480
+ _ => false,
6481
+ }
6482
+ }
6483
+
6484
+ /// Every `return` in `s` yields a numeric-shaped value, so a native `-> f64` body is sound.
6485
+ /// Lets an unannotated-but-numeric-returning fn (e.g. `function fib(n) {...}` after M4 typed
6486
+ /// the param) become M5-eligible.
6487
+ fn returns_numeric(
6488
+ s: &Statement,
6489
+ params: &std::collections::HashSet<String>,
6490
+ cand: &std::collections::HashSet<String>,
6491
+ ) -> bool {
6492
+ match s {
6493
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
6494
+ statements.iter().all(|x| Self::returns_numeric(x, params, cand))
6495
+ }
6496
+ Statement::Return { value, .. } => {
6497
+ value.as_ref().is_some_and(|e| Self::numeric_shaped(e, params, cand))
6498
+ }
6499
+ Statement::If { then_branch, else_branch, .. } => {
6500
+ Self::returns_numeric(then_branch, params, cand)
6501
+ && else_branch.as_ref().is_none_or(|e| Self::returns_numeric(e, params, cand))
6502
+ }
6503
+ Statement::While { body, .. } | Statement::For { body, .. } => {
6504
+ Self::returns_numeric(body, params, cand)
6505
+ }
6506
+ _ => true, // no return in this statement form
6507
+ }
6508
+ }
6509
+
6510
+ /// `e` evaluates to a number: built from numeric params, number literals, ARITHMETIC binops
6511
+ /// (comparisons/logical yield bool → excluded), numeric unary, conditionals, and calls to
6512
+ /// eligible native fns / 1-arg Math.
6513
+ fn numeric_shaped(
6514
+ e: &Expr,
6515
+ params: &std::collections::HashSet<String>,
6516
+ cand: &std::collections::HashSet<String>,
6517
+ ) -> bool {
6518
+ match e {
6519
+ Expr::Literal { value: Literal::Number(_), .. } => true,
6520
+ Expr::Ident { name, .. } => params.contains(name.as_ref()),
6521
+ Expr::Binary { left, op, right, .. } => {
6522
+ matches!(
6523
+ op,
6524
+ BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow
6525
+ ) && Self::numeric_shaped(left, params, cand)
6526
+ && Self::numeric_shaped(right, params, cand)
6527
+ }
6528
+ Expr::Unary { op, operand, .. } => {
6529
+ matches!(op, UnaryOp::Neg | UnaryOp::Pos)
6530
+ && Self::numeric_shaped(operand, params, cand)
6531
+ }
6532
+ Expr::Conditional { then_branch, else_branch, .. } => {
6533
+ Self::numeric_shaped(then_branch, params, cand)
6534
+ && Self::numeric_shaped(else_branch, params, cand)
6535
+ }
6536
+ Expr::Call { callee, .. } => match callee.as_ref() {
6537
+ Expr::Ident { name, .. } => cand.contains(name.as_ref()),
6538
+ Expr::Member { object, prop: MemberProp::Name { name: m, .. }, .. } => {
6539
+ matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
6540
+ && matches!(
6541
+ m.as_ref(),
6542
+ "sqrt" | "sin" | "cos" | "tan" | "abs" | "floor" | "ceil" | "exp"
6543
+ | "trunc" | "log"
6544
+ )
6545
+ }
6546
+ _ => false,
6547
+ },
6548
+ _ => false,
6549
+ }
6550
+ }
6551
+
6552
+ /// Emit `fn name_native(p: f64, ...) -> f64 { ... }` (top level) for each eligible fn.
6553
+ fn emit_native_fns(&mut self, statements: &[Statement]) -> Result<(), CompileError> {
6554
+ for s in statements {
6555
+ if let Statement::FunDecl { name, params, body, .. } = s {
6556
+ if !self.native_fns.contains(name.as_ref()) {
6557
+ continue;
6558
+ }
6559
+ let plist: Vec<String> = params
6560
+ .iter()
6561
+ .filter_map(|p| match p {
6562
+ FunParam::Simple(tp) => {
6563
+ Some(format!("mut {}: f64", Self::escape_ident(tp.name.as_ref())))
6564
+ }
6565
+ _ => None,
6566
+ })
6567
+ .collect();
6568
+ self.type_context.push_scope();
6569
+ for p in params {
6570
+ if let FunParam::Simple(tp) = p {
6571
+ self.type_context.define(tp.name.as_ref(), RustType::F64);
6572
+ }
6573
+ }
6574
+ self.writeln(&format!("fn {}_native({}) -> f64 {{", Self::escape_ident(name.as_ref()), plist.join(", ")));
6575
+ self.indent += 1;
6576
+ self.emit_native_fn_body(body)?;
6577
+ // Functions that fall off the end without returning: JS yields undefined; an
6578
+ // eligible numeric fn shouldn't, but emit a default to keep `-> f64` total.
6579
+ self.writeln("0.0");
6580
+ self.indent -= 1;
6581
+ self.writeln("}");
6582
+ self.type_context.pop_scope();
6583
+ }
6584
+ }
6585
+ Ok(())
6586
+ }
6587
+
6588
+ fn emit_native_fn_body(&mut self, stmt: &Statement) -> Result<(), CompileError> {
6589
+ match stmt {
6590
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
6591
+ for s in statements {
6592
+ self.emit_native_fn_body(s)?;
6593
+ }
6594
+ }
6595
+ Statement::Return { value, .. } => {
6596
+ let e = value.as_ref().expect("eligible return has a value");
6597
+ let (code, ty) = self.emit_typed_expr(e)?;
6598
+ let f = if ty == RustType::F64 {
6599
+ code
6600
+ } else if ty == RustType::Value {
6601
+ RustType::F64.from_value_expr(&code)
6602
+ } else {
6603
+ code
6604
+ };
6605
+ self.writeln(&format!("return {};", f));
6606
+ }
6607
+ Statement::If { cond, then_branch, else_branch, .. } => {
6608
+ let (c, ct) = self.emit_typed_expr(cond)?;
6609
+ let c_bool = match ct {
6610
+ RustType::Bool => c,
6611
+ RustType::F64 => format!("({} != 0.0)", c),
6612
+ _ => format!("{}.is_truthy()", c),
6613
+ };
6614
+ self.writeln(&format!("if {} {{", c_bool));
6615
+ self.indent += 1;
6616
+ self.emit_native_fn_body(then_branch)?;
6617
+ self.indent -= 1;
6618
+ if let Some(eb) = else_branch {
6619
+ self.writeln("} else {");
6620
+ self.indent += 1;
6621
+ self.emit_native_fn_body(eb)?;
6622
+ self.indent -= 1;
6623
+ }
6624
+ self.writeln("}");
6625
+ }
6626
+ Statement::ExprStmt { expr, .. } => {
6627
+ let (code, _) = self.emit_typed_expr(expr)?;
6628
+ self.writeln(&format!("{};", code));
6629
+ }
6630
+ _ => unreachable!("emit_native_fn_body: eligibility guarantees only handled statements"),
6631
+ }
6632
+ Ok(())
6633
+ }
6634
+
6635
+ fn emit_typed_expr(&mut self, expr: &Expr) -> Result<(String, RustType), CompileError> {
6636
+ match expr {
6637
+ // ── literals ─────────────────────────────────────────────────────────
6638
+ Expr::Literal { value, .. } => match value {
6639
+ Literal::Number(n) => Ok((Self::f64_lit(*n), RustType::F64)),
6640
+ Literal::String(s) => {
6641
+ Ok((format!("{:?}.to_string()", s.as_ref()), RustType::String))
6642
+ }
6643
+ Literal::Bool(b) => Ok((format!("{}", b), RustType::Bool)),
6644
+ Literal::Null => Ok(("Value::Null".to_string(), RustType::Value)),
6645
+ },
6646
+
6647
+ // ── identifiers ──────────────────────────────────────────────────────
6648
+ Expr::Ident { name, .. } => {
6649
+ let escaped = Self::escape_ident(name.as_ref());
6650
+ if self.refcell_wrapped_vars.contains(name.as_ref()) {
6651
+ let var_type = self.type_context.get_type(name.as_ref());
6652
+ if var_type.is_native() {
6653
+ Ok((format!("(*{}.borrow()).clone()", escaped), var_type))
6654
+ } else {
6655
+ Ok((format!("(*{}.borrow()).clone()", escaped), RustType::Value))
6656
+ }
6657
+ } else {
6658
+ let var_type = self.type_context.get_type(name.as_ref());
6659
+ if var_type.is_native() {
6660
+ Ok((escaped.into_owned(), var_type))
6661
+ } else {
6662
+ Ok((escaped.into_owned(), RustType::Value))
6663
+ }
6664
+ }
6665
+ }
6666
+
6667
+ // ── binary expressions ───────────────────────────────────────────────
6668
+ Expr::Binary {
6669
+ left,
6670
+ op,
6671
+ right,
6672
+ span,
6673
+ ..
6674
+ } => {
6675
+ let (l, lt) = self.emit_typed_expr(left)?;
6676
+ let (r, rt) = self.emit_typed_expr(right)?;
6677
+
6678
+ if let Some(result_ty) = RustType::result_type_of_binop(*op, &lt, &rt) {
6679
+ // Both sides are compatible native types → emit native op.
6680
+ let code = match op {
6681
+ BinOp::Add if result_ty == RustType::String => {
6682
+ // M2: Rust `String + String` is illegal; build a fresh String.
6683
+ // `format!` borrows both operands, so chained concats (`a + b + c`)
6684
+ // nest cleanly with no move/clone hazards.
6685
+ format!("format!(\"{{}}{{}}\", {}, {})", l, r)
6686
+ }
6687
+ BinOp::Add => format!("({} + {})", l, r),
6688
+ BinOp::Sub => format!("({} - {})", l, r),
6689
+ BinOp::Mul => format!("({} * {})", l, r),
6690
+ BinOp::Div => format!("({} / {})", l, r),
6691
+ BinOp::Mod => format!("({} % {})", l, r),
6692
+ BinOp::Pow => format!("({}).powf({})", l, r),
6693
+ BinOp::Lt => format!("({} < {})", l, r),
6694
+ BinOp::Le => format!("({} <= {})", l, r),
6695
+ BinOp::Gt => format!("({} > {})", l, r),
6696
+ BinOp::Ge => format!("({} >= {})", l, r),
6697
+ BinOp::StrictEq => format!("({} == {})", l, r),
6698
+ BinOp::StrictNe => format!("({} != {})", l, r),
6699
+ BinOp::And => format!("({} && {})", l, r),
6700
+ BinOp::Or => format!("({} || {})", l, r),
6701
+ // Native int32 bitwise/shift (operands are f64 here). `to_int32`/`to_uint32`
6702
+ // is JS ToInt32/ToUint32 (modulo 2³², NaN/±Infinity → 0; `#[inline]` so the
6703
+ // `is_finite` guard folds away on the hot finite path); shift counts mask to
6704
+ // 5 bits via `wrapping_sh*` (JS semantics, no panic).
6705
+ BinOp::BitAnd => format!(
6706
+ "((tishlang_runtime::to_int32({}) & tishlang_runtime::to_int32({})) as f64)",
6707
+ l, r
6708
+ ),
6709
+ BinOp::BitOr => format!(
6710
+ "((tishlang_runtime::to_int32({}) | tishlang_runtime::to_int32({})) as f64)",
6711
+ l, r
6712
+ ),
6713
+ BinOp::BitXor => format!(
6714
+ "((tishlang_runtime::to_int32({}) ^ tishlang_runtime::to_int32({})) as f64)",
6715
+ l, r
6716
+ ),
6717
+ BinOp::Shl => format!(
6718
+ "(tishlang_runtime::to_int32({}).wrapping_shl(tishlang_runtime::to_uint32({})) as f64)",
6719
+ l, r
6720
+ ),
6721
+ BinOp::Shr => format!(
6722
+ "(tishlang_runtime::to_int32({}).wrapping_shr(tishlang_runtime::to_uint32({})) as f64)",
6723
+ l, r
6724
+ ),
6725
+ BinOp::UShr => format!(
6726
+ "(tishlang_runtime::to_uint32({}).wrapping_shr(tishlang_runtime::to_uint32({})) as f64)",
6727
+ l, r
6728
+ ),
6729
+ _ => unreachable!("result_type_of_binop covers all handled ops"),
6730
+ };
6731
+ return Ok((code, result_ty));
6732
+ }
6733
+
6734
+ // Fall back: convert both sides to Value and use the runtime.
6735
+ let lv = if lt.is_native() {
6736
+ lt.to_value_expr(&l)
6737
+ } else {
6738
+ l
6739
+ };
6740
+ let rv = if rt.is_native() {
6741
+ rt.to_value_expr(&r)
6742
+ } else {
6743
+ r
6744
+ };
6745
+ let result = self.emit_binop(&lv, *op, &rv, *span)?;
6746
+ Ok((result, RustType::Value))
6747
+ }
6748
+
6749
+ // ── array indexing ───────────────────────────────────────────────────
6750
+ Expr::Index {
6751
+ object,
6752
+ index,
6753
+ optional,
6754
+ ..
6755
+ } => {
6756
+ // Native fast path: `vec[i]` where vec is Vec<T> and i is numeric.
6757
+ if !optional {
5217
6758
  if let Expr::Ident { name, .. } = object.as_ref() {
5218
6759
  if !self.refcell_wrapped_vars.contains(name.as_ref()) {
5219
6760
  let obj_type = self.type_context.get_type(name.as_ref());
@@ -5234,7 +6775,37 @@ impl Codegen {
5234
6775
  )
5235
6776
  };
5236
6777
  let elem_ty = *elem_type.clone();
5237
- return Ok((format!("{}[{}]", esc_obj, idx_usize), elem_ty));
6778
+ // OOB-safe read for numeric/bool Vecs: JS `arr[oob]` is `undefined`
6779
+ // (→ NaN / false in those contexts), NOT a panic. In-bounds is the
6780
+ // same bounds-checked access, so this is purely a correctness gain
6781
+ // (and what lets index reads be *inferred* as native — phase 2).
6782
+ let access = match &elem_ty {
6783
+ RustType::F64 => format!(
6784
+ "{}.get({}).copied().unwrap_or(f64::NAN)",
6785
+ esc_obj, idx_usize
6786
+ ),
6787
+ RustType::Bool => {
6788
+ format!("{}.get({}).copied().unwrap_or(false)", esc_obj, idx_usize)
6789
+ }
6790
+ // Other element types keep the direct index (unchanged).
6791
+ _ => format!("{}[{}]", esc_obj, idx_usize),
6792
+ };
6793
+ return Ok((access, elem_ty));
6794
+ }
6795
+ // Native tuple access: `tuple[const]` -> `tuple.const` (Rust tuples
6796
+ // require a literal index; a variable index falls through to boxed).
6797
+ if let RustType::Tuple(elems) = &obj_type {
6798
+ if let Expr::Literal {
6799
+ value: Literal::Number(n),
6800
+ ..
6801
+ } = index.as_ref()
6802
+ {
6803
+ let i = *n as usize;
6804
+ if n.fract() == 0.0 && i < elems.len() {
6805
+ let esc_obj = Self::escape_ident(name.as_ref()).into_owned();
6806
+ return Ok((format!("{}.{}", esc_obj, i), elems[i].clone()));
6807
+ }
6808
+ }
5238
6809
  }
5239
6810
  }
5240
6811
  }
@@ -5255,6 +6826,132 @@ impl Codegen {
5255
6826
  Ok((result, RustType::Value))
5256
6827
  }
5257
6828
 
6829
+ // ── native Math intrinsics ───────────────────────────────────────────
6830
+ // `Math.sqrt(x)` etc. with a native-f64 arg lowers to a direct f64 method,
6831
+ // skipping the boxed value_call per element. Only methods whose Rust f64 op
6832
+ // matches JS semantics (round half-up & sign(0) differ → left to the runtime).
6833
+ Expr::Call { callee, args, .. } => {
6834
+ // M5: direct call to an eligible native fn -> `name_native(<native args>)`.
6835
+ if let Expr::Ident { name: fname, .. } = callee.as_ref() {
6836
+ if self.native_fns.contains(fname.as_ref()) {
6837
+ let mut argc: Vec<String> = Vec::with_capacity(args.len());
6838
+ let mut ok = true;
6839
+ for a in args {
6840
+ if let CallArg::Expr(e) = a {
6841
+ let (ac, at) = self.emit_typed_expr(e)?;
6842
+ argc.push(if at == RustType::Value {
6843
+ RustType::F64.from_value_expr(&ac)
6844
+ } else {
6845
+ ac
6846
+ });
6847
+ } else {
6848
+ ok = false;
6849
+ break;
6850
+ }
6851
+ }
6852
+ if ok {
6853
+ return Ok((
6854
+ format!(
6855
+ "{}_native({})",
6856
+ Self::escape_ident(fname.as_ref()),
6857
+ argc.join(", ")
6858
+ ),
6859
+ RustType::F64,
6860
+ ));
6861
+ }
6862
+ }
6863
+ }
6864
+ if let [CallArg::Expr(arg_expr)] = args.as_slice() {
6865
+ if let Expr::Member {
6866
+ object,
6867
+ prop: MemberProp::Name { name: method, .. },
6868
+ ..
6869
+ } = callee.as_ref()
6870
+ {
6871
+ if matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
6872
+ {
6873
+ let rust_m = match method.as_ref() {
6874
+ "sqrt" => Some("sqrt"),
6875
+ "sin" => Some("sin"),
6876
+ "cos" => Some("cos"),
6877
+ "tan" => Some("tan"),
6878
+ "abs" => Some("abs"),
6879
+ "floor" => Some("floor"),
6880
+ "ceil" => Some("ceil"),
6881
+ "exp" => Some("exp"),
6882
+ "trunc" => Some("trunc"),
6883
+ "log" => Some("ln"),
6884
+ "sinh" => Some("sinh"),
6885
+ "cosh" => Some("cosh"),
6886
+ "tanh" => Some("tanh"),
6887
+ "asinh" => Some("asinh"),
6888
+ "acosh" => Some("acosh"),
6889
+ "atanh" => Some("atanh"),
6890
+ "cbrt" => Some("cbrt"),
6891
+ "log2" => Some("log2"),
6892
+ "log10" => Some("log10"),
6893
+ _ => None,
6894
+ };
6895
+ if let Some(m) = rust_m {
6896
+ let (arg_code, arg_ty) = self.emit_typed_expr(arg_expr)?;
6897
+ let arg_f64 = if arg_ty == RustType::F64 {
6898
+ arg_code
6899
+ } else if arg_ty == RustType::Value {
6900
+ RustType::F64.from_value_expr(&arg_code)
6901
+ } else {
6902
+ arg_code
6903
+ };
6904
+ return Ok((format!("({}).{}()", arg_f64, m), RustType::F64));
6905
+ }
6906
+ }
6907
+ }
6908
+ }
6909
+ // Native typed-array HOFs over a `Vec<f64>` receiver (TISH_NATIVE_HOF):
6910
+ // `xs.reduce/map/filter/some/every(<arrow>)` → a direct Rust iterator chain,
6911
+ // eliminating the per-element `value_call` and all `Value` boxing.
6912
+ if let Some(res) = self.native_vec_hof_for_call(callee, args)? {
6913
+ return Ok(res);
6914
+ }
6915
+ let result = self.emit_expr(expr)?;
6916
+ Ok((result, RustType::Value))
6917
+ }
6918
+
6919
+ // ── native struct field access ───────────────────────────────────────
6920
+ // `o.x` where `o` is a `RustType::Named` struct local and `x` is a native
6921
+ // (f64/bool/string) field → a direct Rust field read with that native type,
6922
+ // instead of boxing it through `Value::Number(o.x)`. This keeps `sum + o.x + o.y`
6923
+ // entirely native (the object_sum hot loop) — see emit_expr's struct fast path,
6924
+ // which returns the SAME access but wrapped in `Value::*` for the dynamic callers.
6925
+ Expr::Member {
6926
+ object,
6927
+ prop: MemberProp::Name { name: prop_name, .. },
6928
+ optional: false,
6929
+ ..
6930
+ } => {
6931
+ if let Expr::Ident { name: var_name, .. } = object.as_ref() {
6932
+ let var_type = self.type_context.get_type(var_name.as_ref());
6933
+ if let RustType::Named { fields, .. } = &var_type {
6934
+ if let Some((_, field_ty)) =
6935
+ fields.iter().find(|(k, _)| k.as_ref() == prop_name.as_ref())
6936
+ {
6937
+ if field_ty.is_native() {
6938
+ let var_esc = Self::escape_ident(var_name.as_ref()).into_owned();
6939
+ let field = crate::types::field_ident(prop_name.as_ref());
6940
+ let access = if self.refcell_wrapped_vars.contains(var_name.as_ref())
6941
+ {
6942
+ format!("(*{}.borrow()).{}.clone()", var_esc, field)
6943
+ } else {
6944
+ format!("{}.{}", var_esc, field)
6945
+ };
6946
+ return Ok((access, field_ty.clone()));
6947
+ }
6948
+ }
6949
+ }
6950
+ }
6951
+ let result = self.emit_expr(expr)?;
6952
+ Ok((result, RustType::Value))
6953
+ }
6954
+
5258
6955
  // ── everything else: delegate to emit_expr ───────────────────────────
5259
6956
  _ => {
5260
6957
  let result = self.emit_expr(expr)?;
@@ -5280,6 +6977,293 @@ impl Codegen {
5280
6977
  }
5281
6978
  }
5282
6979
 
6980
+ /// Fused `reduce`: if the callback is exactly `(acc, x) => acc OP x` (or `x OP acc`) with a
6981
+ /// plain binop of the two params, emit a native fold over the array using the SAME runtime
6982
+ /// Value op the closure body would — eliminating the per-element `value_call`. Sound (identical
6983
+ /// Value semantics, including string `+`). Returns `None` to fall back to `array_reduce`.
6984
+ fn try_fused_reduce(
6985
+ &self,
6986
+ args: &[CallArg],
6987
+ obj_expr: &str,
6988
+ initial: &str,
6989
+ ) -> Result<Option<String>, CompileError> {
6990
+ let Some(CallArg::Expr(Expr::ArrowFunction { params, body, .. })) = args.first() else {
6991
+ return Ok(None);
6992
+ };
6993
+ let tishlang_ast::ArrowBody::Expr(be) = body else {
6994
+ return Ok(None);
6995
+ };
6996
+ if params.len() != 2 {
6997
+ return Ok(None);
6998
+ }
6999
+ let pname = |p: &FunParam| -> Option<std::sync::Arc<str>> {
7000
+ match p {
7001
+ FunParam::Simple(tp) if tp.default.is_none() => Some(std::sync::Arc::clone(&tp.name)),
7002
+ _ => None,
7003
+ }
7004
+ };
7005
+ let (Some(acc), Some(cur)) = (pname(&params[0]), pname(&params[1])) else {
7006
+ return Ok(None);
7007
+ };
7008
+ let Expr::Binary {
7009
+ left, op, right, span,
7010
+ } = be.as_ref()
7011
+ else {
7012
+ return Ok(None);
7013
+ };
7014
+ let ident = |e: &Expr| -> Option<std::sync::Arc<str>> {
7015
+ match e {
7016
+ Expr::Ident { name, .. } => Some(std::sync::Arc::clone(name)),
7017
+ _ => None,
7018
+ }
7019
+ };
7020
+ let (Some(ln), Some(rn)) = (ident(left), ident(right)) else {
7021
+ return Ok(None);
7022
+ };
7023
+ // Map each operand to `_acc` / `_x` in the body's actual order.
7024
+ let (ls, rs) = if ln.as_ref() == acc.as_ref() && rn.as_ref() == cur.as_ref() {
7025
+ ("_acc", "_x")
7026
+ } else if ln.as_ref() == cur.as_ref() && rn.as_ref() == acc.as_ref() {
7027
+ ("_x", "_acc")
7028
+ } else {
7029
+ return Ok(None);
7030
+ };
7031
+ let body_code = self.emit_binop(ls, *op, rs, *span)?;
7032
+
7033
+ // Native-f64 fast path for arithmetic reducers in the standard `acc OP x` order. We can't
7034
+ // assume the array is numeric at compile time (`+` concatenates strings in JS), so emit a
7035
+ // runtime all-numeric guard: if the init and every element are `Value::Number`, fold in raw
7036
+ // f64 (no per-element `ops::add` call, no Result, no re-boxing); otherwise fall back to the
7037
+ // boxed fold from the original init — identical semantics either way. This is the array_hof
7038
+ // hot loop; a fully-unboxed `Vec<f64>` (packed arrays / task #13) would go further.
7039
+ let nat_op = if (ls, rs) == ("_acc", "_x") {
7040
+ match op {
7041
+ BinOp::Add => Some("+="),
7042
+ BinOp::Sub => Some("-="),
7043
+ BinOp::Mul => Some("*="),
7044
+ BinOp::Div => Some("/="),
7045
+ _ => None,
7046
+ }
7047
+ } else {
7048
+ None
7049
+ };
7050
+ if let Some(nat_op) = nat_op {
7051
+ return Ok(Some(format!(
7052
+ "{{ let _init0 = {init}; let _arr = ({obj}).clone(); \
7053
+ if let Value::Array(ref _a) = _arr {{ let _b = _a.borrow(); \
7054
+ let mut _accn: f64 = 0.0; let mut _ok = false; \
7055
+ if let Value::Number(_i0) = &_init0 {{ _accn = *_i0; _ok = true; }} \
7056
+ if _ok {{ for _el in _b.iter() {{ \
7057
+ if let Value::Number(_n) = _el {{ _accn {nat_op} *_n; }} else {{ _ok = false; break; }} }} }} \
7058
+ if _ok {{ Value::Number(_accn) }} \
7059
+ else {{ let mut _acc = _init0; for _el in _b.iter() {{ let _x = _el.clone(); _acc = {body}; }} _acc }} \
7060
+ }} else {{ _init0 }} }}",
7061
+ init = initial,
7062
+ obj = obj_expr,
7063
+ nat_op = nat_op,
7064
+ body = body_code
7065
+ )));
7066
+ }
7067
+
7068
+ Ok(Some(format!(
7069
+ "{{ let mut _acc = {init}; let _arr = ({obj}).clone(); \
7070
+ if let Value::Array(ref _a) = _arr {{ for _el in _a.borrow().iter() {{ \
7071
+ let _x = _el.clone(); _acc = {body}; }} }} _acc }}",
7072
+ init = initial,
7073
+ obj = obj_expr,
7074
+ body = body_code
7075
+ )))
7076
+ }
7077
+
7078
+ /// If `callee(args)` is `<Vec<f64>-ident>.reduce/map/filter/some/every(<arrow>)` and the
7079
+ /// `TISH_NATIVE_HOF` flag is set, lower it to a native iterator chain. Shared by
7080
+ /// `emit_typed_expr` (native sub-expressions) and `emit_native_expr` (typed `let` RHS), so a
7081
+ /// typed-array HOF lowers natively whether its result flows into arithmetic or a binding.
7082
+ fn native_vec_hof_for_call(
7083
+ &mut self,
7084
+ callee: &Expr,
7085
+ args: &[CallArg],
7086
+ ) -> Result<Option<(String, RustType)>, CompileError> {
7087
+ if std::env::var("TISH_NATIVE_HOF").is_err() {
7088
+ return Ok(None);
7089
+ }
7090
+ let Expr::Member {
7091
+ object,
7092
+ prop: MemberProp::Name { name: method, .. },
7093
+ optional: false,
7094
+ ..
7095
+ } = callee
7096
+ else {
7097
+ return Ok(None);
7098
+ };
7099
+ let Expr::Ident { name: recv_name, .. } = object.as_ref() else {
7100
+ return Ok(None);
7101
+ };
7102
+ // A RefCell-wrapped receiver would need a borrow to iterate — bail to the boxed path.
7103
+ if self.refcell_wrapped_vars.contains(recv_name.as_ref()) {
7104
+ return Ok(None);
7105
+ }
7106
+ let RustType::Vec(inner) = self.type_context.get_type(recv_name.as_ref()) else {
7107
+ return Ok(None);
7108
+ };
7109
+ if *inner != RustType::F64 {
7110
+ return Ok(None);
7111
+ }
7112
+ let recv_code = Self::escape_ident(recv_name.as_ref()).into_owned();
7113
+ self.try_native_vec_hof(&recv_code, &inner, recv_name.as_ref(), method.as_ref(), args)
7114
+ }
7115
+
7116
+ /// Native typed-array HOFs (`TISH_NATIVE_HOF`): when the receiver is a native `Vec<f64>`
7117
+ /// (a typed `number[]`), lower `reduce`/`map`/`filter`/`some`/`every` to a direct Rust
7118
+ /// iterator chain — no per-element `value_call`, no `Value` boxing.
7119
+ ///
7120
+ /// Preconditions (any failure → `Ok(None)` → boxed `array_*`, correctness over coverage):
7121
+ /// - element type is `f64` (Copy → `.copied()`),
7122
+ /// - the callback is an arrow with simple, no-default params and an **expression** body,
7123
+ /// - the body does **not** mention the receiver — `pi_mentions` is conservative (unknown
7124
+ /// AST nodes count as "mentions"), so a `&mut`/alias of the array inside the closure can't
7125
+ /// slip through and break the `.iter()` borrow,
7126
+ /// - the body's emitted native type matches what the method needs (`f64` for `reduce`/`map`
7127
+ /// element, `bool` for `filter`/`some`/`every`).
7128
+ ///
7129
+ /// The closure params are bound natively (`f64`) only while the body is emitted, then popped.
7130
+ fn try_native_vec_hof(
7131
+ &mut self,
7132
+ recv: &str,
7133
+ elem: &RustType,
7134
+ recv_name: &str,
7135
+ method: &str,
7136
+ args: &[CallArg],
7137
+ ) -> Result<Option<(String, RustType)>, CompileError> {
7138
+ // Only numeric arrays for now: `.copied()` needs a `Copy` element.
7139
+ if *elem != RustType::F64 {
7140
+ return Ok(None);
7141
+ }
7142
+ let Some(CallArg::Expr(Expr::ArrowFunction { params, body, .. })) = args.first() else {
7143
+ return Ok(None);
7144
+ };
7145
+ let tishlang_ast::ArrowBody::Expr(be) = body else {
7146
+ return Ok(None);
7147
+ };
7148
+ // The body must not touch the receiver (aliasing would break the `.iter()` borrow).
7149
+ if crate::infer::pi_mentions(be, recv_name) {
7150
+ return Ok(None);
7151
+ }
7152
+ let simple_name = |p: &FunParam| -> Option<std::sync::Arc<str>> {
7153
+ match p {
7154
+ FunParam::Simple(tp) if tp.default.is_none() => Some(std::sync::Arc::clone(&tp.name)),
7155
+ _ => None,
7156
+ }
7157
+ };
7158
+ // Emit `be` with `binds` (name, type) installed as native locals; restore on the way out.
7159
+ let emit_with = |this: &mut Self,
7160
+ binds: &[(&std::sync::Arc<str>, RustType)]|
7161
+ -> Result<(String, RustType), CompileError> {
7162
+ this.type_context.push_scope();
7163
+ for (n, t) in binds {
7164
+ this.type_context.define(n.as_ref(), t.clone());
7165
+ }
7166
+ let res = this.emit_typed_expr(be);
7167
+ this.type_context.pop_scope();
7168
+ res
7169
+ };
7170
+ match method {
7171
+ "reduce" => {
7172
+ if args.len() != 2 || params.len() != 2 {
7173
+ return Ok(None);
7174
+ }
7175
+ let (Some(acc), Some(x)) = (simple_name(&params[0]), simple_name(&params[1])) else {
7176
+ return Ok(None);
7177
+ };
7178
+ let CallArg::Expr(init_e) = &args[1] else {
7179
+ return Ok(None);
7180
+ };
7181
+ let (init_code, init_ty) = self.emit_typed_expr(init_e)?;
7182
+ let init_f64 = match init_ty {
7183
+ RustType::F64 => init_code,
7184
+ RustType::Value => RustType::F64.from_value_expr(&init_code),
7185
+ _ => return Ok(None),
7186
+ };
7187
+ let (body_code, body_ty) =
7188
+ emit_with(self, &[(&acc, RustType::F64), (&x, RustType::F64)])?;
7189
+ if body_ty != RustType::F64 {
7190
+ return Ok(None);
7191
+ }
7192
+ let acc_esc = Self::escape_ident(acc.as_ref()).into_owned();
7193
+ let x_esc = Self::escape_ident(x.as_ref()).into_owned();
7194
+ Ok(Some((
7195
+ format!(
7196
+ "{{ let mut {acc}: f64 = {init}; for {x} in {recv}.iter().copied() {{ {acc} = {body}; }} {acc} }}",
7197
+ acc = acc_esc, init = init_f64, x = x_esc, recv = recv, body = body_code
7198
+ ),
7199
+ RustType::F64,
7200
+ )))
7201
+ }
7202
+ "map" => {
7203
+ if args.len() != 1 || params.len() != 1 {
7204
+ return Ok(None);
7205
+ }
7206
+ let Some(x) = simple_name(&params[0]) else {
7207
+ return Ok(None);
7208
+ };
7209
+ let (body_code, body_ty) = emit_with(self, &[(&x, RustType::F64)])?;
7210
+ if !body_ty.is_native() {
7211
+ return Ok(None);
7212
+ }
7213
+ let x_esc = Self::escape_ident(x.as_ref()).into_owned();
7214
+ Ok(Some((
7215
+ format!(
7216
+ "{recv}.iter().copied().map(|{x}| {body}).collect::<Vec<{ety}>>()",
7217
+ recv = recv, x = x_esc, body = body_code, ety = body_ty.to_rust_type_str()
7218
+ ),
7219
+ RustType::Vec(Box::new(body_ty)),
7220
+ )))
7221
+ }
7222
+ "filter" => {
7223
+ if args.len() != 1 || params.len() != 1 {
7224
+ return Ok(None);
7225
+ }
7226
+ let Some(x) = simple_name(&params[0]) else {
7227
+ return Ok(None);
7228
+ };
7229
+ let (body_code, body_ty) = emit_with(self, &[(&x, RustType::F64)])?;
7230
+ if body_ty != RustType::Bool {
7231
+ return Ok(None);
7232
+ }
7233
+ let x_esc = Self::escape_ident(x.as_ref()).into_owned();
7234
+ Ok(Some((
7235
+ format!(
7236
+ "{recv}.iter().copied().filter(|&{x}| {body}).collect::<Vec<f64>>()",
7237
+ recv = recv, x = x_esc, body = body_code
7238
+ ),
7239
+ RustType::Vec(Box::new(RustType::F64)),
7240
+ )))
7241
+ }
7242
+ "some" | "every" => {
7243
+ if args.len() != 1 || params.len() != 1 {
7244
+ return Ok(None);
7245
+ }
7246
+ let Some(x) = simple_name(&params[0]) else {
7247
+ return Ok(None);
7248
+ };
7249
+ let (body_code, body_ty) = emit_with(self, &[(&x, RustType::F64)])?;
7250
+ if body_ty != RustType::Bool {
7251
+ return Ok(None);
7252
+ }
7253
+ let x_esc = Self::escape_ident(x.as_ref()).into_owned();
7254
+ let adapter = if method == "some" { "any" } else { "all" };
7255
+ Ok(Some((
7256
+ format!(
7257
+ "{recv}.iter().copied().{adapter}(|{x}| {body})",
7258
+ recv = recv, adapter = adapter, x = x_esc, body = body_code
7259
+ ),
7260
+ RustType::Bool,
7261
+ )))
7262
+ }
7263
+ _ => Ok(None),
7264
+ }
7265
+ }
7266
+
5283
7267
  fn emit_binop(&self, l: &str, op: BinOp, r: &str, span: Span) -> Result<String, CompileError> {
5284
7268
  Ok(match op {
5285
7269
  BinOp::Add => format!(
@@ -5318,8 +7302,9 @@ impl Codegen {
5318
7302
  BinOp::BitAnd => Self::emit_bitwise_binop(l, r, "&"),
5319
7303
  BinOp::BitOr => Self::emit_bitwise_binop(l, r, "|"),
5320
7304
  BinOp::BitXor => Self::emit_bitwise_binop(l, r, "^"),
5321
- BinOp::Shl => Self::emit_bitwise_binop(l, r, "<<"),
5322
- BinOp::Shr => Self::emit_bitwise_binop(l, r, ">>"),
7305
+ BinOp::Shl => Self::emit_shift_binop(l, r, "to_int32", "wrapping_shl"),
7306
+ BinOp::Shr => Self::emit_shift_binop(l, r, "to_int32", "wrapping_shr"),
7307
+ BinOp::UShr => Self::emit_shift_binop(l, r, "to_uint32", "wrapping_shr"),
5323
7308
  BinOp::In => format!("tish_in_operator(&{}, &{})", l, r),
5324
7309
  BinOp::Eq | BinOp::Ne => {
5325
7310
  return Err(CompileError::new(