@tishlang/tish 2.2.7 → 2.9.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 (55) hide show
  1. package/bin/tish +0 -0
  2. package/crates/js_to_tish/src/transform/expr.rs +5 -1
  3. package/crates/tish/Cargo.toml +2 -1
  4. package/crates/tish/src/cli_help.rs +15 -0
  5. package/crates/tish/src/main.rs +139 -12
  6. package/crates/tish/tests/integration_test.rs +65 -0
  7. package/crates/tish_ast/src/ast.rs +6 -2
  8. package/crates/tish_builtins/src/array.rs +82 -1
  9. package/crates/tish_builtins/src/globals.rs +50 -16
  10. package/crates/tish_builtins/src/math.rs +63 -9
  11. package/crates/tish_builtins/src/number.rs +20 -1
  12. package/crates/tish_builtins/src/string.rs +68 -4
  13. package/crates/tish_builtins/src/typedarrays.rs +23 -16
  14. package/crates/tish_bytecode/src/compiler.rs +94 -28
  15. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +19 -0
  16. package/crates/tish_compile/src/check.rs +1 -1
  17. package/crates/tish_compile/src/codegen.rs +3133 -582
  18. package/crates/tish_compile/src/infer.rs +1950 -56
  19. package/crates/tish_compile/src/lib.rs +38 -0
  20. package/crates/tish_compile/src/resolve.rs +3 -2
  21. package/crates/tish_compile/src/types.rs +17 -0
  22. package/crates/tish_compile_js/Cargo.toml +3 -0
  23. package/crates/tish_compile_js/src/codegen.rs +365 -9
  24. package/crates/tish_compile_js/src/lib.rs +2 -1
  25. package/crates/tish_compile_js/src/tests_jsx.rs +229 -1
  26. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +1 -0
  27. package/crates/tish_core/Cargo.toml +2 -0
  28. package/crates/tish_core/src/json.rs +135 -34
  29. package/crates/tish_core/src/lib.rs +23 -1
  30. package/crates/tish_core/src/value.rs +64 -11
  31. package/crates/tish_eval/src/eval.rs +144 -197
  32. package/crates/tish_eval/src/natives.rs +45 -45
  33. package/crates/tish_eval/src/regex.rs +35 -27
  34. package/crates/tish_eval/src/value.rs +8 -0
  35. package/crates/tish_fmt/src/lib.rs +45 -1
  36. package/crates/tish_lint/src/bin/tish-lint.rs +17 -8
  37. package/crates/tish_lint/src/lib.rs +197 -21
  38. package/crates/tish_lsp/Cargo.toml +6 -0
  39. package/crates/tish_lsp/src/import_goto.rs +52 -7
  40. package/crates/tish_lsp/src/main.rs +856 -140
  41. package/crates/tish_opt/src/lib.rs +5 -3
  42. package/crates/tish_parser/src/lib.rs +23 -5
  43. package/crates/tish_parser/src/parser.rs +15 -26
  44. package/crates/tish_resolve/src/lib.rs +188 -18
  45. package/crates/tish_runtime/src/lib.rs +58 -18
  46. package/crates/tish_ui/src/jsx.rs +2 -2
  47. package/crates/tish_vm/src/jit.rs +212 -10
  48. package/crates/tish_vm/src/vm.rs +39 -18
  49. package/crates/tish_wasm/src/lib.rs +116 -22
  50. package/package.json +1 -1
  51. package/platform/darwin-arm64/tish +0 -0
  52. package/platform/darwin-x64/tish +0 -0
  53. package/platform/linux-arm64/tish +0 -0
  54. package/platform/linux-x64/tish +0 -0
  55. package/platform/win32-x64/tish.exe +0 -0
@@ -179,7 +179,7 @@ impl UsageAnalyzer {
179
179
  Expr::Object { props, .. } => {
180
180
  for prop in props {
181
181
  match prop {
182
- ObjectProp::KeyValue(_, v) => self.analyze_expr(v),
182
+ ObjectProp::KeyValue(_, v, _) => self.analyze_expr(v),
183
183
  ObjectProp::Spread(e) => self.analyze_expr(e),
184
184
  }
185
185
  }
@@ -398,7 +398,7 @@ fn program_uses_async(program: &Program) -> bool {
398
398
  ArrayElement::Expr(e) | ArrayElement::Spread(e) => expr_has_await(e),
399
399
  }),
400
400
  Expr::Object { props, .. } => props.iter().any(|p| match p {
401
- ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => expr_has_await(e),
401
+ ObjectProp::KeyValue(_, e, _) | ObjectProp::Spread(e) => expr_has_await(e),
402
402
  }),
403
403
  Expr::Assign { value, .. }
404
404
  | Expr::CompoundAssign { value, .. }
@@ -737,6 +737,41 @@ pub fn compile_with_native_modules_emit(
737
737
  Ok(g.output)
738
738
  }
739
739
 
740
+ /// #177 (S-E/S-F): the return shape of a de-virtualized aggregate (struct/array) free fn.
741
+ #[derive(Debug, Clone, PartialEq)]
742
+ enum AggRet {
743
+ /// Returns the unboxed struct by value (the `body()` factory).
744
+ Struct,
745
+ /// Returns `Vec<TishStruct_alias>` by value (the `makeBodies()` array factory).
746
+ ArrayOfStruct,
747
+ /// Returns a plain `f64` (`energy()`).
748
+ F64,
749
+ /// Returns nothing (`advance()`, `offsetMomentum()` — JS `undefined`).
750
+ Unit,
751
+ }
752
+
753
+ /// #177: one parameter of an aggregate free fn, in source order.
754
+ #[derive(Debug, Clone)]
755
+ enum AggParamKind {
756
+ /// The `Vec<TishStruct_alias>` array param, threaded by shared reference
757
+ /// (`&mut` if the fn mutates an element, `&` if read-only).
758
+ Array { is_mut: bool },
759
+ /// A scalar param (always `f64` for the nbody shape).
760
+ Scalar(RustType),
761
+ }
762
+
763
+ /// #177: the de-virtualized native signature of one aggregate fn.
764
+ #[derive(Debug, Clone)]
765
+ struct AggFnSig {
766
+ /// Source params in order: (name, kind).
767
+ params: Vec<(String, AggParamKind)>,
768
+ /// Top-level numeric globals the body references, appended as trailing `f64`
769
+ /// params (sorted for a stable decl/call-site order).
770
+ captured: Vec<String>,
771
+ /// What the fn returns.
772
+ ret: AggRet,
773
+ }
774
+
740
775
  struct Codegen {
741
776
  output: String,
742
777
  indent: usize,
@@ -774,6 +809,20 @@ struct Codegen {
774
809
  /// free `fn f_native(f64,..)->f64` (all params `: number`, returns `number`, native-safe
775
810
  /// body). Direct calls to these route to the native fn, bypassing the boxed `value_call`.
776
811
  native_fns: std::collections::HashSet<String>,
812
+ /// #177 S-E/S-F (dark-shipped behind `TISH_AGGREGATE_INFER`): the unboxed struct alias name
813
+ /// (e.g. `TishAnon_0`) when the interprocedural aggregate path is active for this program,
814
+ /// else `None`. Set in `emit_program` only after the de-virtualized fns emit successfully.
815
+ aggregate_alias: Option<String>,
816
+ /// #177: fn name → its de-virtualized native signature. Calls to these route directly to the
817
+ /// `fn name_agg(..)` free fn (threading the `Vec<TishStruct_alias>` by reference), bypassing
818
+ /// the boxed `value_call`. Empty when the aggregate path is inactive.
819
+ aggregate_fns: std::collections::HashMap<String, AggFnSig>,
820
+ /// #177: top-level `let` names bound to the unboxed `Vec<TishStruct_alias>` (e.g. `bodies`).
821
+ /// These are emitted `let mut` and passed `&mut`/`&` into the aggregate fns.
822
+ aggregate_array_locals: std::collections::HashSet<String>,
823
+ /// #177: while emitting an aggregate fn body, the return shape of the fn currently being
824
+ /// emitted (drives `Return` lowering). `None` outside aggregate-fn emission.
825
+ agg_cur_ret: Option<AggRet>,
777
826
  /// Names of `number`-typed locals demoted to a boxed `Value` because some reassignment can
778
827
  /// store a non-number — e.g. `let s = 0; s = s + arr[i]` where `arr` is a boxed Value: `+` is
779
828
  /// JS string concat, so `s` may become a `String`. Lowering `s` to a native `f64` would panic
@@ -782,6 +831,27 @@ struct Codegen {
782
831
  /// `RustType::Value`. This is the rust-AOT analogue of the VM array-JIT bailing to the
783
832
  /// interpreter on a non-numeric element. See `collect_demoted_numeric_locals`.
784
833
  demoted_numeric_locals: std::collections::HashSet<String>,
834
+ /// Integer-range lattice (#174): names of `f64` locals the analysis proves always hold an
835
+ /// integer within `[min, max]`, both strictly inside `(-2^53, 2^53)` so `as i64` is exact and
836
+ /// `i64` arithmetic is bit-identical to the `f64` the interpreter/VM use. Lets the codegen
837
+ /// lower e.g. `x % c` to a fast integer remainder instead of `fmod`. Conservative: a name absent
838
+ /// here is treated as unbounded. Populated by `collect_int_range_locals`.
839
+ int_range_locals: std::collections::HashMap<String, (i64, i64)>,
840
+ /// Integer-range lattice (#174): locals that are always INTEGER-valued (an `f64` with zero
841
+ /// fractional part), possibly of unbounded magnitude — unlike `int_range_locals`. Loop counters
842
+ /// (`i = i + 1`) qualify even though their magnitude isn't bounded. Used to prove a modulo
843
+ /// result like `r % 97` is integral, so it can seed a fold accumulator's bounded range.
844
+ int_valued_locals: std::collections::HashSet<String>,
845
+ /// Integer-range lattice (#174): `number[]` locals initialized from an array literal of integer
846
+ /// literals → the inclusive element range, both inside `(-2^53, 2^53)`. Bounds a native fold's
847
+ /// element variable so the fold body can lower to native `i64` arithmetic.
848
+ array_elem_ranges: std::collections::HashMap<String, (i64, i64)>,
849
+ /// i32-loop-var lowering: names of `number` accumulators a per-body analysis proved can live
850
+ /// in an `i32` register across a bitwise/hash hot loop (`h` in FNV) instead of round-tripping
851
+ /// `f64`↔`i32` every op. Each is declared `let mut h: i32`, every reassignment lowers via
852
+ /// `emit_int32_operand`, and reads coerce `(h as f64)`. See `collect_i32_loop_vars` for the
853
+ /// (strict) eligibility/soundness gate. Scoped per function body / top level.
854
+ i32_loop_vars: std::collections::HashSet<String>,
785
855
  /// Scopes of names whose Rust binding is actually `Rc<RefCell<_>>` (emitted at VarDecl).
786
856
  /// `refcell_wrapped_vars` alone is insufficient: it is set by prepasses before decl may run.
787
857
  rc_cell_storage_scopes: Vec<std::collections::HashSet<String>>,
@@ -835,7 +905,15 @@ impl Codegen {
835
905
  outer_vars_stack: vec![Vec::new()], // Start with module-level scope
836
906
  refcell_wrapped_vars: std::collections::HashSet::new(),
837
907
  native_fns: std::collections::HashSet::new(),
908
+ aggregate_alias: None,
909
+ aggregate_fns: std::collections::HashMap::new(),
910
+ aggregate_array_locals: std::collections::HashSet::new(),
911
+ agg_cur_ret: None,
838
912
  demoted_numeric_locals: std::collections::HashSet::new(),
913
+ int_range_locals: std::collections::HashMap::new(),
914
+ int_valued_locals: std::collections::HashSet::new(),
915
+ array_elem_ranges: std::collections::HashMap::new(),
916
+ i32_loop_vars: std::collections::HashSet::new(),
839
917
  rc_cell_storage_scopes: vec![std::collections::HashSet::new()],
840
918
  usage_analyzer: None,
841
919
  type_context: TypeContext::new(),
@@ -1343,23 +1421,27 @@ impl Codegen {
1343
1421
 
1344
1422
  /// Generate code for a bitwise binary operation (`& | ^`). `to_int32` is JS ToInt32
1345
1423
  /// (modulo 2³², NaN/±Infinity → 0) — out-of-range operands wrap, not saturate.
1424
+ /// Boxed/`Value`-path bitwise op (`& | ^`). Uses the `*_value(&Value)` coercion helpers rather
1425
+ /// than a `let Value::Number(a) = &(..) else { panic!() }` block: the block bound `a`/`b`, so a
1426
+ /// nested bitwise operand (whose block *also* binds `a`/`b`) shadowed the outer binding and the
1427
+ /// generated code failed to compile (`error[E0308]`, `&&f64` vs `Value`). The helpers bind no
1428
+ /// name, so the ops compose at any nesting depth, and they coerce non-numbers to `NaN` (→ `0`)
1429
+ /// exactly like the interpreter/VM instead of panicking.
1346
1430
  fn emit_bitwise_binop(l: &str, r: &str, op: &str) -> String {
1347
1431
  format!(
1348
- "Value::Number({{ let Value::Number(a) = &({}) else {{ panic!() }}; \
1349
- let Value::Number(b) = &({}) else {{ panic!() }}; (tishlang_runtime::to_int32(*a) {} tishlang_runtime::to_int32(*b)) as f64 }})",
1350
- l, r, op
1432
+ "Value::Number((tishlang_runtime::to_int32_value(&({})) {} tishlang_runtime::to_int32_value(&({}))) as f64)",
1433
+ l, op, r
1351
1434
  )
1352
1435
  }
1353
1436
 
1354
- /// Generate code for a shift (`<< >> >>>`). `a_to` is the left-operand coercion
1355
- /// (`to_int32` signed, `to_uint32` for the logical `>>>`); `method` is the `wrapping_sh*`
1356
- /// call. Counts go through `to_uint32` then mask to 5 bits — exact JS semantics, panic-free.
1437
+ /// Boxed/`Value`-path shift (`<< >> >>>`). `a_to` is the left-operand coercion helper
1438
+ /// (`to_int32_value` signed, `to_uint32_value` for the logical `>>>`); `method` is the
1439
+ /// `wrapping_sh*` call. Counts go through `to_uint32_value` then mask to 5 bits — exact JS
1440
+ /// semantics, panic-free, and composable (no name binding — see `emit_bitwise_binop`).
1357
1441
  fn emit_shift_binop(l: &str, r: &str, a_to: &str, method: &str) -> String {
1358
1442
  format!(
1359
- "Value::Number({{ let Value::Number(a) = &({}) else {{ panic!() }}; \
1360
- let Value::Number(b) = &({}) else {{ panic!() }}; \
1361
- tishlang_runtime::{}(*a).{}(tishlang_runtime::to_uint32(*b)) as f64 }})",
1362
- l, r, a_to, method
1443
+ "Value::Number(tishlang_runtime::{}(&({})).{}(tishlang_runtime::to_uint32_value(&({}))) as f64)",
1444
+ a_to, l, method, r
1363
1445
  )
1364
1446
  }
1365
1447
 
@@ -1435,7 +1517,7 @@ impl Codegen {
1435
1517
  self.write("use std::cell::RefCell;\n");
1436
1518
  self.write("use std::rc::Rc;\n");
1437
1519
  self.write("use std::sync::Arc;\n");
1438
- 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");
1520
+ self.write("use tishlang_runtime::{console_debug as tish_console_debug, console_info as tish_console_info, console_log as tish_console_log, console_warn as tish_console_warn, console_error as tish_console_error, boolean as tish_boolean, decode_uri as tish_decode_uri, encode_uri as tish_encode_uri, string_escape_html_impl as tish_escape_html, in_operator as tish_in_operator, is_finite as tish_is_finite, is_nan as tish_is_nan, json_parse as tish_json_parse, json_stringify as tish_json_stringify, math_abs as tish_math_abs, math_ceil as tish_math_ceil, math_floor as tish_math_floor, math_max as tish_math_max, math_min as tish_math_min, math_round as tish_math_round, math_sqrt as tish_math_sqrt, parse_float as tish_parse_float, parse_int as tish_parse_int, math_random as tish_math_random, math_pow as tish_math_pow, math_sin as tish_math_sin, math_cos as tish_math_cos, math_tan as tish_math_tan, math_log as tish_math_log, math_exp as tish_math_exp, math_sign as tish_math_sign, math_trunc as tish_math_trunc, math_imul as tish_math_imul, math_sinh as tish_math_sinh, math_cosh as tish_math_cosh, math_tanh as tish_math_tanh, math_asinh as tish_math_asinh, math_acosh as tish_math_acosh, math_atanh as tish_math_atanh, math_cbrt as tish_math_cbrt, math_log2 as tish_math_log2, math_log10 as tish_math_log10, math_hypot as tish_math_hypot, math_atan2 as tish_math_atan2, math_asin as tish_math_asin, math_acos as tish_math_acos, math_atan as tish_math_atan, array_is_array as tish_array_is_array, array_construct as tish_array_construct, string_from_char_code as tish_string_from_char_code, string_convert as tish_string_convert, number_convert as tish_number_convert, object_assign as tish_object_assign, object_keys as tish_object_keys, object_values as tish_object_values, object_entries as tish_object_entries, object_from_entries as tish_object_from_entries, symbol_object as tish_symbol_object, tish_construct, tish_error_constructor, tish_date_constructor, tish_set_constructor, tish_map_constructor, 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");
1439
1521
  if self.program_has_jsx {
1440
1522
  self.write("use tishlang_ui::{fragment_value, install_thread_local_host, native_create_root, native_use_state, ui_h, ui_text, HeadlessHost};\n");
1441
1523
  }
@@ -1513,12 +1595,25 @@ impl Codegen {
1513
1595
  self.writeln("");
1514
1596
  }
1515
1597
  }
1598
+ // #177 (S-E/S-F, dark-shipped behind TISH_AGGREGATE_INFER): de-virtualize the nbody-shape
1599
+ // aggregate fns into native Rust free fns operating on an unboxed `Vec<TishStruct_alias>`
1600
+ // threaded by reference. Computed + emitted here (before `run()`); if any fn can't be
1601
+ // lowered the whole path is disabled and we fall back to the boxed closures unchanged.
1602
+ if std::env::var("TISH_AGGREGATE_INFER").map(|v| v != "0").unwrap_or(false) {
1603
+ self.setup_aggregate_fns(program);
1604
+ }
1516
1605
  // Soundness pass — must run after type aliases + `native_fns` are known (both feed the
1517
1606
  // native-type oracle): find `number`-typed locals a reassignment can turn non-numeric so
1518
1607
  // `VarDecl` lowers them as boxed `Value` rather than native `f64` (else the store coerces
1519
1608
  // and panics on a JS string-concat result like `s = s + arr[i]`). See
1520
1609
  // `collect_demoted_numeric_locals` / `demoted_numeric_locals`.
1521
1610
  self.demoted_numeric_locals = self.collect_demoted_numeric_locals(&program.statements);
1611
+ self.int_valued_locals = Self::collect_int_valued_locals(&program.statements);
1612
+ self.int_range_locals = self.collect_int_range_locals(&program.statements);
1613
+ self.array_elem_ranges = Self::collect_array_elem_ranges(&program.statements);
1614
+ // i32-loop-var lowering: must run AFTER `int_range_locals` (the soundness backstop that
1615
+ // proves the accumulator stays an exact integer reinterpretable as i32).
1616
+ self.i32_loop_vars = self.collect_i32_loop_vars(&program.statements);
1522
1617
  if self.is_async {
1523
1618
  self.writeln("async fn run() -> Result<(), Box<dyn std::error::Error>> {");
1524
1619
  } else if self.emit_mode == crate::NativeEmitMode::EmbeddedLib {
@@ -1592,7 +1687,8 @@ impl Codegen {
1592
1687
  self.writeln(
1593
1688
  "(Arc::from(\"imul\"), Value::native(|args: &[Value]| tish_math_imul(args))),",
1594
1689
  );
1595
- // Hyperbolic / inverse-hyperbolic / cbrt / base-2/10 logs (issue #61).
1690
+ // Hyperbolic / inverse-hyperbolic / cbrt / base-2/10 logs (issue #61) + hypot/atan2 and the
1691
+ // inverse trig that were missing on the native Math but present on the vm (#247).
1596
1692
  for (name, func) in [
1597
1693
  ("sinh", "tish_math_sinh"),
1598
1694
  ("cosh", "tish_math_cosh"),
@@ -1603,6 +1699,11 @@ impl Codegen {
1603
1699
  ("cbrt", "tish_math_cbrt"),
1604
1700
  ("log2", "tish_math_log2"),
1605
1701
  ("log10", "tish_math_log10"),
1702
+ ("hypot", "tish_math_hypot"),
1703
+ ("atan2", "tish_math_atan2"),
1704
+ ("asin", "tish_math_asin"),
1705
+ ("acos", "tish_math_acos"),
1706
+ ("atan", "tish_math_atan"),
1606
1707
  ] {
1607
1708
  self.writeln(&format!(
1608
1709
  "(Arc::from(\"{name}\"), Value::native(|args: &[Value]| {func}(args))),"
@@ -1795,6 +1896,11 @@ impl Codegen {
1795
1896
  let top_level_funcs = self.prescan_function_decls(&program.statements);
1796
1897
  *self.function_scope_stack.last_mut().unwrap() = top_level_funcs.clone();
1797
1898
  for func_name in &top_level_funcs {
1899
+ // #177: functions promoted to native aggregate free fns (`<name>_agg`) have their
1900
+ // boxed closure + cell suppressed — no boxed value exists to back-patch.
1901
+ if self.aggregate_alias.is_some() && self.aggregate_fns.contains_key(func_name) {
1902
+ continue;
1903
+ }
1798
1904
  let escaped = Self::escape_ident(func_name);
1799
1905
  self.writeln(&format!(
1800
1906
  "let {}_cell: VmRef<Value> = VmRef::new(Value::Null);",
@@ -1862,6 +1968,30 @@ impl Codegen {
1862
1968
  match expr {
1863
1969
  Expr::Assign { name, value, .. } => {
1864
1970
  let rust_type = self.type_context.get_type(name.as_ref());
1971
+ // i32-loop-var lowering: the accumulator lives in an `i32` register. Each
1972
+ // reassignment RHS is a bitwise/shift chain the gate proved lowers fully via
1973
+ // `emit_int32_operand` (a `>>> 0` result is u32 reinterpreted to i32; signed
1974
+ // bitwise ops yield i32 directly) — so store the i32 with NO `f64` round-trip.
1975
+ if rust_type == RustType::I32 {
1976
+ if let Some(int_code) = self.emit_int32_operand(value)? {
1977
+ let escaped = Self::escape_ident(name.as_ref());
1978
+ return Ok(format!("{} = ({}) as i32", escaped, int_code));
1979
+ }
1980
+ // Defensive: gate guarantees `Some`, but if a future RHS shape slips through,
1981
+ // fall back to a sound f64-narrowed store rather than miscompiling.
1982
+ let (val_code, val_ty) = self.emit_typed_expr(value)?;
1983
+ let v = if val_ty.is_native() {
1984
+ val_ty.to_value_expr(&val_code)
1985
+ } else {
1986
+ val_code
1987
+ };
1988
+ let escaped = Self::escape_ident(name.as_ref());
1989
+ return Ok(format!(
1990
+ "{} = {}",
1991
+ escaped,
1992
+ RustType::I32.from_value_expr(&v)
1993
+ ));
1994
+ }
1865
1995
  // String self-append `s = s + rhs` -> in-place push_str (amortized O(1)). The
1866
1996
  // general path boxes via `ops::add(Value::String(s.clone()), ...)` which clones
1867
1997
  // the whole string per concat -> O(n^2) string building. rhs must be String-typed.
@@ -2040,10 +2170,41 @@ impl Codegen {
2040
2170
  rust_type = RustType::Value;
2041
2171
  }
2042
2172
 
2173
+ // i32-loop-var lowering: a `number` accumulator the analysis proved can live in an
2174
+ // `i32` register across a bitwise/hash hot loop. Declare `let mut h: i32` with the
2175
+ // init reinterpreted via `u32` so a literal ≥ 2^31 keeps its JS ToInt32 bit-pattern.
2176
+ if rust_type == RustType::F64 && self.i32_loop_vars.contains(name.as_ref()) {
2177
+ let init_lit = init
2178
+ .as_ref()
2179
+ .and_then(|e| Self::int_literal_value_of(e));
2180
+ if let Some(v) = init_lit {
2181
+ rust_type = RustType::I32;
2182
+ self.type_context.define(name.as_ref(), rust_type.clone());
2183
+ let escaped_name = Self::escape_ident(name.as_ref());
2184
+ let mutability = if *mutable { "let mut" } else { "let" };
2185
+ // `v` is an exact integer (gate proved it); reinterpret its low 32 bits as
2186
+ // i32 = ToInt32(v), the same bit-pattern the bitwise path produces.
2187
+ self.writeln(&format!(
2188
+ "{} {}: i32 = ({}u32) as i32;",
2189
+ mutability,
2190
+ escaped_name,
2191
+ (v as i64 as u32)
2192
+ ));
2193
+ if let Some(scope) = self.outer_vars_stack.last_mut() {
2194
+ scope.push(name.to_string());
2195
+ }
2196
+ return Ok(());
2197
+ }
2198
+ }
2199
+
2043
2200
  // Track the variable type
2044
2201
  self.type_context.define(name.as_ref(), rust_type.clone());
2045
2202
 
2046
- let mutability = if *mutable { "let mut" } else { "let" };
2203
+ // #177: the unboxed `Vec<TishStruct>` local is threaded `&mut` into the aggregate
2204
+ // operators (`advance`/`offsetMomentum`), so it must be `mut` even when never
2205
+ // directly reassigned in source.
2206
+ let force_mut = self.aggregate_array_locals.contains(name.as_ref());
2207
+ let mutability = if *mutable || force_mut { "let mut" } else { "let" };
2047
2208
  let escaped_name = Self::escape_ident(name.as_ref());
2048
2209
 
2049
2210
  if rust_type.is_native() {
@@ -2513,6 +2674,15 @@ impl Codegen {
2513
2674
  span,
2514
2675
  ..
2515
2676
  } => {
2677
+ // #177: this function was de-virtualized into a native aggregate free fn
2678
+ // (`<name>_agg`, emitted before `run()`); all call sites were routed there.
2679
+ // Skip the boxed closure entirely — its body now references unboxed structs
2680
+ // that no longer fit the boxed `Value` ABI.
2681
+ if self.aggregate_alias.is_some()
2682
+ && self.aggregate_fns.contains_key(name.as_ref())
2683
+ {
2684
+ return Ok(());
2685
+ }
2516
2686
  // Use Rc<RefCell<>> pattern to allow recursive function calls
2517
2687
  // The function can reference itself through the cell
2518
2688
  let name_raw = name.as_ref();
@@ -3212,22 +3382,31 @@ impl Codegen {
3212
3382
  let o = self.emit_expr(operand)?;
3213
3383
  match op {
3214
3384
  UnaryOp::Not => format!("Value::Bool(!{}.is_truthy())", o),
3215
- UnaryOp::Neg => format!(
3216
- "Value::Number({{ let Value::Number(n) = &({}) else {{ panic!(\"Expected number\") }}; -n }})",
3217
- o
3218
- ),
3219
- UnaryOp::Pos => format!(
3220
- "Value::Number({{ let Value::Number(n) = &({}) else {{ panic!(\"Expected number\") }}; *n }})",
3221
- o
3222
- ),
3385
+ // `*_value(&Value)` coercion (no name binding) so unary ops compose over nested
3386
+ // bitwise/unary operands without the `let Value::Number(n) = &(..)` shadowing
3387
+ // miscompile, and coerce non-numbers to `NaN` like the interpreter/VM.
3388
+ UnaryOp::Neg => {
3389
+ format!("Value::Number(-tishlang_runtime::to_number_value(&({})))", o)
3390
+ }
3391
+ UnaryOp::Pos => {
3392
+ format!("Value::Number(tishlang_runtime::to_number_value(&({})))", o)
3393
+ }
3223
3394
  UnaryOp::BitNot => format!(
3224
- "Value::Number({{ let Value::Number(n) = &({}) else {{ panic!(\"Expected number\") }}; (!tishlang_runtime::to_int32(*n)) as f64 }})",
3395
+ "Value::Number((!tishlang_runtime::to_int32_value(&({}))) as f64)",
3225
3396
  o
3226
3397
  ),
3227
3398
  UnaryOp::Void => format!("{{ {}; Value::Null }}", o),
3228
3399
  }
3229
3400
  }
3230
3401
  Expr::Call { callee, args, .. } => {
3402
+ // #177: route a top-level call to a de-virtualized aggregate fn. Void fns
3403
+ // (`advance`/`offsetMomentum`) emit `name_agg(&mut bodies, …)` (a `()` statement);
3404
+ // an f64-returning fn (`energy`) is boxed back into `Value::Number` for this path.
3405
+ if !self.aggregate_fns.is_empty() {
3406
+ if let Some((code, _)) = self.try_emit_toplevel_agg_call(callee, args, true)? {
3407
+ return Ok(code);
3408
+ }
3409
+ }
3231
3410
  // Typed-struct shortcut for `JSON.stringify(typedValue)`.
3232
3411
  // When the single arg has a known native type that owns a
3233
3412
  // hand-rolled `_tish_write_json` (struct or `Vec<struct>`),
@@ -3429,9 +3608,10 @@ impl Codegen {
3429
3608
  }
3430
3609
  "split" => {
3431
3610
  let sep = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
3611
+ let limit = arg_exprs.get(1).cloned().unwrap_or_else(|| "Value::Null".to_string());
3432
3612
  return Ok(format!(
3433
- "tishlang_runtime::string_split(&{}, &{})",
3434
- obj_expr, sep
3613
+ "tishlang_runtime::string_split_limit(&{}, &{}, &{})",
3614
+ obj_expr, sep, limit
3435
3615
  ));
3436
3616
  }
3437
3617
  "trim" => {
@@ -3498,6 +3678,15 @@ impl Codegen {
3498
3678
  obj_expr, idx
3499
3679
  ));
3500
3680
  }
3681
+ "at" => {
3682
+ // `at` is on both String and Array; this match is by method name, so
3683
+ // dispatch on the runtime value type (#247).
3684
+ let idx = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Number(0.0)".to_string());
3685
+ return Ok(format!(
3686
+ "tishlang_runtime::value_at(&{}, &{})",
3687
+ obj_expr, idx
3688
+ ));
3689
+ }
3501
3690
  "charCodeAt" => {
3502
3691
  let idx = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Number(0.0)".to_string());
3503
3692
  return Ok(format!(
@@ -3599,6 +3788,20 @@ impl Codegen {
3599
3788
  obj_expr, callback
3600
3789
  ));
3601
3790
  }
3791
+ "findLast" => {
3792
+ let callback = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
3793
+ return Ok(format!(
3794
+ "tishlang_runtime::array_find_last(&{}, &{})",
3795
+ obj_expr, callback
3796
+ ));
3797
+ }
3798
+ "findLastIndex" => {
3799
+ let callback = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
3800
+ return Ok(format!(
3801
+ "tishlang_runtime::array_find_last_index(&{}, &{})",
3802
+ obj_expr, callback
3803
+ ));
3804
+ }
3602
3805
  "some" => {
3603
3806
  let callback = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
3604
3807
  return Ok(format!(
@@ -3844,7 +4047,13 @@ impl Codegen {
3844
4047
  for elem in elements {
3845
4048
  if let ArrayElement::Expr(expr) = elem {
3846
4049
  let v = self.emit_expr(expr)?;
3847
- if self.should_clone(expr) {
4050
+ // A `Value`-typed identifier (object, or a global like `NaN`/`Infinity`)
4051
+ // is emitted bare here, so moving it into the array breaks any later use
4052
+ // in the SAME expression — e.g. `[1, o].includes(o)` borrows `o` after the
4053
+ // array moved it. The scope-local last-use analysis can't see that reuse,
4054
+ // so clone every identifier element (cheap; these literals are cold, and
4055
+ // string/number idents already clone inside their `Value::*` conversion).
4056
+ if matches!(expr, Expr::Ident { .. }) || self.should_clone(expr) {
3848
4057
  els.push(format!("({}).clone()", v));
3849
4058
  } else {
3850
4059
  els.push(v);
@@ -3868,7 +4077,7 @@ impl Codegen {
3868
4077
  let mut parts = Vec::new();
3869
4078
  for prop in props {
3870
4079
  match prop {
3871
- ObjectProp::KeyValue(k, v) => {
4080
+ ObjectProp::KeyValue(k, v, _) => {
3872
4081
  let val = self.emit_expr(v)?;
3873
4082
  if self.should_clone(v) {
3874
4083
  parts.push(format!("_obj.insert(Arc::from({:?}), ({}).clone());", k.as_ref(), val));
@@ -3886,7 +4095,7 @@ impl Codegen {
3886
4095
  } else {
3887
4096
  let mut parts = Vec::new();
3888
4097
  for prop in props {
3889
- if let ObjectProp::KeyValue(k, v) = prop {
4098
+ if let ObjectProp::KeyValue(k, v, _) = prop {
3890
4099
  let val = self.emit_expr(v)?;
3891
4100
  if self.should_clone(v) {
3892
4101
  parts.push(format!("(Arc::from({:?}), ({}).clone())", k.as_ref(), val));
@@ -4553,7 +4762,7 @@ impl Codegen {
4553
4762
  Expr::Object { props, .. } => {
4554
4763
  for prop in props {
4555
4764
  match prop {
4556
- ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => {
4765
+ ObjectProp::KeyValue(_, e, _) | ObjectProp::Spread(e) => {
4557
4766
  Self::collect_expr_idents(e, idents)
4558
4767
  }
4559
4768
  }
@@ -4812,7 +5021,7 @@ impl Codegen {
4812
5021
  Expr::Object { props, .. } => {
4813
5022
  for prop in props {
4814
5023
  match prop {
4815
- ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => {
5024
+ ObjectProp::KeyValue(_, e, _) | ObjectProp::Spread(e) => {
4816
5025
  Self::collect_assigned_idents_in_expr(e, names);
4817
5026
  }
4818
5027
  }
@@ -5026,7 +5235,7 @@ impl Codegen {
5026
5235
  Expr::Object { props, .. } => {
5027
5236
  for prop in props {
5028
5237
  match prop {
5029
- ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => {
5238
+ ObjectProp::KeyValue(_, e, _) | ObjectProp::Spread(e) => {
5030
5239
  Self::collect_captured_block_vars_from_expr(e, block_vars, result);
5031
5240
  }
5032
5241
  }
@@ -5774,6 +5983,16 @@ impl Codegen {
5774
5983
  expr: &Expr,
5775
5984
  target_type: &RustType,
5776
5985
  ) -> Result<String, CompileError> {
5986
+ // #177: `let bodies = makeBodies()` — route the array-factory call to its native free fn
5987
+ // returning `Vec<TishStruct_alias>` directly (no boxed `Value::Array` round-trip).
5988
+ if !self.aggregate_fns.is_empty() {
5989
+ if let Expr::Call { callee, args, .. } = expr {
5990
+ if let Some((code, _)) = self.try_emit_toplevel_agg_call(callee, args, false)? {
5991
+ return Ok(code);
5992
+ }
5993
+ }
5994
+ }
5995
+
5777
5996
  // Try to emit literals directly as native types
5778
5997
  if let Expr::Literal { value, .. } = expr {
5779
5998
  match (target_type, value) {
@@ -5848,7 +6067,7 @@ impl Codegen {
5848
6067
  let mut bail = false;
5849
6068
  for prop in props {
5850
6069
  match prop {
5851
- ObjectProp::KeyValue(key, value) => {
6070
+ ObjectProp::KeyValue(key, value, _) => {
5852
6071
  if let Some(field_ty) = field_types.get(key.as_ref()) {
5853
6072
  let v = self.emit_native_expr(value, field_ty)?;
5854
6073
  field_inits.insert(crate::types::field_ident(key.as_ref()), v);
@@ -5905,6 +6124,19 @@ impl Codegen {
5905
6124
  }
5906
6125
  }
5907
6126
 
6127
+ // Fast path: when the native typed emitter already yields the target type, use its code
6128
+ // directly — skipping the `Value::Number(<expr>)` box that `from_value_expr` would
6129
+ // immediately unbox. This round-trip otherwise lands in hot loops: `let xt = x*x - y*y + x0`
6130
+ // (xt inferred f64) emitted `match &Value::Number(<expr>) { Value::Number(n) => *n,
6131
+ // _ => panic!() }` *every iteration*. `emit_typed_expr`'s contract guarantees `code` is a
6132
+ // value of `typed_ty` directly, so when it equals the target the code is exactly what we
6133
+ // want, unboxed. (Any other type falls through to the unchanged box-and-coerce path below.)
6134
+ if let Ok((typed_code, typed_ty)) = self.emit_typed_expr(expr) {
6135
+ if &typed_ty == target_type {
6136
+ return Ok(typed_code);
6137
+ }
6138
+ }
6139
+
5908
6140
  // Fall back to emit_expr + conversion
5909
6141
  let value_expr = self.emit_expr(expr)?;
5910
6142
  Ok(target_type.from_value_expr(&value_expr))
@@ -5972,686 +6204,2851 @@ impl Codegen {
5972
6204
  demoted
5973
6205
  }
5974
6206
 
5975
- /// Record every annotated `VarDecl`/param name → its native `RustType`, recursing through all
5976
- /// nested statements (loops, ifs, blocks, switch/try, function bodies). Flat; last write wins.
5977
- fn collect_annotated_types(
5978
- stmts: &[Statement],
5979
- aliases: &HashMap<String, RustType>,
5980
- env: &mut HashMap<String, RustType>,
5981
- ) {
5982
- for s in stmts {
5983
- match s {
5984
- Statement::VarDecl {
5985
- name,
5986
- type_ann: Some(ann),
5987
- ..
5988
- } => {
5989
- env.insert(
5990
- name.to_string(),
5991
- RustType::from_annotation_with_aliases(ann, aliases),
5992
- );
5993
- }
5994
- Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
5995
- Self::collect_annotated_types(statements, aliases, env)
5996
- }
5997
- Statement::If {
5998
- then_branch,
5999
- else_branch,
6000
- ..
6001
- } => {
6002
- Self::collect_annotated_types(std::slice::from_ref(then_branch), aliases, env);
6003
- if let Some(e) = else_branch {
6004
- Self::collect_annotated_types(std::slice::from_ref(e), aliases, env);
6005
- }
6006
- }
6007
- Statement::While { body, .. } | Statement::DoWhile { body, .. } => {
6008
- Self::collect_annotated_types(std::slice::from_ref(body), aliases, env)
6009
- }
6010
- Statement::ForOf {
6011
- name,
6012
- iterable,
6013
- body,
6014
- ..
6015
- } => {
6016
- // A loop var iterating a `Vec<elem>` local binds `elem` — so `total += n` (n the
6017
- // loop var over a `number[]`) is seen as native f64 and `total` is NOT demoted.
6018
- // Sound: the Vec's elements are genuinely that native type at runtime.
6019
- if let Expr::Ident { name: it_name, .. } = iterable {
6020
- let elem_ty = match env.get(it_name.as_ref()) {
6021
- Some(RustType::Vec(elem)) => Some((**elem).clone()),
6022
- _ => None,
6023
- };
6024
- if let Some(t) = elem_ty {
6025
- env.insert(name.to_string(), t);
6207
+ // ── Integer-range lattice (#174) ────────────────────────────────────────────────────────────
6208
+ //
6209
+ // Prove an `f64` expression always holds an integer within `(-2^53, 2^53)`, so it can be
6210
+ // computed in `i64` with a result BIT-IDENTICAL to the `f64` the interpreter/VM produce. The
6211
+ // immediate payoff is `x % c` → an integer remainder instead of `fmod` (fmod is ~5-10× slower);
6212
+ // the lattice is sound by construction — every rule preserves "integer-valued AND within the
6213
+ // exact-`f64` range", and any unprovable form yields `None` (treated as unbounded → no rewrite).
6214
+ //
6215
+ // The classic win is a `% c`-bounded recurrence (e.g. an LCG `seed = (seed*A + C) % M`): the
6216
+ // modulo caps the result to `[0, M-1]` regardless of the dividend's size, so the fixpoint
6217
+ // converges and every intermediate stays well under 2^53.
6218
+
6219
+ /// Prove `e` is always an integer in `[min, max]` (inclusive), both inside `(-2^53, 2^53)`.
6220
+ /// `ranges` supplies proven bounds for in-scope locals. `None` = unprovable / unbounded.
6221
+ fn int_range(
6222
+ &self,
6223
+ e: &Expr,
6224
+ ranges: &HashMap<String, (i64, i64)>,
6225
+ ) -> Option<(i64, i64)> {
6226
+ const LIM: i64 = 1 << 53;
6227
+ let clamp = |lo: i64, hi: i64| -> Option<(i64, i64)> {
6228
+ if lo <= hi && lo > -LIM && hi < LIM {
6229
+ Some((lo, hi))
6230
+ } else {
6231
+ None
6232
+ }
6233
+ };
6234
+ match e {
6235
+ Expr::Literal {
6236
+ value: Literal::Number(n),
6237
+ ..
6238
+ } => Self::int_literal_value(*n).and_then(|v| clamp(v, v)),
6239
+ Expr::Ident { name, .. } => ranges.get(name.as_ref()).copied(),
6240
+ Expr::Unary {
6241
+ op: UnaryOp::Neg,
6242
+ operand,
6243
+ ..
6244
+ } => {
6245
+ let (lo, hi) = self.int_range(operand, ranges)?;
6246
+ clamp(-hi, -lo)
6247
+ }
6248
+ Expr::Binary {
6249
+ left, op, right, ..
6250
+ } => match op {
6251
+ // Bitwise & shift always yield an int32 exact and far inside 2^53. A positive
6252
+ // literal `&`-mask tightens the upper bound (common: `h & 0xFF` → [0, 255]).
6253
+ BinOp::BitAnd => {
6254
+ let mask = Self::int_literal_value_of(left)
6255
+ .or_else(|| Self::int_literal_value_of(right));
6256
+ match mask {
6257
+ Some(m) if m >= 0 => clamp(0, m),
6258
+ _ => clamp(i32::MIN as i64, i32::MAX as i64),
6259
+ }
6260
+ }
6261
+ BinOp::BitOr | BinOp::BitXor | BinOp::Shl | BinOp::Shr => {
6262
+ clamp(i32::MIN as i64, i32::MAX as i64)
6263
+ }
6264
+ BinOp::UShr => clamp(0, u32::MAX as i64),
6265
+ // `x % c` (c a positive integer literal) — the result is an integer in (-c, c) when
6266
+ // the dividend is a proven integer; sign follows the dividend (JS `%` / Rust `%` both
6267
+ // truncate toward zero), so a non-negative dividend gives `[0, min(c-1, hi)]`.
6268
+ BinOp::Mod => {
6269
+ let c = Self::int_literal_value_of(right).filter(|&c| c > 0)?;
6270
+ // The dividend must be a proven INTEGER (so the result is integral); the modulo
6271
+ // then caps the magnitude to `< c` REGARDLESS of the dividend's size — a fixed
6272
+ // (not dividend-dependent) bound, so `% c`-driven recurrences converge in one
6273
+ // step. Sign follows the dividend (Rust `%` / JS `%` both truncate to zero).
6274
+ // The dividend is integral if it is range-bounded OR merely int-VALUED (e.g.
6275
+ // `r % 97` where `r` is a loop counter — integral but unbounded), the latter
6276
+ // giving the conservative two-sided `(-(c-1), c-1)`.
6277
+ if let Some((lo, _hi)) = self.int_range(left, ranges) {
6278
+ if lo >= 0 {
6279
+ clamp(0, c - 1)
6280
+ } else {
6281
+ clamp(-(c - 1), c - 1)
6026
6282
  }
6283
+ } else if self.expr_is_int_valued(left) {
6284
+ clamp(-(c - 1), c - 1)
6285
+ } else {
6286
+ None
6287
+ }
6288
+ }
6289
+ BinOp::Add => {
6290
+ let (la, ua) = self.int_range(left, ranges)?;
6291
+ let (lb, ub) = self.int_range(right, ranges)?;
6292
+ clamp(la + lb, ua + ub)
6293
+ }
6294
+ BinOp::Sub => {
6295
+ let (la, ua) = self.int_range(left, ranges)?;
6296
+ let (lb, ub) = self.int_range(right, ranges)?;
6297
+ clamp(la - ub, ua - lb)
6298
+ }
6299
+ BinOp::Mul => {
6300
+ let (la, ua) = self.int_range(left, ranges)?;
6301
+ let (lb, ub) = self.int_range(right, ranges)?;
6302
+ // Compute in i128 so corner products can't overflow before the 2^53 clamp.
6303
+ let p = [
6304
+ (la as i128) * (lb as i128),
6305
+ (la as i128) * (ub as i128),
6306
+ (ua as i128) * (lb as i128),
6307
+ (ua as i128) * (ub as i128),
6308
+ ];
6309
+ let lo = *p.iter().min().unwrap();
6310
+ let hi = *p.iter().max().unwrap();
6311
+ if lo > -(LIM as i128) && hi < (LIM as i128) {
6312
+ clamp(lo as i64, hi as i64)
6313
+ } else {
6314
+ None
6027
6315
  }
6028
- Self::collect_annotated_types(std::slice::from_ref(body), aliases, env)
6029
- }
6030
- Statement::For { init, body, .. } => {
6031
- if let Some(i) = init {
6032
- Self::collect_annotated_types(std::slice::from_ref(i), aliases, env);
6033
- }
6034
- Self::collect_annotated_types(std::slice::from_ref(body), aliases, env);
6035
6316
  }
6036
- Statement::FunDecl {
6037
- params,
6038
- rest_param,
6039
- body,
6040
- ..
6041
- } => {
6042
- for p in params {
6043
- if let FunParam::Simple(tp) = p {
6044
- if let Some(ann) = &tp.type_ann {
6045
- env.insert(
6046
- tp.name.to_string(),
6047
- RustType::from_annotation_with_aliases(ann, aliases),
6048
- );
6049
- }
6050
- }
6051
- }
6052
- // Typed rest-param `...args: number[]` -> `Vec<f64>`, so a ForOf loop var over it
6053
- // binds the element type and accumulators stay native.
6054
- if let Some(rp) = rest_param {
6055
- if let Some(ann) = &rp.type_ann {
6056
- env.insert(
6057
- rp.name.to_string(),
6058
- RustType::from_annotation_with_aliases(ann, aliases),
6059
- );
6317
+ _ => None,
6318
+ },
6319
+ _ => None,
6320
+ }
6321
+ }
6322
+
6323
+ /// An integer-valued, exactly-`f64`-representable number from a numeric literal value.
6324
+ fn int_literal_value(n: f64) -> Option<i64> {
6325
+ if n.is_finite() && n.fract() == 0.0 && n.abs() < (1i64 << 53) as f64 {
6326
+ Some(n as i64)
6327
+ } else {
6328
+ None
6329
+ }
6330
+ }
6331
+
6332
+ /// As [`int_literal_value`] but for an `Expr` that is a numeric literal (else `None`).
6333
+ fn int_literal_value_of(e: &Expr) -> Option<i64> {
6334
+ match e {
6335
+ Expr::Literal {
6336
+ value: Literal::Number(n),
6337
+ ..
6338
+ } => Self::int_literal_value(*n),
6339
+ _ => None,
6340
+ }
6341
+ }
6342
+
6343
+ /// Names of `f64` locals provably integer-bounded within `(-2^53, 2^53)` across the whole
6344
+ /// program. Seeds from integer-literal initializers and literal-bounded `for` counters, then
6345
+ /// runs a join fixpoint over reassignments: a local keeps a bound only if its init and EVERY
6346
+ /// reassignment RHS are `int_range`-provable and the joined range stabilizes within a few
6347
+ /// rounds (else it is dropped = unbounded). Sound: a dropped local simply keeps the `f64` path.
6348
+ fn collect_int_range_locals(&self, stmts: &[Statement]) -> HashMap<String, (i64, i64)> {
6349
+ let mut ranges: HashMap<String, (i64, i64)> = HashMap::new();
6350
+ // Seed: `let x = <int literal>` and `for (let i = <int>; i < <int>; i++/i+=1)` counters.
6351
+ Self::seed_int_ranges(stmts, &mut ranges);
6352
+ if ranges.is_empty() {
6353
+ return ranges;
6354
+ }
6355
+ // All reassignments `(name, rhs)` — a local is bounded only if every one stays provable.
6356
+ let mut reassigns: Vec<(String, &Expr)> = Vec::new();
6357
+ Self::collect_reassignments_stmts(stmts, &mut reassigns);
6358
+
6359
+ // Phase A — join rounds: grow each seeded local's range toward a fixpoint. A reassignment
6360
+ // whose RHS is unprovable drops the local immediately. With the modulo cap fixed, `% c`
6361
+ // recurrences converge in ≤2 rounds; the round cap just bounds non-converging growth (those
6362
+ // are caught by phase B).
6363
+ for _round in 0..8 {
6364
+ let mut changed = false;
6365
+ let snapshot = ranges.clone();
6366
+ for (name, rhs) in &reassigns {
6367
+ let Some(&(clo, chi)) = snapshot.get(name.as_str()) else {
6368
+ continue;
6369
+ };
6370
+ match self.int_range(rhs, &snapshot) {
6371
+ Some((rlo, rhi)) => {
6372
+ let (nlo, nhi) = (clo.min(rlo), chi.max(rhi));
6373
+ if (nlo, nhi) != (clo, chi) {
6374
+ ranges.insert(name.clone(), (nlo, nhi));
6375
+ changed = true;
6060
6376
  }
6061
6377
  }
6062
- Self::collect_annotated_types(std::slice::from_ref(body), aliases, env);
6063
- }
6064
- Statement::Switch {
6065
- cases,
6066
- default_body,
6067
- ..
6068
- } => {
6069
- for (_, body) in cases {
6070
- Self::collect_annotated_types(body, aliases, env);
6071
- }
6072
- if let Some(b) = default_body {
6073
- Self::collect_annotated_types(b, aliases, env);
6378
+ None => {
6379
+ ranges.remove(name.as_str());
6380
+ changed = true;
6074
6381
  }
6075
6382
  }
6076
- Statement::Try {
6077
- body,
6078
- catch_body,
6079
- finally_body,
6080
- ..
6081
- } => {
6082
- Self::collect_annotated_types(std::slice::from_ref(body), aliases, env);
6083
- if let Some(b) = catch_body {
6084
- Self::collect_annotated_types(std::slice::from_ref(b), aliases, env);
6085
- }
6086
- if let Some(b) = finally_body {
6087
- Self::collect_annotated_types(std::slice::from_ref(b), aliases, env);
6088
- }
6383
+ }
6384
+ if !changed {
6385
+ break;
6386
+ }
6387
+ }
6388
+ // Phase B — validate the result is an INDUCTIVE INVARIANT: a local keeps its range only if
6389
+ // every reassignment's RHS range (evaluated against the final map) stays within it. A local
6390
+ // that kept growing (e.g. `s = s + 1`, no cap) fails this and is dropped — and dropping it
6391
+ // can make other RHS unprovable, so iterate to a fixpoint. This is what makes the analysis
6392
+ // SOUND regardless of the round cap: only true fixpoints survive.
6393
+ loop {
6394
+ let mut dropped = false;
6395
+ let snapshot = ranges.clone();
6396
+ for (name, rhs) in &reassigns {
6397
+ let Some(&(clo, chi)) = snapshot.get(name.as_str()) else {
6398
+ continue;
6399
+ };
6400
+ let ok = matches!(self.int_range(rhs, &snapshot), Some((rlo, rhi)) if rlo >= clo && rhi <= chi);
6401
+ if !ok {
6402
+ ranges.remove(name.as_str());
6403
+ dropped = true;
6089
6404
  }
6090
- _ => {}
6405
+ }
6406
+ if !dropped {
6407
+ return ranges;
6091
6408
  }
6092
6409
  }
6093
6410
  }
6094
6411
 
6095
- fn collect_reassignments_stmts<'a>(stmts: &'a [Statement], out: &mut Vec<(String, &'a Expr)>) {
6096
- for s in stmts {
6097
- Self::collect_reassignments_stmt(s, out);
6412
+ // ── i32-loop-var lowering (bun/JSC-style integer-register hash accumulator) ─────────────────
6413
+ //
6414
+ // A `number` local `h` that (i) is declared `let h = <int literal>` immediately before a `for`,
6415
+ // (ii) is reassigned ONLY inside that loop by bitwise/shift expressions that lower fully in the
6416
+ // int32 domain, and (iii) whose every NUMERIC (non-bitwise) read happens where `h`'s JS value is
6417
+ // a *signed* int32 — can be kept in an `i32` register across the loop instead of round-tripping
6418
+ // `f64`↔`i32` on each op. The single excursion is an arithmetic node (`h * C`) that `int_range`
6419
+ // proves exceeds 2^53, so it stays `f64` (the multiply rounds in f64 *before* `ToUint32`, exactly
6420
+ // as V8 does). Soundness rests on:
6421
+ // • `int_range_locals` proving `h` is always an exact integer in (-2^53, 2^53) — the i32
6422
+ // register then holds precisely `ToInt32(h)`, and reads coerce `(h as f64)` = the signed
6423
+ // int32 value, while `>>> 0` boxings reinterpret the register as `u32`.
6424
+ // • the SIGNEDNESS pass below: after `^ & | << >>` `h` is signed-int32-valued; after `>>>`
6425
+ // (and at init, since the literal may exceed i32::MAX) it is uint32-valued. A *numeric* read
6426
+ // of `h` at a uint32-valued point would see the wrong sign → BAIL to the f64 path.
6427
+ // Anything unprovable bails → the existing f64 lowering, so this is purely additive.
6428
+
6429
+ /// `op` is a bitwise/shift operator (operands coerced to int32 by JS).
6430
+ fn is_bitwise_op(op: BinOp) -> bool {
6431
+ matches!(
6432
+ op,
6433
+ BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor | BinOp::Shl | BinOp::Shr | BinOp::UShr
6434
+ )
6435
+ }
6436
+
6437
+ /// `e` lowers FULLY in the int32 domain (top node bitwise/shift; every leaf is either a numeric
6438
+ /// literal, the loop var `h`, or an arithmetic/`Mod` subtree of int-provable numbers that becomes
6439
+ /// a single `f64` excursion re-narrowed by `to_int32`). Mirrors what `emit_int32_operand` will
6440
+ /// actually emit, so a `true` here means the reassignment really lowers without a per-op round
6441
+ /// trip. Conservative: unknown forms (calls, member access, etc.) → false.
6442
+ fn i32_chain_lowerable(&self, e: &Expr, var: &str) -> bool {
6443
+ match e {
6444
+ Expr::Binary { left, op, right, .. } if Self::is_bitwise_op(*op) => {
6445
+ self.i32_chain_lowerable(left, var) && self.i32_chain_lowerable(right, var)
6446
+ }
6447
+ // A non-bitwise node is a LEAF in the int32 chain: it must lower to a plain `f64` that
6448
+ // `to_int32` then narrows. Require it provably integer-valued (so the f64 is exact) —
6449
+ // either the var itself, an int literal, or an int-range/int-valued arithmetic subtree.
6450
+ _ => self.i32_leaf_is_f64(e, var),
6098
6451
  }
6099
6452
  }
6100
6453
 
6101
- /// Collect every `(name, rhs)` reassignment (`=`, compound `+=`, logical `||=`) reachable from
6102
- /// `s` descending through nested statements and expressions (including closures).
6103
- fn collect_reassignments_stmt<'a>(s: &'a Statement, out: &mut Vec<(String, &'a Expr)>) {
6104
- match s {
6105
- Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
6106
- Self::collect_reassignments_stmts(statements, out)
6454
+ /// An int32-chain LEAF that provably emits a plain `f64`: the loop var, an integer literal, or a
6455
+ /// `+ - * % / **`-arithmetic / unary subtree over numbers proven integer-valued (so `as f64` is
6456
+ /// exact and `to_int32` recovers the bit-pattern). Bitwise sub-nodes are handled by the caller.
6457
+ fn i32_leaf_is_f64(&self, e: &Expr, var: &str) -> bool {
6458
+ match e {
6459
+ Expr::Ident { name, .. } => name.as_ref() == var || self.expr_is_int_valued(e),
6460
+ Expr::Literal { value: Literal::Number(n), .. } => {
6461
+ n.is_finite() && n.fract() == 0.0
6107
6462
  }
6108
- Statement::VarDecl { init: Some(e), .. } => Self::collect_reassignments_expr(e, out),
6109
- Statement::VarDeclDestructure { init, .. } => {
6110
- Self::collect_reassignments_expr(init, out)
6463
+ Expr::Unary { op: UnaryOp::Neg | UnaryOp::BitNot, operand, .. } => {
6464
+ self.i32_leaf_is_f64(operand, var)
6111
6465
  }
6112
- Statement::ExprStmt { expr, .. } => Self::collect_reassignments_expr(expr, out),
6113
- Statement::If {
6114
- cond,
6115
- then_branch,
6116
- else_branch,
6117
- ..
6118
- } => {
6119
- Self::collect_reassignments_expr(cond, out);
6120
- Self::collect_reassignments_stmt(then_branch, out);
6121
- if let Some(e) = else_branch {
6122
- Self::collect_reassignments_stmt(e, out);
6466
+ Expr::Binary { left, op, right, .. } => match op {
6467
+ BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Mod => {
6468
+ self.i32_leaf_is_f64(left, var) && self.i32_leaf_is_f64(right, var)
6123
6469
  }
6470
+ op if Self::is_bitwise_op(*op) => {
6471
+ self.i32_chain_lowerable(left, var) && self.i32_chain_lowerable(right, var)
6472
+ }
6473
+ _ => false,
6474
+ },
6475
+ _ => false,
6476
+ }
6477
+ }
6478
+
6479
+ /// `e` provably evaluates to a FINITE f64 with `|e| < 2^62`, so `to_int32_unchecked` (no
6480
+ /// `is_finite` guard, no saturating cast) is sound for it. Handled shapes: an `I32`-register read
6481
+ /// (`|x| < 2^31`), a finite numeric literal, unary `-`, and `+ - *` over such operands (bounds
6482
+ /// combined; any branch unprovable ⇒ `None`). Bitwise/shift sub-nodes are NOT leaves here, so we
6483
+ /// don't descend into them (the caller's `to_int32`/`to_uint32` already bound those to 32 bits).
6484
+ fn f64_finite_bounded_below_2pow62(&self, e: &Expr) -> bool {
6485
+ self.f64_abs_bound(e).is_some_and(|b| b < 4.611686018427388e18) // 2^62
6486
+ }
6487
+
6488
+ /// Conservative magnitude bound for [`f64_finite_bounded_below_2pow62`]; `None` if not provable.
6489
+ fn f64_abs_bound(&self, e: &Expr) -> Option<f64> {
6490
+ match e {
6491
+ // An i32-register accumulator: its magnitude is `< 2^31`.
6492
+ Expr::Ident { name, .. }
6493
+ if self.type_context.get_type(name.as_ref()) == RustType::I32 =>
6494
+ {
6495
+ Some(2147483648.0) // 2^31
6124
6496
  }
6125
- Statement::While { cond, body, .. } => {
6126
- Self::collect_reassignments_expr(cond, out);
6127
- Self::collect_reassignments_stmt(body, out);
6128
- }
6129
- Statement::DoWhile { body, cond, .. } => {
6130
- Self::collect_reassignments_stmt(body, out);
6131
- Self::collect_reassignments_expr(cond, out);
6132
- }
6133
- Statement::For {
6134
- init,
6135
- cond,
6136
- update,
6137
- body,
6138
- ..
6139
- } => {
6140
- if let Some(i) = init {
6141
- Self::collect_reassignments_stmt(i, out);
6497
+ Expr::Literal { value: Literal::Number(n), .. } if n.is_finite() => Some(n.abs()),
6498
+ Expr::Unary { op: UnaryOp::Neg, operand, .. } => self.f64_abs_bound(operand),
6499
+ Expr::Binary { left, op, right, .. } => {
6500
+ let la = self.f64_abs_bound(left)?;
6501
+ let ra = self.f64_abs_bound(right)?;
6502
+ match op {
6503
+ BinOp::Add | BinOp::Sub => Some(la + ra),
6504
+ BinOp::Mul => Some(la * ra),
6505
+ _ => None,
6142
6506
  }
6143
- if let Some(c) = cond {
6144
- Self::collect_reassignments_expr(c, out);
6507
+ }
6508
+ _ => None,
6509
+ }
6510
+ }
6511
+
6512
+ /// Walk `e` and decide whether `var` is read SAFELY given it is currently `signed`-int32-valued
6513
+ /// (`signed == false` ⇒ uint32-valued). A read of `var` directly under a bitwise/shift op is a
6514
+ /// register read (always safe); a read of `var` in any *numeric* position is safe only while
6515
+ /// `signed`. `bitwise_parent` tracks whether the immediate parent op is bitwise/shift. Returns
6516
+ /// `false` (bail) if any numeric read happens while not `signed`.
6517
+ fn i32_reads_ok(e: &Expr, var: &str, signed: bool, bitwise_parent: bool) -> bool {
6518
+ match e {
6519
+ Expr::Ident { name, .. } if name.as_ref() == var => bitwise_parent || signed,
6520
+ Expr::Binary { left, op, right, .. } => {
6521
+ let bw = Self::is_bitwise_op(*op);
6522
+ Self::i32_reads_ok(left, var, signed, bw)
6523
+ && Self::i32_reads_ok(right, var, signed, bw)
6524
+ }
6525
+ Expr::Unary { operand, .. } => Self::i32_reads_ok(operand, var, signed, false),
6526
+ _ => {
6527
+ // Any other read of `var` (call arg, member, index, ternary, …) is a numeric/opaque
6528
+ // use: only bitwise-parent or signed positions pass; otherwise bail if it mentions
6529
+ // `var` at all (conservative — we can't track signedness through opaque forms).
6530
+ if Self::collect_idents_of(e).contains(var) {
6531
+ bitwise_parent || signed
6532
+ } else {
6533
+ true
6145
6534
  }
6146
- if let Some(u) = update {
6147
- Self::collect_reassignments_expr(u, out);
6535
+ }
6536
+ }
6537
+ }
6538
+
6539
+ fn collect_idents_of(e: &Expr) -> HashSet<String> {
6540
+ let mut idents = HashSet::new();
6541
+ Self::collect_expr_idents(e, &mut idents);
6542
+ idents
6543
+ }
6544
+
6545
+ /// EVERY read of `var` outside its own body-assignment RHSs must be a register (bitwise) read —
6546
+ /// e.g. the final `return h >>> 0`. Body-assignment RHSs (`var = <rhs>`) are vetted by the
6547
+ /// ordered signedness pass, so this walker SKIPS the RHS of an assignment whose target is `var`,
6548
+ /// and rejects any other numeric (non-bitwise) read of `var` anywhere in `stmts`.
6549
+ fn i32_only_bitwise_reads_outside_assigns(stmts: &[Statement], var: &str) -> bool {
6550
+ stmts
6551
+ .iter()
6552
+ .all(|s| Self::i32_external_reads_ok_stmt(s, var))
6553
+ }
6554
+
6555
+ fn i32_external_reads_ok_stmt(s: &Statement, var: &str) -> bool {
6556
+ let mut ok = true;
6557
+ Self::for_each_stmt_expr(s, &mut |e| {
6558
+ if !ok {
6559
+ return;
6560
+ }
6561
+ ok &= Self::i32_external_reads_ok_expr(e, var, false);
6562
+ });
6563
+ ok
6564
+ }
6565
+
6566
+ /// As `i32_reads_ok` with `signed = false` (the strictest state), but a `var = <rhs>` assignment
6567
+ /// node has its RHS reads SKIPPED — those are the loop assignments, vetted by the ordered pass.
6568
+ fn i32_external_reads_ok_expr(e: &Expr, var: &str, bitwise_parent: bool) -> bool {
6569
+ match e {
6570
+ // The write target name is not a read; its RHS is vetted by the ordered signedness pass.
6571
+ Expr::Assign { name, value, .. } if name.as_ref() == var => {
6572
+ // The RHS may itself contain *nested* assigns to OTHER vars referencing `var`, but
6573
+ // those would have failed the "single-writer" check; the RHS `var` reads are the
6574
+ // ordered-pass's job, so don't re-check them here.
6575
+ let _ = value;
6576
+ true
6577
+ }
6578
+ Expr::Ident { name, .. } if name.as_ref() == var => bitwise_parent,
6579
+ Expr::Binary { left, op, right, .. } => {
6580
+ let bw = Self::is_bitwise_op(*op);
6581
+ Self::i32_external_reads_ok_expr(left, var, bw)
6582
+ && Self::i32_external_reads_ok_expr(right, var, bw)
6583
+ }
6584
+ Expr::Unary { operand, .. } => Self::i32_external_reads_ok_expr(operand, var, false),
6585
+ _ => {
6586
+ if Self::collect_idents_of(e).contains(var) {
6587
+ bitwise_parent
6588
+ } else {
6589
+ true
6148
6590
  }
6149
- Self::collect_reassignments_stmt(body, out);
6150
6591
  }
6151
- Statement::ForOf { iterable, body, .. } => {
6152
- Self::collect_reassignments_expr(iterable, out);
6153
- Self::collect_reassignments_stmt(body, out);
6592
+ }
6593
+ }
6594
+
6595
+ /// Collect every `number` accumulator eligible for i32-register loop lowering. Scans every
6596
+ /// statement list (top level + nested blocks/loops/fn bodies); the eligibility gate itself uses
6597
+ /// the whole-program (name-keyed) reassignment set, so a name with any writer outside its loop
6598
+ /// body bails. Soundness is per-name, not per-scope, which the strict gate guarantees.
6599
+ fn collect_i32_loop_vars(&self, stmts: &[Statement]) -> HashSet<String> {
6600
+ let mut out = HashSet::new();
6601
+ self.collect_i32_loop_vars_in(stmts, stmts, &mut out);
6602
+ out
6603
+ }
6604
+
6605
+ /// `stmts` is the statement list currently being scanned for the decl-then-`for` pattern;
6606
+ /// `root` is the whole program, used by the gate's whole-program writer/reader checks.
6607
+ fn collect_i32_loop_vars_in(
6608
+ &self,
6609
+ stmts: &[Statement],
6610
+ root: &[Statement],
6611
+ out: &mut HashSet<String>,
6612
+ ) {
6613
+ // `let h = <int>` directly followed by a `for` whose body reassigns `h`.
6614
+ for win in stmts.windows(2) {
6615
+ if let (
6616
+ Statement::VarDecl {
6617
+ name,
6618
+ mutable: true,
6619
+ init: Some(init),
6620
+ ..
6621
+ },
6622
+ Statement::For { body, .. },
6623
+ ) = (&win[0], &win[1])
6624
+ {
6625
+ if Self::int_literal_value_of(init).is_some()
6626
+ && self.i32_loop_var_eligible(name.as_ref(), body, root)
6627
+ {
6628
+ out.insert(name.to_string());
6629
+ }
6154
6630
  }
6155
- Statement::Return { value: Some(e), .. } => Self::collect_reassignments_expr(e, out),
6156
- Statement::Throw { value, .. } => Self::collect_reassignments_expr(value, out),
6157
- Statement::FunDecl { body, .. } => Self::collect_reassignments_stmt(body, out),
6158
- Statement::Switch {
6159
- expr,
6160
- cases,
6161
- default_body,
6162
- ..
6163
- } => {
6164
- Self::collect_reassignments_expr(expr, out);
6165
- for (g, body) in cases {
6166
- if let Some(g) = g {
6167
- Self::collect_reassignments_expr(g, out);
6631
+ }
6632
+ // Recurse into nested statement lists (each block / fn body / loop body is scanned).
6633
+ for s in stmts {
6634
+ Self::for_each_child_stmt_list(s, &mut |list| {
6635
+ self.collect_i32_loop_vars_in(list, root, out)
6636
+ });
6637
+ }
6638
+ }
6639
+
6640
+ /// Eligibility gate for the i32-register lowering of `var`, declared just before `for (…) body`.
6641
+ /// All bail conditions keep the existing f64 path (purely additive). `var` qualifies iff:
6642
+ /// (a) `int_range` proves it always holds an exact integer in (-2^53, 2^53);
6643
+ /// (b) it is not closure-captured into a cell;
6644
+ /// (c) it is written ONLY by the assignments inside `body`, each a bitwise/shift expr that
6645
+ /// lowers fully in the int32 domain;
6646
+ /// (d) the forward signedness pass over those assignments admits every numeric read of `var`;
6647
+ /// (e) every read of `var` OUTSIDE those assignment RHSs is a register (bitwise) read.
6648
+ fn i32_loop_var_eligible(&self, var: &str, body: &Statement, root: &[Statement]) -> bool {
6649
+ // (a)
6650
+ if !self.int_range_locals.contains_key(var) {
6651
+ return false;
6652
+ }
6653
+ // (b)
6654
+ if self.refcell_wrapped_vars.contains(var) {
6655
+ return false;
6656
+ }
6657
+ // (c) reassignments to `var` inside the loop body, in source order.
6658
+ let mut body_assigns: Vec<&Expr> = Vec::new();
6659
+ Self::collect_ordered_assigns_to(body, var, &mut body_assigns);
6660
+ if body_assigns.is_empty() {
6661
+ return false;
6662
+ }
6663
+ for rhs in &body_assigns {
6664
+ let top_bitwise = matches!(rhs, Expr::Binary { op, .. } if Self::is_bitwise_op(*op));
6665
+ if !top_bitwise || !self.i32_chain_lowerable(rhs, var) {
6666
+ return false;
6667
+ }
6668
+ }
6669
+ // `var` must have NO writer outside this loop body — whole-program count must match.
6670
+ let mut all_assigns: Vec<(String, &Expr)> = Vec::new();
6671
+ Self::collect_reassignments_stmts(root, &mut all_assigns);
6672
+ let total_writes = all_assigns.iter().filter(|(n, _)| n == var).count();
6673
+ if total_writes != body_assigns.len() {
6674
+ return false;
6675
+ }
6676
+ // (d) SIGNEDNESS pass. Init value may exceed i32::MAX ⇒ start uint32-valued. Each RHS is read
6677
+ // against the CURRENT signedness; new signedness follows the top op (`>>>` → unsigned).
6678
+ let mut signed = false;
6679
+ for rhs in &body_assigns {
6680
+ if !Self::i32_reads_ok(rhs, var, signed, false) {
6681
+ return false;
6682
+ }
6683
+ signed = !matches!(rhs, Expr::Binary { op: BinOp::UShr, .. });
6684
+ }
6685
+ // (e) Every other read of `var` in the program must be a register (bitwise) read.
6686
+ if !Self::i32_only_bitwise_reads_outside_assigns(root, var) {
6687
+ return false;
6688
+ }
6689
+ true
6690
+ }
6691
+
6692
+ /// Collect, in source order, the RHS of every top-level `var = <rhs>` assignment to `var`
6693
+ /// reachable in `body` (descending blocks/if/loops but NOT into nested fn bodies).
6694
+ fn collect_ordered_assigns_to<'a>(s: &'a Statement, var: &str, out: &mut Vec<&'a Expr>) {
6695
+ match s {
6696
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
6697
+ for st in statements {
6698
+ Self::collect_ordered_assigns_to(st, var, out);
6699
+ }
6700
+ }
6701
+ Statement::ExprStmt { expr, .. } => {
6702
+ if let Expr::Assign { name, value, .. } = expr {
6703
+ if name.as_ref() == var {
6704
+ out.push(value.as_ref());
6168
6705
  }
6169
- Self::collect_reassignments_stmts(body, out);
6706
+ }
6707
+ }
6708
+ Statement::If { then_branch, else_branch, .. } => {
6709
+ Self::collect_ordered_assigns_to(then_branch, var, out);
6710
+ if let Some(e) = else_branch {
6711
+ Self::collect_ordered_assigns_to(e, var, out);
6712
+ }
6713
+ }
6714
+ Statement::For { body, .. }
6715
+ | Statement::ForOf { body, .. }
6716
+ | Statement::While { body, .. }
6717
+ | Statement::DoWhile { body, .. } => {
6718
+ Self::collect_ordered_assigns_to(body, var, out)
6719
+ }
6720
+ _ => {}
6721
+ }
6722
+ }
6723
+
6724
+ /// Invoke `f` with every nested *statement list* directly reachable from `s` (blocks, `if`
6725
+ /// branches, loop bodies, fn bodies). Used to scan each lexical scope for the decl-then-`for`
6726
+ /// pattern. Branch/loop bodies are single `Statement`s, passed as 1-element slices.
6727
+ fn for_each_child_stmt_list(s: &Statement, f: &mut dyn FnMut(&[Statement])) {
6728
+ match s {
6729
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
6730
+ f(statements)
6731
+ }
6732
+ Statement::If { then_branch, else_branch, .. } => {
6733
+ f(std::slice::from_ref(then_branch));
6734
+ if let Some(e) = else_branch {
6735
+ f(std::slice::from_ref(e));
6736
+ }
6737
+ }
6738
+ Statement::For { body, .. }
6739
+ | Statement::ForOf { body, .. }
6740
+ | Statement::While { body, .. }
6741
+ | Statement::DoWhile { body, .. }
6742
+ | Statement::FunDecl { body, .. } => f(std::slice::from_ref(body)),
6743
+ Statement::Switch { cases, default_body, .. } => {
6744
+ for (_, body) in cases {
6745
+ f(body);
6170
6746
  }
6171
6747
  if let Some(b) = default_body {
6172
- Self::collect_reassignments_stmts(b, out);
6748
+ f(b);
6173
6749
  }
6174
6750
  }
6175
- Statement::Try {
6176
- body,
6177
- catch_body,
6178
- finally_body,
6179
- ..
6180
- } => {
6181
- Self::collect_reassignments_stmt(body, out);
6751
+ Statement::Try { body, catch_body, finally_body, .. } => {
6752
+ f(std::slice::from_ref(body));
6182
6753
  if let Some(b) = catch_body {
6183
- Self::collect_reassignments_stmt(b, out);
6754
+ f(std::slice::from_ref(b));
6184
6755
  }
6185
6756
  if let Some(b) = finally_body {
6186
- Self::collect_reassignments_stmt(b, out);
6757
+ f(std::slice::from_ref(b));
6187
6758
  }
6188
6759
  }
6189
6760
  _ => {}
6190
6761
  }
6191
6762
  }
6192
6763
 
6193
- fn collect_reassignments_expr<'a>(e: &'a Expr, out: &mut Vec<(String, &'a Expr)>) {
6194
- match e {
6195
- Expr::Assign { name, value, .. }
6196
- | Expr::CompoundAssign { name, value, .. }
6197
- | Expr::LogicalAssign { name, value, .. } => {
6198
- out.push((name.to_string(), value.as_ref()));
6199
- Self::collect_reassignments_expr(value, out);
6200
- }
6201
- Expr::Binary { left, right, .. } | Expr::NullishCoalesce { left, right, .. } => {
6202
- Self::collect_reassignments_expr(left, out);
6203
- Self::collect_reassignments_expr(right, out);
6204
- }
6205
- Expr::Unary { operand, .. }
6206
- | Expr::TypeOf { operand, .. }
6207
- | Expr::Await { operand, .. } => Self::collect_reassignments_expr(operand, out),
6208
- Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
6209
- Self::collect_reassignments_expr(callee, out);
6210
- for a in args {
6211
- match a {
6212
- CallArg::Expr(x) | CallArg::Spread(x) => {
6213
- Self::collect_reassignments_expr(x, out)
6214
- }
6215
- }
6764
+ /// Invoke `f` on every top-level expression of `s`, recursing through nested control-flow
6765
+ /// statements (blocks, if, loops, switch, try, return/throw). `f` is responsible for recursing
6766
+ /// into each expression's own subtree. Does NOT descend into nested fn-decl bodies (a different
6767
+ /// lexical scope; a captured loop var would be RefCell-bailed before reaching here).
6768
+ fn for_each_stmt_expr(s: &Statement, f: &mut dyn FnMut(&Expr)) {
6769
+ match s {
6770
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
6771
+ for st in statements {
6772
+ Self::for_each_stmt_expr(st, f);
6216
6773
  }
6217
6774
  }
6218
- Expr::Member { object, prop, .. } => {
6219
- Self::collect_reassignments_expr(object, out);
6220
- if let MemberProp::Expr(p) = prop {
6221
- Self::collect_reassignments_expr(p, out);
6775
+ Statement::VarDecl { init: Some(e), .. } => f(e),
6776
+ Statement::VarDeclDestructure { init, .. } => f(init),
6777
+ Statement::ExprStmt { expr, .. } => f(expr),
6778
+ Statement::If { cond, then_branch, else_branch, .. } => {
6779
+ f(cond);
6780
+ Self::for_each_stmt_expr(then_branch, f);
6781
+ if let Some(e) = else_branch {
6782
+ Self::for_each_stmt_expr(e, f);
6222
6783
  }
6223
6784
  }
6224
- Expr::Index { object, index, .. } => {
6225
- Self::collect_reassignments_expr(object, out);
6226
- Self::collect_reassignments_expr(index, out);
6785
+ Statement::While { cond, body, .. } => {
6786
+ f(cond);
6787
+ Self::for_each_stmt_expr(body, f);
6227
6788
  }
6228
- Expr::Conditional {
6229
- cond,
6230
- then_branch,
6231
- else_branch,
6232
- ..
6233
- } => {
6234
- Self::collect_reassignments_expr(cond, out);
6235
- Self::collect_reassignments_expr(then_branch, out);
6236
- Self::collect_reassignments_expr(else_branch, out);
6789
+ Statement::DoWhile { body, cond, .. } => {
6790
+ Self::for_each_stmt_expr(body, f);
6791
+ f(cond);
6237
6792
  }
6238
- Expr::Array { elements, .. } => {
6239
- for el in elements {
6240
- match el {
6241
- ArrayElement::Expr(x) | ArrayElement::Spread(x) => {
6242
- Self::collect_reassignments_expr(x, out)
6243
- }
6244
- }
6793
+ Statement::For { init, cond, update, body, .. } => {
6794
+ if let Some(i) = init {
6795
+ Self::for_each_stmt_expr(i, f);
6245
6796
  }
6246
- }
6247
- Expr::Object { props, .. } => {
6248
- for p in props {
6249
- match p {
6250
- ObjectProp::KeyValue(_, v) => Self::collect_reassignments_expr(v, out),
6251
- ObjectProp::Spread(x) => Self::collect_reassignments_expr(x, out),
6252
- }
6797
+ if let Some(c) = cond {
6798
+ f(c);
6799
+ }
6800
+ if let Some(u) = update {
6801
+ f(u);
6253
6802
  }
6803
+ Self::for_each_stmt_expr(body, f);
6254
6804
  }
6255
- Expr::MemberAssign { object, value, .. } => {
6256
- Self::collect_reassignments_expr(object, out);
6257
- Self::collect_reassignments_expr(value, out);
6805
+ Statement::ForOf { iterable, body, .. } => {
6806
+ f(iterable);
6807
+ Self::for_each_stmt_expr(body, f);
6258
6808
  }
6259
- Expr::IndexAssign {
6260
- object,
6261
- index,
6262
- value,
6263
- ..
6264
- } => {
6265
- Self::collect_reassignments_expr(object, out);
6266
- Self::collect_reassignments_expr(index, out);
6267
- Self::collect_reassignments_expr(value, out);
6809
+ Statement::Return { value: Some(e), .. } => f(e),
6810
+ Statement::Throw { value, .. } => f(value),
6811
+ Statement::Switch { expr, cases, default_body, .. } => {
6812
+ f(expr);
6813
+ for (g, body) in cases {
6814
+ if let Some(g) = g {
6815
+ f(g);
6816
+ }
6817
+ for st in body {
6818
+ Self::for_each_stmt_expr(st, f);
6819
+ }
6820
+ }
6821
+ if let Some(b) = default_body {
6822
+ for st in b {
6823
+ Self::for_each_stmt_expr(st, f);
6824
+ }
6825
+ }
6268
6826
  }
6269
- Expr::TemplateLiteral { exprs, .. } => {
6270
- for x in exprs {
6271
- Self::collect_reassignments_expr(x, out);
6827
+ Statement::Try { body, catch_body, finally_body, .. } => {
6828
+ Self::for_each_stmt_expr(body, f);
6829
+ if let Some(b) = catch_body {
6830
+ Self::for_each_stmt_expr(b, f);
6831
+ }
6832
+ if let Some(b) = finally_body {
6833
+ Self::for_each_stmt_expr(b, f);
6272
6834
  }
6273
6835
  }
6274
- Expr::ArrowFunction { body, .. } => match body {
6275
- ArrowBody::Expr(x) => Self::collect_reassignments_expr(x, out),
6276
- ArrowBody::Block(b) => Self::collect_reassignments_stmt(b, out),
6277
- },
6836
+ // A nested `function f(){…}` body is a separate scope: a loop var read there would be a
6837
+ // capture (RefCell-bailed) or a shadow (different binding) — don't descend.
6278
6838
  _ => {}
6279
6839
  }
6280
6840
  }
6281
6841
 
6282
- /// Read-only mirror of `emit_typed_expr`'s native-type decision (no code generated), over a
6283
- /// flat `name RustType` env. Returns `RustType::F64` only for forms that provably lower to a
6284
- /// native `f64`; everything else → `RustType::Value`. Conservative by construction: it never
6285
- /// claims `F64` where `emit_typed_expr` would box, so a numeric local is never wrongly kept
6286
- /// native (which would reintroduce the coercion panic).
6287
- fn expr_native_type(&self, e: &Expr, env: &HashMap<String, RustType>) -> RustType {
6288
- match e {
6289
- Expr::Literal { value, .. } => match value {
6290
- Literal::Number(_) => RustType::F64,
6291
- Literal::String(_) => RustType::String,
6292
- Literal::Bool(_) => RustType::Bool,
6293
- Literal::Null => RustType::Value,
6294
- },
6295
- Expr::Ident { name, .. } => env
6296
- .get(name.as_ref())
6297
- .filter(|t| t.is_native())
6298
- .cloned()
6299
- .unwrap_or(RustType::Value),
6842
+ /// Seed integer ranges: integer-literal `let` initializers and literal-bounded `for` counters.
6843
+ fn seed_int_ranges(stmts: &[Statement], out: &mut HashMap<String, (i64, i64)>) {
6844
+ for s in stmts {
6845
+ match s {
6846
+ Statement::VarDecl {
6847
+ name,
6848
+ init: Some(e),
6849
+ ..
6850
+ } => {
6851
+ if let Some(v) = Self::int_literal_value_of(e) {
6852
+ out.insert(name.to_string(), (v, v));
6853
+ }
6854
+ }
6855
+ Statement::For {
6856
+ init, cond, body, ..
6857
+ } => {
6858
+ // `for (let i = <int>; i < <int>; ...)` → counter `i` ∈ [start, end-1].
6859
+ if let (
6860
+ Some(Statement::VarDecl {
6861
+ name,
6862
+ init: Some(istart),
6863
+ ..
6864
+ }),
6865
+ Some(Expr::Binary {
6866
+ left,
6867
+ op: BinOp::Lt,
6868
+ right,
6869
+ ..
6870
+ }),
6871
+ ) = (init.as_deref(), cond.as_ref())
6872
+ {
6873
+ if let (Some(start), Some(end)) = (
6874
+ Self::int_literal_value_of(istart),
6875
+ Self::int_literal_value_of(right),
6876
+ ) {
6877
+ if matches!(left.as_ref(), Expr::Ident { name: cn, .. } if cn.as_ref() == name.as_ref())
6878
+ && end > start
6879
+ && end - 1 < (1i64 << 53)
6880
+ {
6881
+ out.insert(name.to_string(), (start, end - 1));
6882
+ }
6883
+ }
6884
+ }
6885
+ Self::seed_int_ranges(std::slice::from_ref(body), out);
6886
+ }
6887
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
6888
+ Self::seed_int_ranges(statements, out)
6889
+ }
6890
+ Statement::If {
6891
+ then_branch,
6892
+ else_branch,
6893
+ ..
6894
+ } => {
6895
+ Self::seed_int_ranges(std::slice::from_ref(then_branch), out);
6896
+ if let Some(e) = else_branch {
6897
+ Self::seed_int_ranges(std::slice::from_ref(e), out);
6898
+ }
6899
+ }
6900
+ Statement::While { body, .. } | Statement::DoWhile { body, .. } => {
6901
+ Self::seed_int_ranges(std::slice::from_ref(body), out)
6902
+ }
6903
+ Statement::FunDecl { body, .. } => Self::seed_int_ranges(std::slice::from_ref(body), out),
6904
+ _ => {}
6905
+ }
6906
+ }
6907
+ }
6908
+
6909
+ /// `e` is provably INTEGER-valued (zero fractional part at runtime), per `set` for locals.
6910
+ /// Closed under `+ - * %` (modulo by a positive integer literal), unary `- ~`, bitwise/shift,
6911
+ /// and integer literals — so a loop counter (`0`, then `i + 1`) stays integral. Magnitude is
6912
+ /// NOT tracked (that is `int_range`'s job); this only certifies integrality.
6913
+ fn is_int_valued(e: &Expr, set: &HashSet<String>) -> bool {
6914
+ match e {
6915
+ Expr::Literal {
6916
+ value: Literal::Number(n),
6917
+ ..
6918
+ } => n.is_finite() && n.fract() == 0.0,
6919
+ Expr::Ident { name, .. } => set.contains(name.as_ref()),
6920
+ Expr::Unary {
6921
+ op: UnaryOp::Neg | UnaryOp::BitNot,
6922
+ operand,
6923
+ ..
6924
+ } => Self::is_int_valued(operand, set),
6300
6925
  Expr::Binary {
6301
6926
  left, op, right, ..
6927
+ } => match op {
6928
+ BinOp::Add | BinOp::Sub | BinOp::Mul => {
6929
+ Self::is_int_valued(left, set) && Self::is_int_valued(right, set)
6930
+ }
6931
+ BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor | BinOp::Shl | BinOp::Shr
6932
+ | BinOp::UShr => true,
6933
+ BinOp::Mod => {
6934
+ Self::int_literal_value_of(right).is_some_and(|c| c != 0)
6935
+ && Self::is_int_valued(left, set)
6936
+ }
6937
+ _ => false,
6938
+ },
6939
+ _ => false,
6940
+ }
6941
+ }
6942
+
6943
+ /// `self`-bound [`is_int_valued`] against the computed `int_valued_locals`.
6944
+ fn expr_is_int_valued(&self, e: &Expr) -> bool {
6945
+ Self::is_int_valued(e, &self.int_valued_locals)
6946
+ }
6947
+
6948
+ /// Locals that are always integer-valued. Greatest-fixpoint: assume every `let` local is
6949
+ /// integral, then drop any whose initializer or any reassignment RHS is not `is_int_valued`
6950
+ /// under the current set, until stable. Sound: dropping only ever removes names, and `+ - * %`
6951
+ /// preserve integrality even past 2^53 (the f64 result still has zero fractional part).
6952
+ fn collect_int_valued_locals(stmts: &[Statement]) -> HashSet<String> {
6953
+ // All declared local names (candidates).
6954
+ let mut names: HashSet<String> = HashSet::new();
6955
+ Self::collect_local_decl_names(stmts, &mut names);
6956
+ // Init/reassignment expressions per name.
6957
+ let mut defs: Vec<(String, &Expr)> = Vec::new();
6958
+ Self::collect_int_valued_defs(stmts, &mut defs);
6959
+ let mut reassigns: Vec<(String, &Expr)> = Vec::new();
6960
+ Self::collect_reassignments_stmts(stmts, &mut reassigns);
6961
+ loop {
6962
+ let mut changed = false;
6963
+ for (name, e) in defs.iter().chain(reassigns.iter()) {
6964
+ if names.contains(name.as_str()) && !Self::is_int_valued(e, &names) {
6965
+ names.remove(name.as_str());
6966
+ changed = true;
6967
+ }
6968
+ }
6969
+ if !changed {
6970
+ return names;
6971
+ }
6972
+ }
6973
+ }
6974
+
6975
+ /// Every `let`-declared local name (recursing through all nested statements).
6976
+ fn collect_local_decl_names(stmts: &[Statement], out: &mut HashSet<String>) {
6977
+ for s in stmts {
6978
+ match s {
6979
+ Statement::VarDecl { name, .. } => {
6980
+ out.insert(name.to_string());
6981
+ }
6982
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
6983
+ Self::collect_local_decl_names(statements, out)
6984
+ }
6985
+ Statement::If {
6986
+ then_branch,
6987
+ else_branch,
6988
+ ..
6989
+ } => {
6990
+ Self::collect_local_decl_names(std::slice::from_ref(then_branch), out);
6991
+ if let Some(e) = else_branch {
6992
+ Self::collect_local_decl_names(std::slice::from_ref(e), out);
6993
+ }
6994
+ }
6995
+ Statement::While { body, .. } | Statement::DoWhile { body, .. } => {
6996
+ Self::collect_local_decl_names(std::slice::from_ref(body), out)
6997
+ }
6998
+ Statement::For { init, body, .. } => {
6999
+ if let Some(i) = init {
7000
+ Self::collect_local_decl_names(std::slice::from_ref(i), out);
7001
+ }
7002
+ Self::collect_local_decl_names(std::slice::from_ref(body), out);
7003
+ }
7004
+ Statement::FunDecl { body, .. } => {
7005
+ Self::collect_local_decl_names(std::slice::from_ref(body), out)
7006
+ }
7007
+ _ => {}
7008
+ }
7009
+ }
7010
+ }
7011
+
7012
+ /// `(name, init-expr)` for every `let name = <init>` (recursing), for the int-valued fixpoint.
7013
+ fn collect_int_valued_defs<'a>(stmts: &'a [Statement], out: &mut Vec<(String, &'a Expr)>) {
7014
+ for s in stmts {
7015
+ match s {
7016
+ Statement::VarDecl {
7017
+ name,
7018
+ init: Some(e),
7019
+ ..
7020
+ } => out.push((name.to_string(), e)),
7021
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
7022
+ Self::collect_int_valued_defs(statements, out)
7023
+ }
7024
+ Statement::If {
7025
+ then_branch,
7026
+ else_branch,
7027
+ ..
7028
+ } => {
7029
+ Self::collect_int_valued_defs(std::slice::from_ref(then_branch), out);
7030
+ if let Some(e) = else_branch {
7031
+ Self::collect_int_valued_defs(std::slice::from_ref(e), out);
7032
+ }
7033
+ }
7034
+ Statement::While { body, .. } | Statement::DoWhile { body, .. } => {
7035
+ Self::collect_int_valued_defs(std::slice::from_ref(body), out)
7036
+ }
7037
+ Statement::For { init, body, .. } => {
7038
+ if let Some(i) = init {
7039
+ Self::collect_int_valued_defs(std::slice::from_ref(i), out);
7040
+ }
7041
+ Self::collect_int_valued_defs(std::slice::from_ref(body), out);
7042
+ }
7043
+ Statement::FunDecl { body, .. } => {
7044
+ Self::collect_int_valued_defs(std::slice::from_ref(body), out)
7045
+ }
7046
+ _ => {}
7047
+ }
7048
+ }
7049
+ }
7050
+
7051
+ /// Map `number[]` locals initialized from an array literal of integer literals → the inclusive
7052
+ /// element range, both inside `(-2^53, 2^53)`.
7053
+ fn collect_array_elem_ranges(stmts: &[Statement]) -> HashMap<String, (i64, i64)> {
7054
+ let mut out = HashMap::new();
7055
+ Self::array_elem_ranges_walk(stmts, &mut out);
7056
+ out
7057
+ }
7058
+
7059
+ fn array_elem_ranges_walk(stmts: &[Statement], out: &mut HashMap<String, (i64, i64)>) {
7060
+ for s in stmts {
7061
+ match s {
7062
+ Statement::VarDecl {
7063
+ name,
7064
+ init: Some(Expr::Array { elements, .. }),
7065
+ ..
7066
+ } => {
7067
+ let mut lo = i64::MAX;
7068
+ let mut hi = i64::MIN;
7069
+ let mut ok = !elements.is_empty();
7070
+ for el in elements {
7071
+ match el {
7072
+ ArrayElement::Expr(e) => match Self::int_literal_value_of(e) {
7073
+ Some(v) => {
7074
+ lo = lo.min(v);
7075
+ hi = hi.max(v);
7076
+ }
7077
+ None => {
7078
+ ok = false;
7079
+ break;
7080
+ }
7081
+ },
7082
+ _ => {
7083
+ ok = false;
7084
+ break;
7085
+ }
7086
+ }
7087
+ }
7088
+ if ok {
7089
+ out.insert(name.to_string(), (lo, hi));
7090
+ }
7091
+ }
7092
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
7093
+ Self::array_elem_ranges_walk(statements, out)
7094
+ }
7095
+ Statement::If {
7096
+ then_branch,
7097
+ else_branch,
7098
+ ..
7099
+ } => {
7100
+ Self::array_elem_ranges_walk(std::slice::from_ref(then_branch), out);
7101
+ if let Some(e) = else_branch {
7102
+ Self::array_elem_ranges_walk(std::slice::from_ref(e), out);
7103
+ }
7104
+ }
7105
+ Statement::While { body, .. } | Statement::DoWhile { body, .. } => {
7106
+ Self::array_elem_ranges_walk(std::slice::from_ref(body), out)
7107
+ }
7108
+ Statement::For { body, .. } => {
7109
+ Self::array_elem_ranges_walk(std::slice::from_ref(body), out)
7110
+ }
7111
+ Statement::FunDecl { body, .. } => {
7112
+ Self::array_elem_ranges_walk(std::slice::from_ref(body), out)
7113
+ }
7114
+ _ => {}
7115
+ }
7116
+ }
7117
+ }
7118
+
7119
+ /// Emit `e` as a native `i64` expression, used inside a native fold whose accumulator is `i64`.
7120
+ /// Returns `None` (caller keeps the `f64` fold) unless `e` is provably integer AND magnitude-
7121
+ /// bounded `< 2^53` at every node — so the `i64` arithmetic is bit-identical to the `f64` the
7122
+ /// interpreter/VM produce. `i64vars` are names already bound as `i64` (emitted bare); any other
7123
+ /// operand must be a bounded `f64` (emitted via `emit_typed_expr` then `as i64`, exact for
7124
+ /// integers < 2^53). Handles `+ - *` and `% <pos int literal>`; bails on anything else.
7125
+ fn emit_i64(
7126
+ &mut self,
7127
+ e: &Expr,
7128
+ i64vars: &HashSet<String>,
7129
+ ranges: &HashMap<String, (i64, i64)>,
7130
+ ) -> Result<Option<String>, CompileError> {
7131
+ // Whole-node bound (proves integrality + < 2^53). Without it, i64 could diverge from f64.
7132
+ if self.int_range(e, ranges).is_none() {
7133
+ return Ok(None);
7134
+ }
7135
+ if let Expr::Literal {
7136
+ value: Literal::Number(n),
7137
+ ..
7138
+ } = e
7139
+ {
7140
+ return Ok(Self::int_literal_value(*n).map(|v| format!("{}i64", v)));
7141
+ }
7142
+ if let Expr::Ident { name, .. } = e {
7143
+ if i64vars.contains(name.as_ref()) {
7144
+ return Ok(Some(Self::escape_ident(name.as_ref()).into_owned()));
7145
+ }
7146
+ }
7147
+ if let Expr::Binary {
7148
+ left, op, right, ..
7149
+ } = e
7150
+ {
7151
+ match op {
7152
+ BinOp::Add | BinOp::Sub | BinOp::Mul => {
7153
+ let (Some(l), Some(r)) = (
7154
+ self.emit_i64(left, i64vars, ranges)?,
7155
+ self.emit_i64(right, i64vars, ranges)?,
7156
+ ) else {
7157
+ return Ok(None);
7158
+ };
7159
+ let sym = match op {
7160
+ BinOp::Add => "+",
7161
+ BinOp::Sub => "-",
7162
+ _ => "*",
7163
+ };
7164
+ return Ok(Some(format!("({} {} {})", l, sym, r)));
7165
+ }
7166
+ BinOp::Mod => {
7167
+ if let Some(c) = Self::int_literal_value_of(right).filter(|&c| c > 0) {
7168
+ if let Some(l) = self.emit_i64(left, i64vars, ranges)? {
7169
+ return Ok(Some(format!("({} % {}i64)", l, c)));
7170
+ }
7171
+ }
7172
+ return Ok(None);
7173
+ }
7174
+ _ => return Ok(None),
7175
+ }
7176
+ }
7177
+ // Fallback: a bounded non-i64-var leaf (e.g. the f64 element variable) → cast once.
7178
+ let (code, ty) = self.emit_typed_expr(e)?;
7179
+ if ty == RustType::F64 {
7180
+ Ok(Some(format!("(({}) as i64)", code)))
7181
+ } else {
7182
+ Ok(None)
7183
+ }
7184
+ }
7185
+
7186
+ /// Record every annotated `VarDecl`/param name → its native `RustType`, recursing through all
7187
+ /// nested statements (loops, ifs, blocks, switch/try, function bodies). Flat; last write wins.
7188
+ fn collect_annotated_types(
7189
+ stmts: &[Statement],
7190
+ aliases: &HashMap<String, RustType>,
7191
+ env: &mut HashMap<String, RustType>,
7192
+ ) {
7193
+ for s in stmts {
7194
+ match s {
7195
+ Statement::VarDecl {
7196
+ name,
7197
+ type_ann: Some(ann),
7198
+ ..
7199
+ } => {
7200
+ env.insert(
7201
+ name.to_string(),
7202
+ RustType::from_annotation_with_aliases(ann, aliases),
7203
+ );
7204
+ }
7205
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
7206
+ Self::collect_annotated_types(statements, aliases, env)
7207
+ }
7208
+ Statement::If {
7209
+ then_branch,
7210
+ else_branch,
7211
+ ..
7212
+ } => {
7213
+ Self::collect_annotated_types(std::slice::from_ref(then_branch), aliases, env);
7214
+ if let Some(e) = else_branch {
7215
+ Self::collect_annotated_types(std::slice::from_ref(e), aliases, env);
7216
+ }
7217
+ }
7218
+ Statement::While { body, .. } | Statement::DoWhile { body, .. } => {
7219
+ Self::collect_annotated_types(std::slice::from_ref(body), aliases, env)
7220
+ }
7221
+ Statement::ForOf {
7222
+ name,
7223
+ iterable,
7224
+ body,
7225
+ ..
7226
+ } => {
7227
+ // A loop var iterating a `Vec<elem>` local binds `elem` — so `total += n` (n the
7228
+ // loop var over a `number[]`) is seen as native f64 and `total` is NOT demoted.
7229
+ // Sound: the Vec's elements are genuinely that native type at runtime.
7230
+ if let Expr::Ident { name: it_name, .. } = iterable {
7231
+ let elem_ty = match env.get(it_name.as_ref()) {
7232
+ Some(RustType::Vec(elem)) => Some((**elem).clone()),
7233
+ _ => None,
7234
+ };
7235
+ if let Some(t) = elem_ty {
7236
+ env.insert(name.to_string(), t);
7237
+ }
7238
+ }
7239
+ Self::collect_annotated_types(std::slice::from_ref(body), aliases, env)
7240
+ }
7241
+ Statement::For { init, body, .. } => {
7242
+ if let Some(i) = init {
7243
+ Self::collect_annotated_types(std::slice::from_ref(i), aliases, env);
7244
+ }
7245
+ Self::collect_annotated_types(std::slice::from_ref(body), aliases, env);
7246
+ }
7247
+ Statement::FunDecl {
7248
+ params,
7249
+ rest_param,
7250
+ body,
7251
+ ..
7252
+ } => {
7253
+ for p in params {
7254
+ if let FunParam::Simple(tp) = p {
7255
+ if let Some(ann) = &tp.type_ann {
7256
+ env.insert(
7257
+ tp.name.to_string(),
7258
+ RustType::from_annotation_with_aliases(ann, aliases),
7259
+ );
7260
+ }
7261
+ }
7262
+ }
7263
+ // Typed rest-param `...args: number[]` -> `Vec<f64>`, so a ForOf loop var over it
7264
+ // binds the element type and accumulators stay native.
7265
+ if let Some(rp) = rest_param {
7266
+ if let Some(ann) = &rp.type_ann {
7267
+ env.insert(
7268
+ rp.name.to_string(),
7269
+ RustType::from_annotation_with_aliases(ann, aliases),
7270
+ );
7271
+ }
7272
+ }
7273
+ Self::collect_annotated_types(std::slice::from_ref(body), aliases, env);
7274
+ }
7275
+ Statement::Switch {
7276
+ cases,
7277
+ default_body,
7278
+ ..
7279
+ } => {
7280
+ for (_, body) in cases {
7281
+ Self::collect_annotated_types(body, aliases, env);
7282
+ }
7283
+ if let Some(b) = default_body {
7284
+ Self::collect_annotated_types(b, aliases, env);
7285
+ }
7286
+ }
7287
+ Statement::Try {
7288
+ body,
7289
+ catch_body,
7290
+ finally_body,
7291
+ ..
7292
+ } => {
7293
+ Self::collect_annotated_types(std::slice::from_ref(body), aliases, env);
7294
+ if let Some(b) = catch_body {
7295
+ Self::collect_annotated_types(std::slice::from_ref(b), aliases, env);
7296
+ }
7297
+ if let Some(b) = finally_body {
7298
+ Self::collect_annotated_types(std::slice::from_ref(b), aliases, env);
7299
+ }
7300
+ }
7301
+ _ => {}
7302
+ }
7303
+ }
7304
+ }
7305
+
7306
+ fn collect_reassignments_stmts<'a>(stmts: &'a [Statement], out: &mut Vec<(String, &'a Expr)>) {
7307
+ for s in stmts {
7308
+ Self::collect_reassignments_stmt(s, out);
7309
+ }
7310
+ }
7311
+
7312
+ /// Collect every `(name, rhs)` reassignment (`=`, compound `+=`, logical `||=`) reachable from
7313
+ /// `s` — descending through nested statements and expressions (including closures).
7314
+ fn collect_reassignments_stmt<'a>(s: &'a Statement, out: &mut Vec<(String, &'a Expr)>) {
7315
+ match s {
7316
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
7317
+ Self::collect_reassignments_stmts(statements, out)
7318
+ }
7319
+ Statement::VarDecl { init: Some(e), .. } => Self::collect_reassignments_expr(e, out),
7320
+ Statement::VarDeclDestructure { init, .. } => {
7321
+ Self::collect_reassignments_expr(init, out)
7322
+ }
7323
+ Statement::ExprStmt { expr, .. } => Self::collect_reassignments_expr(expr, out),
7324
+ Statement::If {
7325
+ cond,
7326
+ then_branch,
7327
+ else_branch,
7328
+ ..
7329
+ } => {
7330
+ Self::collect_reassignments_expr(cond, out);
7331
+ Self::collect_reassignments_stmt(then_branch, out);
7332
+ if let Some(e) = else_branch {
7333
+ Self::collect_reassignments_stmt(e, out);
7334
+ }
7335
+ }
7336
+ Statement::While { cond, body, .. } => {
7337
+ Self::collect_reassignments_expr(cond, out);
7338
+ Self::collect_reassignments_stmt(body, out);
7339
+ }
7340
+ Statement::DoWhile { body, cond, .. } => {
7341
+ Self::collect_reassignments_stmt(body, out);
7342
+ Self::collect_reassignments_expr(cond, out);
7343
+ }
7344
+ Statement::For {
7345
+ init,
7346
+ cond,
7347
+ update,
7348
+ body,
7349
+ ..
7350
+ } => {
7351
+ if let Some(i) = init {
7352
+ Self::collect_reassignments_stmt(i, out);
7353
+ }
7354
+ if let Some(c) = cond {
7355
+ Self::collect_reassignments_expr(c, out);
7356
+ }
7357
+ if let Some(u) = update {
7358
+ Self::collect_reassignments_expr(u, out);
7359
+ }
7360
+ Self::collect_reassignments_stmt(body, out);
7361
+ }
7362
+ Statement::ForOf { iterable, body, .. } => {
7363
+ Self::collect_reassignments_expr(iterable, out);
7364
+ Self::collect_reassignments_stmt(body, out);
7365
+ }
7366
+ Statement::Return { value: Some(e), .. } => Self::collect_reassignments_expr(e, out),
7367
+ Statement::Throw { value, .. } => Self::collect_reassignments_expr(value, out),
7368
+ Statement::FunDecl { body, .. } => Self::collect_reassignments_stmt(body, out),
7369
+ Statement::Switch {
7370
+ expr,
7371
+ cases,
7372
+ default_body,
7373
+ ..
7374
+ } => {
7375
+ Self::collect_reassignments_expr(expr, out);
7376
+ for (g, body) in cases {
7377
+ if let Some(g) = g {
7378
+ Self::collect_reassignments_expr(g, out);
7379
+ }
7380
+ Self::collect_reassignments_stmts(body, out);
7381
+ }
7382
+ if let Some(b) = default_body {
7383
+ Self::collect_reassignments_stmts(b, out);
7384
+ }
7385
+ }
7386
+ Statement::Try {
7387
+ body,
7388
+ catch_body,
7389
+ finally_body,
7390
+ ..
7391
+ } => {
7392
+ Self::collect_reassignments_stmt(body, out);
7393
+ if let Some(b) = catch_body {
7394
+ Self::collect_reassignments_stmt(b, out);
7395
+ }
7396
+ if let Some(b) = finally_body {
7397
+ Self::collect_reassignments_stmt(b, out);
7398
+ }
7399
+ }
7400
+ _ => {}
7401
+ }
7402
+ }
7403
+
7404
+ fn collect_reassignments_expr<'a>(e: &'a Expr, out: &mut Vec<(String, &'a Expr)>) {
7405
+ match e {
7406
+ Expr::Assign { name, value, .. }
7407
+ | Expr::CompoundAssign { name, value, .. }
7408
+ | Expr::LogicalAssign { name, value, .. } => {
7409
+ out.push((name.to_string(), value.as_ref()));
7410
+ Self::collect_reassignments_expr(value, out);
7411
+ }
7412
+ Expr::Binary { left, right, .. } | Expr::NullishCoalesce { left, right, .. } => {
7413
+ Self::collect_reassignments_expr(left, out);
7414
+ Self::collect_reassignments_expr(right, out);
7415
+ }
7416
+ Expr::Unary { operand, .. }
7417
+ | Expr::TypeOf { operand, .. }
7418
+ | Expr::Await { operand, .. } => Self::collect_reassignments_expr(operand, out),
7419
+ Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
7420
+ Self::collect_reassignments_expr(callee, out);
7421
+ for a in args {
7422
+ match a {
7423
+ CallArg::Expr(x) | CallArg::Spread(x) => {
7424
+ Self::collect_reassignments_expr(x, out)
7425
+ }
7426
+ }
7427
+ }
7428
+ }
7429
+ Expr::Member { object, prop, .. } => {
7430
+ Self::collect_reassignments_expr(object, out);
7431
+ if let MemberProp::Expr(p) = prop {
7432
+ Self::collect_reassignments_expr(p, out);
7433
+ }
7434
+ }
7435
+ Expr::Index { object, index, .. } => {
7436
+ Self::collect_reassignments_expr(object, out);
7437
+ Self::collect_reassignments_expr(index, out);
7438
+ }
7439
+ Expr::Conditional {
7440
+ cond,
7441
+ then_branch,
7442
+ else_branch,
7443
+ ..
7444
+ } => {
7445
+ Self::collect_reassignments_expr(cond, out);
7446
+ Self::collect_reassignments_expr(then_branch, out);
7447
+ Self::collect_reassignments_expr(else_branch, out);
7448
+ }
7449
+ Expr::Array { elements, .. } => {
7450
+ for el in elements {
7451
+ match el {
7452
+ ArrayElement::Expr(x) | ArrayElement::Spread(x) => {
7453
+ Self::collect_reassignments_expr(x, out)
7454
+ }
7455
+ }
7456
+ }
7457
+ }
7458
+ Expr::Object { props, .. } => {
7459
+ for p in props {
7460
+ match p {
7461
+ ObjectProp::KeyValue(_, v, _) => Self::collect_reassignments_expr(v, out),
7462
+ ObjectProp::Spread(x) => Self::collect_reassignments_expr(x, out),
7463
+ }
7464
+ }
7465
+ }
7466
+ Expr::MemberAssign { object, value, .. } => {
7467
+ Self::collect_reassignments_expr(object, out);
7468
+ Self::collect_reassignments_expr(value, out);
7469
+ }
7470
+ Expr::IndexAssign {
7471
+ object,
7472
+ index,
7473
+ value,
7474
+ ..
7475
+ } => {
7476
+ Self::collect_reassignments_expr(object, out);
7477
+ Self::collect_reassignments_expr(index, out);
7478
+ Self::collect_reassignments_expr(value, out);
7479
+ }
7480
+ Expr::TemplateLiteral { exprs, .. } => {
7481
+ for x in exprs {
7482
+ Self::collect_reassignments_expr(x, out);
7483
+ }
7484
+ }
7485
+ Expr::ArrowFunction { body, .. } => match body {
7486
+ ArrowBody::Expr(x) => Self::collect_reassignments_expr(x, out),
7487
+ ArrowBody::Block(b) => Self::collect_reassignments_stmt(b, out),
7488
+ },
7489
+ _ => {}
7490
+ }
7491
+ }
7492
+
7493
+ /// Read-only mirror of `emit_typed_expr`'s native-type decision (no code generated), over a
7494
+ /// flat `name → RustType` env. Returns `RustType::F64` only for forms that provably lower to a
7495
+ /// native `f64`; everything else → `RustType::Value`. Conservative by construction: it never
7496
+ /// claims `F64` where `emit_typed_expr` would box, so a numeric local is never wrongly kept
7497
+ /// native (which would reintroduce the coercion panic).
7498
+ fn expr_native_type(&self, e: &Expr, env: &HashMap<String, RustType>) -> RustType {
7499
+ match e {
7500
+ Expr::Literal { value, .. } => match value {
7501
+ Literal::Number(_) => RustType::F64,
7502
+ Literal::String(_) => RustType::String,
7503
+ Literal::Bool(_) => RustType::Bool,
7504
+ Literal::Null => RustType::Value,
7505
+ },
7506
+ Expr::Ident { name, .. } => env
7507
+ .get(name.as_ref())
7508
+ .filter(|t| t.is_native())
7509
+ .cloned()
7510
+ .unwrap_or(RustType::Value),
7511
+ Expr::Binary {
7512
+ left, op, right, ..
7513
+ } => {
7514
+ let lt = self.expr_native_type(left, env);
7515
+ let rt = self.expr_native_type(right, env);
7516
+ RustType::result_type_of_binop(*op, &lt, &rt).unwrap_or(RustType::Value)
7517
+ }
7518
+ // `vec[i]` where `vec` is a `number[]` (Vec<f64>) → the element type. A `Vec<f64>`
7519
+ // can only hold numbers, so this never feeds a string into the accumulator.
7520
+ Expr::Index {
7521
+ object,
7522
+ optional: false,
7523
+ ..
7524
+ } => {
7525
+ if let Expr::Ident { name, .. } = object.as_ref() {
7526
+ if let Some(RustType::Vec(inner)) = env.get(name.as_ref()) {
7527
+ return (**inner).clone();
7528
+ }
7529
+ }
7530
+ RustType::Value
7531
+ }
7532
+ // `o.field` where `o` is a native struct local and `field` is a native field.
7533
+ Expr::Member {
7534
+ object,
7535
+ prop: MemberProp::Name { name: prop_name, .. },
7536
+ optional: false,
7537
+ ..
7538
+ } => {
7539
+ if let Expr::Ident { name: var_name, .. } = object.as_ref() {
7540
+ if let Some(RustType::Named { fields, .. }) = env.get(var_name.as_ref()) {
7541
+ if let Some((_, field_ty)) =
7542
+ fields.iter().find(|(k, _)| k.as_ref() == prop_name.as_ref())
7543
+ {
7544
+ if field_ty.is_native() {
7545
+ return field_ty.clone();
7546
+ }
7547
+ }
7548
+ }
7549
+ }
7550
+ RustType::Value
7551
+ }
7552
+ Expr::Call { callee, args, .. } => {
7553
+ // M5 native fn (`fn f_native(..) -> f64`); requires all-positional args.
7554
+ if let Expr::Ident { name: fname, .. } = callee.as_ref() {
7555
+ if self.native_fns.contains(fname.as_ref())
7556
+ && args.iter().all(|a| matches!(a, CallArg::Expr(_)))
7557
+ {
7558
+ return RustType::F64;
7559
+ }
7560
+ }
7561
+ // Single-arg `Math.<intrinsic>(x)` lowered to a direct `f64` method → number.
7562
+ if let [CallArg::Expr(_)] = args.as_slice() {
7563
+ if let Expr::Member {
7564
+ object,
7565
+ prop: MemberProp::Name { name: method, .. },
7566
+ ..
7567
+ } = callee.as_ref()
7568
+ {
7569
+ if matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
7570
+ && matches!(
7571
+ method.as_ref(),
7572
+ "sqrt" | "sin" | "cos" | "tan" | "abs" | "floor" | "ceil" | "exp"
7573
+ | "trunc" | "log"
7574
+ )
7575
+ {
7576
+ return RustType::F64;
7577
+ }
7578
+ }
7579
+ }
7580
+ RustType::Value
7581
+ }
7582
+ // Unary, Conditional, etc. are not modelled by `emit_typed_expr` (it boxes them), so a
7583
+ // store from one already coerces; treat as `Value` to match (→ demote if it feeds an
7584
+ // accumulator). Sound and consistent.
7585
+ _ => RustType::Value,
7586
+ }
7587
+ }
7588
+
7589
+ /// Names of top-level fns eligible for a parallel native `fn f_native(f64,..)->f64`:
7590
+ /// non-async, every param `: number` (no default), `: number` return, and a native-safe
7591
+ /// body (only block/if/return/expr-stmt over native exprs + calls to other eligible fns or
7592
+ /// 1-arg Math intrinsics). Conservative fixpoint — bails on anything else.
7593
+ fn collect_native_fns(statements: &[Statement]) -> std::collections::HashSet<String> {
7594
+ use std::collections::HashSet;
7595
+ let mut cand: HashSet<String> = HashSet::new();
7596
+ let mut decls: Vec<(&str, &Vec<FunParam>, &Statement)> = Vec::new();
7597
+ for s in statements {
7598
+ if let Statement::FunDecl {
7599
+ async_: false,
7600
+ name,
7601
+ params,
7602
+ rest_param: None,
7603
+ return_type,
7604
+ body,
7605
+ ..
7606
+ } = s
7607
+ {
7608
+ let params_ok = params.iter().all(|p| {
7609
+ matches!(p, FunParam::Simple(tp)
7610
+ if tp.default.is_none()
7611
+ && tp.type_ann.as_ref().map(Self::ann_is_number).unwrap_or(false))
7612
+ });
7613
+ // Return: an annotated `: number`, OR unannotated with all-numeric returns
7614
+ // (verified in the fixpoint via `returns_numeric`), so the native `-> f64` holds.
7615
+ let ret_ok = match return_type {
7616
+ Some(rt) => Self::ann_is_number(rt),
7617
+ None => true,
7618
+ };
7619
+ if ret_ok && params_ok && !params.is_empty() {
7620
+ cand.insert(name.to_string());
7621
+ decls.push((name.as_ref(), params, body));
7622
+ }
7623
+ }
7624
+ }
7625
+ loop {
7626
+ let mut remove: Vec<String> = Vec::new();
7627
+ for &(name, params, body) in &decls {
7628
+ if !cand.contains(name) {
7629
+ continue;
7630
+ }
7631
+ let pnames: HashSet<String> =
7632
+ params.iter().flat_map(|p| p.bound_names()).map(|n| n.to_string()).collect();
7633
+ if !Self::native_safe_stmt(body, &pnames, &cand)
7634
+ || !Self::returns_numeric(body, &pnames, &cand)
7635
+ {
7636
+ remove.push(name.to_string());
7637
+ }
7638
+ }
7639
+ if remove.is_empty() {
7640
+ break;
7641
+ }
7642
+ for n in remove {
7643
+ cand.remove(&n);
7644
+ }
7645
+ }
7646
+ cand
7647
+ }
7648
+
7649
+ fn native_safe_stmt(
7650
+ stmt: &Statement,
7651
+ params: &std::collections::HashSet<String>,
7652
+ cand: &std::collections::HashSet<String>,
7653
+ ) -> bool {
7654
+ match stmt {
7655
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
7656
+ statements.iter().all(|s| Self::native_safe_stmt(s, params, cand))
7657
+ }
7658
+ Statement::Return { value, .. } => {
7659
+ value.as_ref().is_some_and(|e| Self::native_safe_expr(e, params, cand))
7660
+ }
7661
+ Statement::If { cond, then_branch, else_branch, .. } => {
7662
+ Self::native_safe_expr(cond, params, cand)
7663
+ && Self::native_safe_stmt(then_branch, params, cand)
7664
+ && else_branch.as_ref().is_none_or(|e| Self::native_safe_stmt(e, params, cand))
7665
+ }
7666
+ Statement::ExprStmt { expr, .. } => Self::native_safe_expr(expr, params, cand),
7667
+ _ => false,
7668
+ }
7669
+ }
7670
+
7671
+ fn native_safe_expr(
7672
+ expr: &Expr,
7673
+ params: &std::collections::HashSet<String>,
7674
+ cand: &std::collections::HashSet<String>,
7675
+ ) -> bool {
7676
+ match expr {
7677
+ Expr::Literal { value, .. } => matches!(value, Literal::Number(_) | Literal::Bool(_)),
7678
+ Expr::Ident { name, .. } => params.contains(name.as_ref()),
7679
+ Expr::Binary { left, op, right, .. } => {
7680
+ matches!(
7681
+ op,
7682
+ BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow
7683
+ | BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge
7684
+ | BinOp::StrictEq | BinOp::StrictNe | BinOp::And | BinOp::Or
7685
+ ) && Self::native_safe_expr(left, params, cand)
7686
+ && Self::native_safe_expr(right, params, cand)
7687
+ }
7688
+ Expr::Unary { op, operand, .. } => {
7689
+ matches!(op, UnaryOp::Neg | UnaryOp::Pos | UnaryOp::Not)
7690
+ && Self::native_safe_expr(operand, params, cand)
7691
+ }
7692
+ Expr::Call { callee, args, .. } => {
7693
+ let args_ok = args
7694
+ .iter()
7695
+ .all(|a| matches!(a, CallArg::Expr(e) if Self::native_safe_expr(e, params, cand)));
7696
+ if !args_ok {
7697
+ return false;
7698
+ }
7699
+ match callee.as_ref() {
7700
+ Expr::Ident { name, .. } => cand.contains(name.as_ref()),
7701
+ Expr::Member { object, prop: MemberProp::Name { name: m, .. }, .. } => {
7702
+ matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
7703
+ && args.len() == 1
7704
+ && matches!(
7705
+ m.as_ref(),
7706
+ "sqrt" | "sin" | "cos" | "tan" | "abs" | "floor" | "ceil" | "exp"
7707
+ | "trunc" | "log"
7708
+ )
7709
+ }
7710
+ _ => false,
7711
+ }
7712
+ }
7713
+ _ => false,
7714
+ }
7715
+ }
7716
+
7717
+ /// Every `return` in `s` yields a numeric-shaped value, so a native `-> f64` body is sound.
7718
+ /// Lets an unannotated-but-numeric-returning fn (e.g. `function fib(n) {...}` after M4 typed
7719
+ /// the param) become M5-eligible.
7720
+ fn returns_numeric(
7721
+ s: &Statement,
7722
+ params: &std::collections::HashSet<String>,
7723
+ cand: &std::collections::HashSet<String>,
7724
+ ) -> bool {
7725
+ match s {
7726
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
7727
+ statements.iter().all(|x| Self::returns_numeric(x, params, cand))
7728
+ }
7729
+ Statement::Return { value, .. } => {
7730
+ value.as_ref().is_some_and(|e| Self::numeric_shaped(e, params, cand))
7731
+ }
7732
+ Statement::If { then_branch, else_branch, .. } => {
7733
+ Self::returns_numeric(then_branch, params, cand)
7734
+ && else_branch.as_ref().is_none_or(|e| Self::returns_numeric(e, params, cand))
7735
+ }
7736
+ Statement::While { body, .. } | Statement::For { body, .. } => {
7737
+ Self::returns_numeric(body, params, cand)
7738
+ }
7739
+ _ => true, // no return in this statement form
7740
+ }
7741
+ }
7742
+
7743
+ /// `e` evaluates to a number: built from numeric params, number literals, ARITHMETIC binops
7744
+ /// (comparisons/logical yield bool → excluded), numeric unary, conditionals, and calls to
7745
+ /// eligible native fns / 1-arg Math.
7746
+ fn numeric_shaped(
7747
+ e: &Expr,
7748
+ params: &std::collections::HashSet<String>,
7749
+ cand: &std::collections::HashSet<String>,
7750
+ ) -> bool {
7751
+ match e {
7752
+ Expr::Literal { value: Literal::Number(_), .. } => true,
7753
+ Expr::Ident { name, .. } => params.contains(name.as_ref()),
7754
+ Expr::Binary { left, op, right, .. } => {
7755
+ matches!(
7756
+ op,
7757
+ BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow
7758
+ ) && Self::numeric_shaped(left, params, cand)
7759
+ && Self::numeric_shaped(right, params, cand)
7760
+ }
7761
+ Expr::Unary { op, operand, .. } => {
7762
+ matches!(op, UnaryOp::Neg | UnaryOp::Pos)
7763
+ && Self::numeric_shaped(operand, params, cand)
7764
+ }
7765
+ Expr::Conditional { then_branch, else_branch, .. } => {
7766
+ Self::numeric_shaped(then_branch, params, cand)
7767
+ && Self::numeric_shaped(else_branch, params, cand)
7768
+ }
7769
+ Expr::Call { callee, .. } => match callee.as_ref() {
7770
+ Expr::Ident { name, .. } => cand.contains(name.as_ref()),
7771
+ Expr::Member { object, prop: MemberProp::Name { name: m, .. }, .. } => {
7772
+ matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
7773
+ && matches!(
7774
+ m.as_ref(),
7775
+ "sqrt" | "sin" | "cos" | "tan" | "abs" | "floor" | "ceil" | "exp"
7776
+ | "trunc" | "log"
7777
+ )
7778
+ }
7779
+ _ => false,
7780
+ },
7781
+ _ => false,
7782
+ }
7783
+ }
7784
+
7785
+ /// Emit `fn name_native(p: f64, ...) -> f64 { ... }` (top level) for each eligible fn.
7786
+ fn emit_native_fns(&mut self, statements: &[Statement]) -> Result<(), CompileError> {
7787
+ for s in statements {
7788
+ if let Statement::FunDecl { name, params, body, .. } = s {
7789
+ if !self.native_fns.contains(name.as_ref()) {
7790
+ continue;
7791
+ }
7792
+ let plist: Vec<String> = params
7793
+ .iter()
7794
+ .filter_map(|p| match p {
7795
+ FunParam::Simple(tp) => {
7796
+ Some(format!("mut {}: f64", Self::escape_ident(tp.name.as_ref())))
7797
+ }
7798
+ _ => None,
7799
+ })
7800
+ .collect();
7801
+ self.type_context.push_scope();
7802
+ for p in params {
7803
+ if let FunParam::Simple(tp) = p {
7804
+ self.type_context.define(tp.name.as_ref(), RustType::F64);
7805
+ }
7806
+ }
7807
+ self.writeln(&format!("fn {}_native({}) -> f64 {{", Self::escape_ident(name.as_ref()), plist.join(", ")));
7808
+ self.indent += 1;
7809
+ self.emit_native_fn_body(body)?;
7810
+ // Functions that fall off the end without returning: JS yields undefined; an
7811
+ // eligible numeric fn shouldn't, but emit a default to keep `-> f64` total.
7812
+ self.writeln("0.0");
7813
+ self.indent -= 1;
7814
+ self.writeln("}");
7815
+ self.type_context.pop_scope();
7816
+ }
7817
+ }
7818
+ Ok(())
7819
+ }
7820
+
7821
+ fn emit_native_fn_body(&mut self, stmt: &Statement) -> Result<(), CompileError> {
7822
+ match stmt {
7823
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
7824
+ for s in statements {
7825
+ self.emit_native_fn_body(s)?;
7826
+ }
7827
+ }
7828
+ Statement::Return { value, .. } => {
7829
+ let e = value.as_ref().expect("eligible return has a value");
7830
+ let (code, ty) = self.emit_typed_expr(e)?;
7831
+ let f = if ty == RustType::F64 {
7832
+ code
7833
+ } else if ty == RustType::Value {
7834
+ RustType::F64.from_value_expr(&code)
7835
+ } else {
7836
+ code
7837
+ };
7838
+ self.writeln(&format!("return {};", f));
7839
+ }
7840
+ Statement::If { cond, then_branch, else_branch, .. } => {
7841
+ let (c, ct) = self.emit_typed_expr(cond)?;
7842
+ let c_bool = match ct {
7843
+ RustType::Bool => c,
7844
+ RustType::F64 => format!("({} != 0.0)", c),
7845
+ _ => format!("{}.is_truthy()", c),
7846
+ };
7847
+ self.writeln(&format!("if {} {{", c_bool));
7848
+ self.indent += 1;
7849
+ self.emit_native_fn_body(then_branch)?;
7850
+ self.indent -= 1;
7851
+ if let Some(eb) = else_branch {
7852
+ self.writeln("} else {");
7853
+ self.indent += 1;
7854
+ self.emit_native_fn_body(eb)?;
7855
+ self.indent -= 1;
7856
+ }
7857
+ self.writeln("}");
7858
+ }
7859
+ Statement::ExprStmt { expr, .. } => {
7860
+ let (code, _) = self.emit_typed_expr(expr)?;
7861
+ self.writeln(&format!("{};", code));
7862
+ }
7863
+ _ => unreachable!("emit_native_fn_body: eligibility guarantees only handled statements"),
7864
+ }
7865
+ Ok(())
7866
+ }
7867
+
7868
+ // ====================================================================================
7869
+ // #177 (S-E / S-F): interprocedural aggregate (unboxed struct + Vec<Struct>) free fns.
7870
+ //
7871
+ // The infer front-end (S-0..S-D, behind TISH_AGGREGATE_INFER) stamps a struct alias and
7872
+ // `: alias` / `: alias[]` annotations onto the nbody-shape factory/array/operator fns iff a
7873
+ // whole-program candidacy predicate holds (monomorphic all-f64 struct, no `===`/escape/
7874
+ // reshape, write-permitting field ops). Here we consume that: emit each such fn as a native
7875
+ // Rust free fn over `Vec<TishStruct_alias>` threaded by `&mut`/`&`, with element access by
7876
+ // index (so JS reference aliasing — `let bi = bodies[i]; bi.vx = …` — lowers to in-place
7877
+ // `bodies[i].vx = …` and the mutation persists, exactly like the boxed `Value::Array`).
7878
+ //
7879
+ // SOUNDNESS: this is the codegen-side gate. We re-run `analyze_aggregate` to recover the
7880
+ // verdict, then emit every fn into a scratch buffer; if ANY construct can't be lowered the
7881
+ // whole path is disabled (the fns + the call hooks fall back to the boxed closures, byte-
7882
+ // identical to flag-off). So we never half-wire the feature or miscompile.
7883
+ // ====================================================================================
7884
+
7885
+ /// Compute the aggregate group from the stamped `: alias` / `: alias[]` annotations the infer
7886
+ /// front-end wrote (the infer→codegen contract — re-running `analyze_aggregate` on the already
7887
+ /// stamped program is NOT idempotent), emit the native free fns, and (on full success) record
7888
+ /// the routing state so call sites de-virtualize. On any failure the state is left empty (path
7889
+ /// disabled) and nothing is appended to the output.
7890
+ fn setup_aggregate_fns(&mut self, program: &Program) {
7891
+ let dbg = std::env::var("TISH_AGG_DEBUG").is_ok();
7892
+ // The unboxed struct alias: the (unique) name `A` used as an `A[]` array param of some fn,
7893
+ // and registered as an all-`Copy`-field struct alias.
7894
+ let Some(alias) = self.detect_aggregate_alias(program) else {
7895
+ if dbg {
7896
+ eprintln!("[agg] detect_aggregate_alias = None; aliases={:?}", self.type_aliases.keys().collect::<Vec<_>>());
7897
+ }
7898
+ return;
7899
+ };
7900
+ if dbg {
7901
+ eprintln!("[agg] alias = {}", alias);
7902
+ }
7903
+ // Top-level numeric global `let` names available to capture as trailing params.
7904
+ let globals = Self::collect_toplevel_global_lets(program);
7905
+
7906
+ // Build a signature for every fn in the group from its stamped annotations.
7907
+ let mut sigs: std::collections::HashMap<String, AggFnSig> = std::collections::HashMap::new();
7908
+ let mut decls: Vec<(String, Vec<FunParam>, Statement)> = Vec::new();
7909
+ for s in &program.statements {
7910
+ if let Statement::FunDecl {
7911
+ async_: false,
7912
+ name,
7913
+ params,
7914
+ rest_param: None,
7915
+ return_type,
7916
+ body,
7917
+ ..
7918
+ } = s
7919
+ {
7920
+ let nm = name.to_string();
7921
+ // The array param: a `Simple`-param annotated `: alias[]`.
7922
+ let array_pi = params.iter().position(|p| {
7923
+ matches!(p, FunParam::Simple(tp)
7924
+ if Self::ann_is_array_of(tp.type_ann.as_ref(), &alias))
7925
+ });
7926
+ // Return shape from the stamped return type, else from the body.
7927
+ let ret = if Self::ann_is_simple(return_type.as_ref(), &alias) {
7928
+ AggRet::Struct
7929
+ } else if Self::ann_is_array_of(return_type.as_ref(), &alias) {
7930
+ AggRet::ArrayOfStruct
7931
+ } else if array_pi.is_some() {
7932
+ if Self::stmt_returns_value(body) {
7933
+ AggRet::F64
7934
+ } else {
7935
+ AggRet::Unit
7936
+ }
7937
+ } else {
7938
+ // Not a factory and takes no array param → not in the group.
7939
+ continue;
7940
+ };
7941
+ let array_pname = array_pi.and_then(|pi| match params.get(pi) {
7942
+ Some(FunParam::Simple(tp)) => Some(tp.name.to_string()),
7943
+ _ => None,
7944
+ });
7945
+ let is_mut = array_pname
7946
+ .as_deref()
7947
+ .map(|p| Self::agg_fn_mutates_array(body, p))
7948
+ .unwrap_or(false);
7949
+ // Per-source-param kind.
7950
+ let mut sig_params: Vec<(String, AggParamKind)> = Vec::new();
7951
+ let mut ok = true;
7952
+ for (pi, p) in params.iter().enumerate() {
7953
+ match p {
7954
+ FunParam::Simple(tp) => {
7955
+ let kind = if Some(pi) == array_pi {
7956
+ AggParamKind::Array { is_mut }
7957
+ } else {
7958
+ // Scalar param: must be annotated `: number` (→ f64).
7959
+ let ty = tp
7960
+ .type_ann
7961
+ .as_ref()
7962
+ .map(|t| {
7963
+ crate::types::RustType::from_annotation_with_aliases(
7964
+ t,
7965
+ &self.type_aliases,
7966
+ )
7967
+ })
7968
+ .unwrap_or(RustType::Value);
7969
+ if ty != RustType::F64 {
7970
+ ok = false;
7971
+ break;
7972
+ }
7973
+ AggParamKind::Scalar(ty)
7974
+ };
7975
+ sig_params.push((tp.name.to_string(), kind));
7976
+ }
7977
+ _ => {
7978
+ ok = false;
7979
+ break;
7980
+ }
7981
+ }
7982
+ }
7983
+ if !ok {
7984
+ continue;
7985
+ }
7986
+ // Captured globals: free idents in the body that are top-level numeric globals and
7987
+ // not params/locals/group-fn names.
7988
+ let captured = Self::agg_captured_globals(body, params, &globals, &sigs, &nm, &alias);
7989
+ sigs.insert(
7990
+ nm.clone(),
7991
+ AggFnSig {
7992
+ params: sig_params,
7993
+ captured,
7994
+ ret,
7995
+ },
7996
+ );
7997
+ decls.push((nm, params.clone(), (**body).clone()));
7998
+ }
7999
+ }
8000
+ if dbg {
8001
+ eprintln!("[agg] sigs = {:?}", sigs.keys().collect::<Vec<_>>());
8002
+ }
8003
+ if sigs.is_empty() {
8004
+ return;
8005
+ }
8006
+
8007
+ // Top-level array locals: a `let bodies: alias[] = …` VarDecl.
8008
+ let array_locals: std::collections::HashSet<String> = program
8009
+ .statements
8010
+ .iter()
8011
+ .filter_map(|s| match s {
8012
+ Statement::VarDecl { name, type_ann, .. }
8013
+ if Self::ann_is_array_of(type_ann.as_ref(), &alias) =>
8014
+ {
8015
+ Some(name.to_string())
8016
+ }
8017
+ _ => None,
8018
+ })
8019
+ .collect();
8020
+
8021
+ // Tentatively commit the routing state so `emit_agg_*` can resolve nested group calls.
8022
+ self.aggregate_alias = Some(alias.clone());
8023
+ self.aggregate_fns = sigs;
8024
+ self.aggregate_array_locals = array_locals;
8025
+
8026
+ // Emit every fn into a scratch buffer; on any failure, roll back (disable the path).
8027
+ let saved = std::mem::take(&mut self.output);
8028
+ let saved_indent = self.indent;
8029
+ self.indent = 0;
8030
+ let mut all_ok = true;
8031
+ for (nm, params, body) in &decls {
8032
+ if self.emit_aggregate_fn(nm, params, body).is_err() {
8033
+ all_ok = false;
8034
+ break;
8035
+ }
8036
+ }
8037
+ let emitted = std::mem::replace(&mut self.output, saved);
8038
+ self.indent = saved_indent;
8039
+ if dbg {
8040
+ eprintln!("[agg] all_ok = {} array_locals = {:?}", all_ok, self.aggregate_array_locals);
8041
+ }
8042
+ if all_ok {
8043
+ self.output.push_str(&emitted);
8044
+ self.writeln("");
8045
+ } else {
8046
+ // Roll back: disable the aggregate path entirely.
8047
+ self.aggregate_alias = None;
8048
+ self.aggregate_fns.clear();
8049
+ self.aggregate_array_locals.clear();
8050
+ }
8051
+ }
8052
+
8053
+ /// Emit one aggregate fn as a native Rust free fn (`fn name_agg(..) -> ..`).
8054
+ fn emit_aggregate_fn(
8055
+ &mut self,
8056
+ name: &str,
8057
+ _params: &[FunParam],
8058
+ body: &Statement,
8059
+ ) -> Result<(), CompileError> {
8060
+ let sig = self
8061
+ .aggregate_fns
8062
+ .get(name)
8063
+ .cloned()
8064
+ .expect("emit_aggregate_fn: sig present");
8065
+ let alias = self
8066
+ .aggregate_alias
8067
+ .clone()
8068
+ .expect("emit_aggregate_fn: alias present");
8069
+ let struct_ty = crate::types::named_struct_ident(&alias);
8070
+
8071
+ // Build the param list + register types in a fresh scope.
8072
+ self.type_context.push_scope();
8073
+ let mut plist: Vec<String> = Vec::new();
8074
+ let mut array_param: Option<String> = None;
8075
+ for (pname, kind) in &sig.params {
8076
+ let esc = Self::escape_ident(pname).into_owned();
8077
+ match kind {
8078
+ AggParamKind::Array { is_mut } => {
8079
+ let r = if *is_mut { "&mut " } else { "&" };
8080
+ plist.push(format!("{}: {}Vec<{}>", esc, r, struct_ty));
8081
+ array_param = Some(pname.clone());
8082
+ }
8083
+ AggParamKind::Scalar(ty) => {
8084
+ plist.push(format!("{}: {}", esc, ty.to_rust_type_str()));
8085
+ self.type_context.define(pname, ty.clone());
8086
+ }
8087
+ }
8088
+ }
8089
+ for g in &sig.captured {
8090
+ plist.push(format!("{}: f64", Self::escape_ident(g)));
8091
+ self.type_context.define(g, RustType::F64);
8092
+ }
8093
+ let ret_str = match sig.ret {
8094
+ AggRet::Struct => format!(" -> {}", struct_ty),
8095
+ AggRet::ArrayOfStruct => format!(" -> Vec<{}>", struct_ty),
8096
+ AggRet::F64 => " -> f64".to_string(),
8097
+ AggRet::Unit => String::new(),
8098
+ };
8099
+ self.writeln("#[allow(non_snake_case, unused)]");
8100
+ self.writeln(&format!(
8101
+ "fn {}_agg({}){} {{",
8102
+ Self::escape_ident(name),
8103
+ plist.join(", "),
8104
+ ret_str
8105
+ ));
8106
+ self.indent += 1;
8107
+ let prev_ret = self.agg_cur_ret.replace(sig.ret.clone());
8108
+ let mut aliases: std::collections::HashMap<String, String> = std::collections::HashMap::new();
8109
+ let res = self.emit_agg_stmt(body, array_param.as_deref(), &mut aliases);
8110
+ self.agg_cur_ret = prev_ret;
8111
+ self.indent -= 1;
8112
+ self.writeln("}");
8113
+ self.type_context.pop_scope();
8114
+ res
8115
+ }
8116
+
8117
+ /// Emit a statement inside an aggregate fn body. `arr` is the array param name (if any);
8118
+ /// `aliases` maps element-alias locals (`let bi = bodies[i]`) to their index-var name.
8119
+ fn emit_agg_stmt(
8120
+ &mut self,
8121
+ stmt: &Statement,
8122
+ arr: Option<&str>,
8123
+ aliases: &mut std::collections::HashMap<String, String>,
8124
+ ) -> Result<(), CompileError> {
8125
+ match stmt {
8126
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
8127
+ for s in statements {
8128
+ self.emit_agg_stmt(s, arr, aliases)?;
8129
+ }
8130
+ Ok(())
8131
+ }
8132
+ Statement::VarDecl { name, init, .. } => {
8133
+ let init = init
8134
+ .as_ref()
8135
+ .ok_or_else(|| CompileError::new("agg: uninit let", None))?;
8136
+ // Element alias: `let bi = bodies[i]` (i a bare ident) → record, emit nothing.
8137
+ if let Expr::Index { object, index, .. } = init {
8138
+ if let (Expr::Ident { name: on, .. }, Expr::Ident { name: iv, .. }) =
8139
+ (object.as_ref(), index.as_ref())
8140
+ {
8141
+ if Some(on.as_ref()) == arr {
8142
+ aliases.insert(name.to_string(), iv.to_string());
8143
+ return Ok(());
8144
+ }
8145
+ }
8146
+ }
8147
+ let (code, ty) = self.emit_agg_expr(init, arr, aliases)?;
8148
+ self.type_context.define(name, ty);
8149
+ self.writeln(&format!("let mut {} = {};", Self::escape_ident(name), code));
8150
+ Ok(())
8151
+ }
8152
+ Statement::ExprStmt { expr, .. } => {
8153
+ let code = self.emit_agg_assign(expr, arr, aliases)?;
8154
+ self.writeln(&format!("{};", code));
8155
+ Ok(())
8156
+ }
8157
+ Statement::Return { value, .. } => {
8158
+ let ret = self
8159
+ .agg_cur_ret
8160
+ .clone()
8161
+ .unwrap_or(AggRet::Unit);
8162
+ match (&ret, value) {
8163
+ (AggRet::Unit, _) => {
8164
+ self.writeln("return;");
8165
+ Ok(())
8166
+ }
8167
+ (AggRet::F64, Some(e)) => {
8168
+ let (code, ty) = self.emit_agg_expr(e, arr, aliases)?;
8169
+ let f = if ty == RustType::F64 {
8170
+ code
8171
+ } else {
8172
+ return Err(CompileError::new("agg: non-f64 return", None));
8173
+ };
8174
+ self.writeln(&format!("return {};", f));
8175
+ Ok(())
8176
+ }
8177
+ (AggRet::Struct, Some(e)) => {
8178
+ let code = self.emit_agg_struct_literal(e, arr, aliases)?;
8179
+ self.writeln(&format!("return {};", code));
8180
+ Ok(())
8181
+ }
8182
+ (AggRet::ArrayOfStruct, Some(e)) => {
8183
+ let code = self.emit_agg_array_literal(e, arr, aliases)?;
8184
+ self.writeln(&format!("return {};", code));
8185
+ Ok(())
8186
+ }
8187
+ _ => Err(CompileError::new("agg: bad return", None)),
8188
+ }
8189
+ }
8190
+ Statement::For {
8191
+ init,
8192
+ cond,
8193
+ update,
8194
+ body,
8195
+ ..
8196
+ } => {
8197
+ // `for (init; cond; update) body` → `{ init; while cond { body; update; } }`.
8198
+ // The candidacy predicate forbids `break`/`continue` reaching this emitter (the
8199
+ // codegen gate below bails on them), so the lowering is faithful.
8200
+ self.writeln("{");
8201
+ self.indent += 1;
8202
+ if let Some(i) = init {
8203
+ self.emit_agg_stmt(i, arr, aliases)?;
8204
+ }
8205
+ let cond_code = match cond {
8206
+ Some(c) => {
8207
+ let (code, _) = self.emit_agg_expr(c, arr, aliases)?;
8208
+ code
8209
+ }
8210
+ None => "true".to_string(),
8211
+ };
8212
+ self.writeln(&format!("while {} {{", cond_code));
8213
+ self.indent += 1;
8214
+ self.emit_agg_stmt(body, arr, aliases)?;
8215
+ if let Some(u) = update {
8216
+ let ucode = self.emit_agg_assign(u, arr, aliases)?;
8217
+ self.writeln(&format!("{};", ucode));
8218
+ }
8219
+ self.indent -= 1;
8220
+ self.writeln("}");
8221
+ self.indent -= 1;
8222
+ self.writeln("}");
8223
+ Ok(())
8224
+ }
8225
+ _ => Err(CompileError::new("agg: unsupported statement", None)),
8226
+ }
8227
+ }
8228
+
8229
+ /// Emit an assignment / increment expression in statement position inside an aggregate fn.
8230
+ fn emit_agg_assign(
8231
+ &mut self,
8232
+ expr: &Expr,
8233
+ arr: Option<&str>,
8234
+ aliases: &std::collections::HashMap<String, String>,
8235
+ ) -> Result<String, CompileError> {
8236
+ match expr {
8237
+ // `px = <f64>` — scalar local/param assign.
8238
+ Expr::Assign { name, value, .. } => {
8239
+ let (v, _) = self.emit_agg_expr(value, arr, aliases)?;
8240
+ Ok(format!("{} = {}", Self::escape_ident(name), v))
8241
+ }
8242
+ Expr::CompoundAssign {
8243
+ name, op, value, ..
8244
+ } => {
8245
+ let (v, _) = self.emit_agg_expr(value, arr, aliases)?;
8246
+ let op_str = match op {
8247
+ CompoundOp::Add => "+=",
8248
+ CompoundOp::Sub => "-=",
8249
+ CompoundOp::Mul => "*=",
8250
+ CompoundOp::Div => "/=",
8251
+ CompoundOp::Mod => "%=",
8252
+ };
8253
+ Ok(format!("{} {} {}", Self::escape_ident(name), op_str, v))
8254
+ }
8255
+ // `i++` / `++i` / `i--` / `--i` (loop update) → native f64 step.
8256
+ Expr::PostfixInc { name, .. } | Expr::PrefixInc { name, .. } => {
8257
+ Ok(format!("{} += 1f64", Self::escape_ident(name)))
8258
+ }
8259
+ Expr::PostfixDec { name, .. } | Expr::PrefixDec { name, .. } => {
8260
+ Ok(format!("{} -= 1f64", Self::escape_ident(name)))
8261
+ }
8262
+ // `bi.vx = <f64>` (alias) / `bodies[i].vx = <f64>` (direct index) field write.
8263
+ Expr::MemberAssign {
8264
+ object,
8265
+ prop,
8266
+ value,
8267
+ ..
8268
+ } => {
8269
+ let field = crate::types::field_ident(prop.as_ref());
8270
+ let place = self.emit_agg_place(object, arr, aliases)?;
8271
+ let (v, _) = self.emit_agg_expr(value, arr, aliases)?;
8272
+ Ok(format!("{}.{} = {}", place, field, v))
8273
+ }
8274
+ _ => Err(CompileError::new("agg: unsupported statement expr", None)),
8275
+ }
8276
+ }
8277
+
8278
+ /// Emit the array-element place for a field write target: an alias ident `bi` or `bodies[i]`.
8279
+ fn emit_agg_place(
8280
+ &mut self,
8281
+ object: &Expr,
8282
+ arr: Option<&str>,
8283
+ aliases: &std::collections::HashMap<String, String>,
8284
+ ) -> Result<String, CompileError> {
8285
+ match object {
8286
+ Expr::Ident { name, .. } => {
8287
+ if let Some(idxvar) = aliases.get(name.as_ref()) {
8288
+ let a = arr.ok_or_else(|| CompileError::new("agg: alias no array", None))?;
8289
+ return Ok(format!(
8290
+ "{}[({}) as usize]",
8291
+ Self::escape_ident(a),
8292
+ Self::escape_ident(idxvar)
8293
+ ));
8294
+ }
8295
+ Err(CompileError::new("agg: bad write target", None))
8296
+ }
8297
+ Expr::Index { object: io, index, .. } => {
8298
+ if let Expr::Ident { name: on, .. } = io.as_ref() {
8299
+ if Some(on.as_ref()) == arr {
8300
+ let (idx, _) = self.emit_agg_expr(index, arr, aliases)?;
8301
+ return Ok(format!("{}[({}) as usize]", Self::escape_ident(on.as_ref()), idx));
8302
+ }
8303
+ }
8304
+ Err(CompileError::new("agg: bad index write target", None))
8305
+ }
8306
+ _ => Err(CompileError::new("agg: bad write target", None)),
8307
+ }
8308
+ }
8309
+
8310
+ /// Emit a (scalar / bool) expression inside an aggregate fn body.
8311
+ fn emit_agg_expr(
8312
+ &mut self,
8313
+ e: &Expr,
8314
+ arr: Option<&str>,
8315
+ aliases: &std::collections::HashMap<String, String>,
8316
+ ) -> Result<(String, RustType), CompileError> {
8317
+ match e {
8318
+ Expr::Literal {
8319
+ value: Literal::Number(n),
8320
+ ..
8321
+ } => Ok((Self::f64_lit(*n), RustType::F64)),
8322
+ Expr::Literal {
8323
+ value: Literal::Bool(b),
8324
+ ..
8325
+ } => Ok((format!("{}", b), RustType::Bool)),
8326
+ Expr::Ident { name, .. } => {
8327
+ let ty = self.type_context.get_type(name.as_ref());
8328
+ Ok((Self::escape_ident(name.as_ref()).into_owned(), ty))
8329
+ }
8330
+ Expr::Unary {
8331
+ op: UnaryOp::Neg,
8332
+ operand,
8333
+ ..
6302
8334
  } => {
6303
- let lt = self.expr_native_type(left, env);
6304
- let rt = self.expr_native_type(right, env);
6305
- RustType::result_type_of_binop(*op, &lt, &rt).unwrap_or(RustType::Value)
8335
+ let (o, _) = self.emit_agg_expr(operand, arr, aliases)?;
8336
+ Ok((format!("(-({}))", o), RustType::F64))
6306
8337
  }
6307
- // `vec[i]` where `vec` is a `number[]` (Vec<f64>) → the element type. A `Vec<f64>`
6308
- // can only hold numbers, so this never feeds a string into the accumulator.
6309
- Expr::Index {
6310
- object,
6311
- optional: false,
8338
+ Expr::Unary {
8339
+ op: UnaryOp::Pos,
8340
+ operand,
6312
8341
  ..
6313
8342
  } => {
6314
- if let Expr::Ident { name, .. } = object.as_ref() {
6315
- if let Some(RustType::Vec(inner)) = env.get(name.as_ref()) {
6316
- return (**inner).clone();
8343
+ let (o, _) = self.emit_agg_expr(operand, arr, aliases)?;
8344
+ Ok((format!("({})", o), RustType::F64))
8345
+ }
8346
+ Expr::Binary {
8347
+ left, op, right, ..
8348
+ } => {
8349
+ let (l, _) = self.emit_agg_expr(left, arr, aliases)?;
8350
+ let (r, _) = self.emit_agg_expr(right, arr, aliases)?;
8351
+ let (code, ty) = match op {
8352
+ BinOp::Add => (format!("({} + {})", l, r), RustType::F64),
8353
+ BinOp::Sub => (format!("({} - {})", l, r), RustType::F64),
8354
+ BinOp::Mul => (format!("({} * {})", l, r), RustType::F64),
8355
+ BinOp::Div => (format!("({} / {})", l, r), RustType::F64),
8356
+ BinOp::Mod => (format!("({} % {})", l, r), RustType::F64),
8357
+ BinOp::Pow => (format!("({}).powf({})", l, r), RustType::F64),
8358
+ BinOp::Lt => (format!("({} < {})", l, r), RustType::Bool),
8359
+ BinOp::Le => (format!("({} <= {})", l, r), RustType::Bool),
8360
+ BinOp::Gt => (format!("({} > {})", l, r), RustType::Bool),
8361
+ BinOp::Ge => (format!("({} >= {})", l, r), RustType::Bool),
8362
+ _ => {
8363
+ return Err(CompileError::new("agg: unsupported binop", None))
6317
8364
  }
6318
- }
6319
- RustType::Value
8365
+ };
8366
+ Ok((code, ty))
6320
8367
  }
6321
- // `o.field` where `o` is a native struct local and `field` is a native field.
6322
8368
  Expr::Member {
6323
8369
  object,
6324
- prop: MemberProp::Name { name: prop_name, .. },
8370
+ prop: MemberProp::Name { name: m, .. },
6325
8371
  optional: false,
6326
8372
  ..
6327
8373
  } => {
6328
- if let Expr::Ident { name: var_name, .. } = object.as_ref() {
6329
- if let Some(RustType::Named { fields, .. }) = env.get(var_name.as_ref()) {
6330
- if let Some((_, field_ty)) =
6331
- fields.iter().find(|(k, _)| k.as_ref() == prop_name.as_ref())
8374
+ if let Expr::Ident { name: on, .. } = object.as_ref() {
8375
+ // `bodies.length` → `(len() as f64)`.
8376
+ if Some(on.as_ref()) == arr && m.as_ref() == "length" {
8377
+ return Ok((
8378
+ format!("({}.len() as f64)", Self::escape_ident(on.as_ref())),
8379
+ RustType::F64,
8380
+ ));
8381
+ }
8382
+ // `bi.field` (element alias) → `bodies[i].field`.
8383
+ if let Some(idxvar) = aliases.get(on.as_ref()) {
8384
+ let a = arr
8385
+ .ok_or_else(|| CompileError::new("agg: alias no array", None))?;
8386
+ return Ok((
8387
+ format!(
8388
+ "{}[({}) as usize].{}",
8389
+ Self::escape_ident(a),
8390
+ Self::escape_ident(idxvar),
8391
+ crate::types::field_ident(m.as_ref())
8392
+ ),
8393
+ RustType::F64,
8394
+ ));
8395
+ }
8396
+ // `localStruct.field` (a `Named` local).
8397
+ let ty = self.type_context.get_type(on.as_ref());
8398
+ if let RustType::Named { fields, .. } = &ty {
8399
+ if let Some((_, ft)) =
8400
+ fields.iter().find(|(k, _)| k.as_ref() == m.as_ref())
6332
8401
  {
6333
- if field_ty.is_native() {
6334
- return field_ty.clone();
6335
- }
8402
+ return Ok((
8403
+ format!(
8404
+ "{}.{}",
8405
+ Self::escape_ident(on.as_ref()),
8406
+ crate::types::field_ident(m.as_ref())
8407
+ ),
8408
+ ft.clone(),
8409
+ ));
6336
8410
  }
6337
8411
  }
6338
8412
  }
6339
- RustType::Value
8413
+ // `bodies[i].field` read.
8414
+ if let Expr::Index { object: io, index, .. } = object.as_ref() {
8415
+ if let Expr::Ident { name: on, .. } = io.as_ref() {
8416
+ if Some(on.as_ref()) == arr {
8417
+ let (idx, _) = self.emit_agg_expr(index, arr, aliases)?;
8418
+ return Ok((
8419
+ format!(
8420
+ "{}[({}) as usize].{}",
8421
+ Self::escape_ident(on.as_ref()),
8422
+ idx,
8423
+ crate::types::field_ident(m.as_ref())
8424
+ ),
8425
+ RustType::F64,
8426
+ ));
8427
+ }
8428
+ }
8429
+ }
8430
+ Err(CompileError::new("agg: unsupported member", None))
6340
8431
  }
6341
8432
  Expr::Call { callee, args, .. } => {
6342
- // M5 native fn (`fn f_native(..) -> f64`); requires all-positional args.
8433
+ // Nested call to another group fn (e.g. `body(...)` from `makeBodies`).
6343
8434
  if let Expr::Ident { name: fname, .. } = callee.as_ref() {
6344
- if self.native_fns.contains(fname.as_ref())
6345
- && args.iter().all(|a| matches!(a, CallArg::Expr(_)))
6346
- {
6347
- return RustType::F64;
8435
+ if self.aggregate_fns.contains_key(fname.as_ref()) {
8436
+ let (code, ret) =
8437
+ self.emit_agg_group_call(fname.as_ref(), args, arr, aliases)?;
8438
+ let ty = match ret {
8439
+ AggRet::F64 => RustType::F64,
8440
+ AggRet::Struct => {
8441
+ let alias = self.aggregate_alias.clone().unwrap();
8442
+ RustType::Named {
8443
+ name: alias.as_str().into(),
8444
+ fields: Vec::new(),
8445
+ }
8446
+ }
8447
+ _ => {
8448
+ return Err(CompileError::new(
8449
+ "agg: call return not usable here",
8450
+ None,
8451
+ ))
8452
+ }
8453
+ };
8454
+ return Ok((code, ty));
6348
8455
  }
6349
8456
  }
6350
- // Single-arg `Math.<intrinsic>(x)` lowered to a direct `f64` method → number.
6351
- if let [CallArg::Expr(_)] = args.as_slice() {
6352
- if let Expr::Member {
6353
- object,
6354
- prop: MemberProp::Name { name: method, .. },
6355
- ..
6356
- } = callee.as_ref()
8457
+ // `Math.<fn>(x)` clean f64-method intrinsics.
8458
+ if let Expr::Member {
8459
+ object,
8460
+ prop: MemberProp::Name { name: method, .. },
8461
+ ..
8462
+ } = callee.as_ref()
8463
+ {
8464
+ if matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
6357
8465
  {
6358
- if matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
6359
- && matches!(
6360
- method.as_ref(),
6361
- "sqrt" | "sin" | "cos" | "tan" | "abs" | "floor" | "ceil" | "exp"
6362
- | "trunc" | "log"
6363
- )
6364
- {
6365
- return RustType::F64;
8466
+ if let Some(m) = Self::agg_clean_math_method(method.as_ref()) {
8467
+ if let [CallArg::Expr(a)] = args.as_slice() {
8468
+ let (ac, _) = self.emit_agg_expr(a, arr, aliases)?;
8469
+ return Ok((format!("({}).{}()", ac, m), RustType::F64));
8470
+ }
6366
8471
  }
6367
8472
  }
6368
8473
  }
6369
- RustType::Value
8474
+ Err(CompileError::new("agg: unsupported call", None))
6370
8475
  }
6371
- // Unary, Conditional, etc. are not modelled by `emit_typed_expr` (it boxes them), so a
6372
- // store from one already coerces; treat as `Value` to match (→ demote if it feeds an
6373
- // accumulator). Sound and consistent.
6374
- _ => RustType::Value,
8476
+ Expr::Conditional {
8477
+ cond,
8478
+ then_branch,
8479
+ else_branch,
8480
+ ..
8481
+ } => {
8482
+ let (c, _) = self.emit_agg_expr(cond, arr, aliases)?;
8483
+ let (t, _) = self.emit_agg_expr(then_branch, arr, aliases)?;
8484
+ let (el, _) = self.emit_agg_expr(else_branch, arr, aliases)?;
8485
+ Ok((format!("(if {} {{ {} }} else {{ {} }})", c, t, el), RustType::F64))
8486
+ }
8487
+ _ => Err(CompileError::new("agg: unsupported expr", None)),
6375
8488
  }
6376
8489
  }
6377
8490
 
6378
- /// Names of top-level fns eligible for a parallel native `fn f_native(f64,..)->f64`:
6379
- /// non-async, every param `: number` (no default), `: number` return, and a native-safe
6380
- /// body (only block/if/return/expr-stmt over native exprs + calls to other eligible fns or
6381
- /// 1-arg Math intrinsics). Conservative fixpoint — bails on anything else.
6382
- fn collect_native_fns(statements: &[Statement]) -> std::collections::HashSet<String> {
6383
- use std::collections::HashSet;
6384
- let mut cand: HashSet<String> = HashSet::new();
6385
- let mut decls: Vec<(&str, &Vec<FunParam>, &Statement)> = Vec::new();
6386
- for s in statements {
6387
- if let Statement::FunDecl {
6388
- async_: false,
6389
- name,
6390
- params,
6391
- rest_param: None,
6392
- return_type,
6393
- body,
6394
- ..
6395
- } = s
6396
- {
6397
- let params_ok = params.iter().all(|p| {
6398
- matches!(p, FunParam::Simple(tp)
6399
- if tp.default.is_none()
6400
- && tp.type_ann.as_ref().map(Self::ann_is_number).unwrap_or(false))
6401
- });
6402
- // Return: an annotated `: number`, OR unannotated with all-numeric returns
6403
- // (verified in the fixpoint via `returns_numeric`), so the native `-> f64` holds.
6404
- let ret_ok = match return_type {
6405
- Some(rt) => Self::ann_is_number(rt),
6406
- None => true,
6407
- };
6408
- if ret_ok && params_ok && !params.is_empty() {
6409
- cand.insert(name.to_string());
6410
- decls.push((name.as_ref(), params, body));
8491
+ /// Emit a `return { ... }` object literal as a native struct literal (the `body()` factory).
8492
+ fn emit_agg_struct_literal(
8493
+ &mut self,
8494
+ e: &Expr,
8495
+ arr: Option<&str>,
8496
+ aliases: &std::collections::HashMap<String, String>,
8497
+ ) -> Result<String, CompileError> {
8498
+ let alias = self.aggregate_alias.clone().unwrap();
8499
+ let struct_ty = crate::types::named_struct_ident(&alias);
8500
+ let fields = match self.type_aliases.get(&alias) {
8501
+ Some(RustType::Named { fields, .. }) | Some(RustType::Object(fields)) => fields.clone(),
8502
+ _ => return Err(CompileError::new("agg: alias not a struct", None)),
8503
+ };
8504
+ let Expr::Object { props, .. } = e else {
8505
+ return Err(CompileError::new("agg: struct return not literal", None));
8506
+ };
8507
+ use std::collections::HashMap;
8508
+ let mut by_key: HashMap<String, &Expr> = HashMap::new();
8509
+ for p in props {
8510
+ match p {
8511
+ ObjectProp::KeyValue(k, v, _) => {
8512
+ by_key.insert(k.to_string(), v);
8513
+ }
8514
+ ObjectProp::Spread(_) => {
8515
+ return Err(CompileError::new("agg: struct spread", None))
6411
8516
  }
6412
8517
  }
6413
8518
  }
6414
- loop {
6415
- let mut remove: Vec<String> = Vec::new();
6416
- for &(name, params, body) in &decls {
6417
- if !cand.contains(name) {
6418
- continue;
8519
+ let mut inits: Vec<String> = Vec::new();
8520
+ for (k, _) in &fields {
8521
+ let v = by_key
8522
+ .get(k.as_ref())
8523
+ .ok_or_else(|| CompileError::new("agg: struct missing field", None))?;
8524
+ let (code, _) = self.emit_agg_expr(v, arr, aliases)?;
8525
+ inits.push(format!("{}: {}", crate::types::field_ident(k.as_ref()), code));
8526
+ }
8527
+ Ok(format!("{} {{ {} }}", struct_ty, inits.join(", ")))
8528
+ }
8529
+
8530
+ /// Emit a `return [a, b, c]` array literal of struct-typed idents as `vec![a, b, c]`.
8531
+ fn emit_agg_array_literal(
8532
+ &mut self,
8533
+ e: &Expr,
8534
+ _arr: Option<&str>,
8535
+ _aliases: &std::collections::HashMap<String, String>,
8536
+ ) -> Result<String, CompileError> {
8537
+ let Expr::Array { elements, .. } = e else {
8538
+ return Err(CompileError::new("agg: array return not literal", None));
8539
+ };
8540
+ let mut items: Vec<String> = Vec::new();
8541
+ for el in elements {
8542
+ match el {
8543
+ ArrayElement::Expr(Expr::Ident { name, .. }) => {
8544
+ items.push(Self::escape_ident(name.as_ref()).into_owned());
6419
8545
  }
6420
- let pnames: HashSet<String> =
6421
- params.iter().flat_map(|p| p.bound_names()).map(|n| n.to_string()).collect();
6422
- if !Self::native_safe_stmt(body, &pnames, &cand)
6423
- || !Self::returns_numeric(body, &pnames, &cand)
6424
- {
6425
- remove.push(name.to_string());
8546
+ _ => {
8547
+ return Err(CompileError::new(
8548
+ "agg: array element not a struct ident",
8549
+ None,
8550
+ ))
6426
8551
  }
6427
8552
  }
6428
- if remove.is_empty() {
6429
- break;
6430
- }
6431
- for n in remove {
6432
- cand.remove(&n);
8553
+ }
8554
+ Ok(format!("vec![{}]", items.join(", ")))
8555
+ }
8556
+
8557
+ /// Emit a direct call to a group fn, threading the array by `&mut`/`&` plus captured globals.
8558
+ /// Returns the call code and the callee's return shape. `arr`/`aliases` describe the CALLER's
8559
+ /// context (so an array arg can be the caller's array param, though nbody only passes scalars
8560
+ /// across nested group calls).
8561
+ fn emit_agg_group_call(
8562
+ &mut self,
8563
+ name: &str,
8564
+ args: &[CallArg],
8565
+ arr: Option<&str>,
8566
+ aliases: &std::collections::HashMap<String, String>,
8567
+ ) -> Result<(String, AggRet), CompileError> {
8568
+ let sig = self
8569
+ .aggregate_fns
8570
+ .get(name)
8571
+ .cloned()
8572
+ .ok_or_else(|| CompileError::new("agg: unknown group fn", None))?;
8573
+ let mut call_args: Vec<String> = Vec::new();
8574
+ for (i, (_pname, kind)) in sig.params.iter().enumerate() {
8575
+ let a = match args.get(i) {
8576
+ Some(CallArg::Expr(e)) => e,
8577
+ _ => return Err(CompileError::new("agg: call arg shape", None)),
8578
+ };
8579
+ match kind {
8580
+ AggParamKind::Array { is_mut } => {
8581
+ // The arg must be a bare ident naming an array (caller's param or local).
8582
+ let Expr::Ident { name: an, .. } = a else {
8583
+ return Err(CompileError::new("agg: array arg not ident", None));
8584
+ };
8585
+ let r = if *is_mut { "&mut " } else { "&" };
8586
+ call_args.push(format!("{}{}", r, Self::escape_ident(an.as_ref())));
8587
+ }
8588
+ AggParamKind::Scalar(_) => {
8589
+ let (code, _) = self.emit_agg_expr(a, arr, aliases)?;
8590
+ call_args.push(code);
8591
+ }
6433
8592
  }
6434
8593
  }
6435
- cand
8594
+ // Captured globals are visible as same-named f64 params/locals in the caller too.
8595
+ for g in &sig.captured {
8596
+ call_args.push(Self::escape_ident(g).into_owned());
8597
+ }
8598
+ Ok((
8599
+ format!("{}_agg({})", Self::escape_ident(name), call_args.join(", ")),
8600
+ sig.ret,
8601
+ ))
6436
8602
  }
6437
8603
 
6438
- fn native_safe_stmt(
6439
- stmt: &Statement,
6440
- params: &std::collections::HashSet<String>,
6441
- cand: &std::collections::HashSet<String>,
6442
- ) -> bool {
6443
- match stmt {
6444
- Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
6445
- statements.iter().all(|s| Self::native_safe_stmt(s, params, cand))
8604
+ /// Try to route a top-level call `name(args)` to its aggregate free fn. `as_value` wraps an
8605
+ /// f64 result in `Value::Number` for the boxed `emit_expr` context. Returns `None` if `name`
8606
+ /// isn't a group fn (caller falls back to the normal path).
8607
+ fn try_emit_toplevel_agg_call(
8608
+ &mut self,
8609
+ callee: &Expr,
8610
+ args: &[CallArg],
8611
+ as_value: bool,
8612
+ ) -> Result<Option<(String, RustType)>, CompileError> {
8613
+ let Expr::Ident { name, .. } = callee else {
8614
+ return Ok(None);
8615
+ };
8616
+ if !self.aggregate_fns.contains_key(name.as_ref()) {
8617
+ return Ok(None);
8618
+ }
8619
+ let sig = self.aggregate_fns.get(name.as_ref()).cloned().unwrap();
8620
+ let mut call_args: Vec<String> = Vec::new();
8621
+ for (i, (_pname, kind)) in sig.params.iter().enumerate() {
8622
+ let a = match args.get(i) {
8623
+ Some(CallArg::Expr(e)) => e,
8624
+ _ => return Ok(None),
8625
+ };
8626
+ match kind {
8627
+ AggParamKind::Array { is_mut } => {
8628
+ let Expr::Ident { name: an, .. } = a else {
8629
+ return Ok(None);
8630
+ };
8631
+ let r = if *is_mut { "&mut " } else { "&" };
8632
+ call_args.push(format!("{}{}", r, Self::escape_ident(an.as_ref())));
8633
+ }
8634
+ AggParamKind::Scalar(_) => {
8635
+ let (code, ty) = self.emit_typed_expr(a)?;
8636
+ let f = if ty == RustType::F64 {
8637
+ code
8638
+ } else if ty == RustType::Value {
8639
+ RustType::F64.from_value_expr(&code)
8640
+ } else {
8641
+ code
8642
+ };
8643
+ call_args.push(f);
8644
+ }
6446
8645
  }
6447
- Statement::Return { value, .. } => {
6448
- value.as_ref().is_some_and(|e| Self::native_safe_expr(e, params, cand))
8646
+ }
8647
+ for g in &sig.captured {
8648
+ let (code, ty) = self.emit_typed_expr(&Expr::Ident {
8649
+ name: g.as_str().into(),
8650
+ span: tishlang_ast::Span::default(),
8651
+ })?;
8652
+ let f = if ty == RustType::F64 {
8653
+ code
8654
+ } else if ty == RustType::Value {
8655
+ RustType::F64.from_value_expr(&code)
8656
+ } else {
8657
+ code
8658
+ };
8659
+ call_args.push(f);
8660
+ }
8661
+ let call = format!("{}_agg({})", Self::escape_ident(name.as_ref()), call_args.join(", "));
8662
+ let (code, ty) = match sig.ret {
8663
+ AggRet::F64 => {
8664
+ if as_value {
8665
+ (format!("Value::Number({})", call), RustType::Value)
8666
+ } else {
8667
+ (call, RustType::F64)
8668
+ }
6449
8669
  }
6450
- Statement::If { cond, then_branch, else_branch, .. } => {
6451
- Self::native_safe_expr(cond, params, cand)
6452
- && Self::native_safe_stmt(then_branch, params, cand)
6453
- && else_branch.as_ref().is_none_or(|e| Self::native_safe_stmt(e, params, cand))
8670
+ AggRet::ArrayOfStruct => {
8671
+ let alias = self.aggregate_alias.clone().unwrap();
8672
+ (
8673
+ call,
8674
+ RustType::Vec(Box::new(RustType::Named {
8675
+ name: alias.as_str().into(),
8676
+ fields: Vec::new(),
8677
+ })),
8678
+ )
8679
+ }
8680
+ AggRet::Struct => {
8681
+ let alias = self.aggregate_alias.clone().unwrap();
8682
+ (
8683
+ call,
8684
+ RustType::Named {
8685
+ name: alias.as_str().into(),
8686
+ fields: Vec::new(),
8687
+ },
8688
+ )
8689
+ }
8690
+ // void call: `()`; valid only in statement position (ExprStmt).
8691
+ AggRet::Unit => (call, RustType::Unit),
8692
+ };
8693
+ Ok(Some((code, ty)))
8694
+ }
8695
+
8696
+ /// Detect the unboxed struct alias: the unique type-alias name `A` that is (a) used as an
8697
+ /// `A[]` array param of some top-level fn and (b) registered as a struct whose fields are all
8698
+ /// `Copy` (numeric/bool) — so element field reads/writes by index are sound. Returns `None`
8699
+ /// if there is no such alias or more than one (ambiguous → bail to boxed).
8700
+ fn detect_aggregate_alias(&self, program: &Program) -> Option<String> {
8701
+ let mut found: Option<String> = None;
8702
+ for s in &program.statements {
8703
+ if let Statement::FunDecl { params, .. } = s {
8704
+ for p in params {
8705
+ if let FunParam::Simple(tp) = p {
8706
+ if let Some(TypeAnnotation::Array(inner)) = tp.type_ann.as_ref() {
8707
+ if let TypeAnnotation::Simple(a, _) = inner.as_ref() {
8708
+ let a = a.to_string();
8709
+ if !self.alias_is_copy_struct(&a) {
8710
+ continue;
8711
+ }
8712
+ match &found {
8713
+ Some(prev) if prev != &a => return None, // ambiguous
8714
+ _ => found = Some(a),
8715
+ }
8716
+ }
8717
+ }
8718
+ }
8719
+ }
8720
+ }
8721
+ }
8722
+ found
8723
+ }
8724
+
8725
+ /// Is `name` a registered struct alias whose every field is a `Copy` scalar (f64/bool/i32)?
8726
+ fn alias_is_copy_struct(&self, name: &str) -> bool {
8727
+ match self.type_aliases.get(name) {
8728
+ Some(RustType::Named { fields, .. }) | Some(RustType::Object(fields)) => {
8729
+ !fields.is_empty()
8730
+ && fields.iter().all(|(_, t)| {
8731
+ matches!(t, RustType::F64 | RustType::Bool | RustType::I32)
8732
+ })
6454
8733
  }
6455
- Statement::ExprStmt { expr, .. } => Self::native_safe_expr(expr, params, cand),
6456
8734
  _ => false,
6457
8735
  }
6458
8736
  }
6459
8737
 
6460
- fn native_safe_expr(
6461
- expr: &Expr,
6462
- params: &std::collections::HashSet<String>,
6463
- cand: &std::collections::HashSet<String>,
6464
- ) -> bool {
6465
- match expr {
6466
- Expr::Literal { value, .. } => matches!(value, Literal::Number(_) | Literal::Bool(_)),
6467
- Expr::Ident { name, .. } => params.contains(name.as_ref()),
6468
- Expr::Binary { left, op, right, .. } => {
6469
- matches!(
6470
- op,
6471
- BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow
6472
- | BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge
6473
- | BinOp::StrictEq | BinOp::StrictNe | BinOp::And | BinOp::Or
6474
- ) && Self::native_safe_expr(left, params, cand)
6475
- && Self::native_safe_expr(right, params, cand)
8738
+ /// Is `ann` exactly `Simple(alias)`?
8739
+ fn ann_is_simple(ann: Option<&TypeAnnotation>, alias: &str) -> bool {
8740
+ matches!(ann, Some(TypeAnnotation::Simple(a, _)) if a.as_ref() == alias)
8741
+ }
8742
+
8743
+ /// Is `ann` exactly `Array(Simple(alias))` (i.e. `alias[]`)?
8744
+ fn ann_is_array_of(ann: Option<&TypeAnnotation>, alias: &str) -> bool {
8745
+ matches!(ann, Some(TypeAnnotation::Array(inner))
8746
+ if matches!(inner.as_ref(), TypeAnnotation::Simple(a, _) if a.as_ref() == alias))
8747
+ }
8748
+
8749
+ /// Math methods whose JS semantics match the Rust `f64` method 1:1 (no rounding/sign quirks).
8750
+ fn agg_clean_math_method(m: &str) -> Option<&'static str> {
8751
+ Some(match m {
8752
+ "sqrt" => "sqrt",
8753
+ "sin" => "sin",
8754
+ "cos" => "cos",
8755
+ "tan" => "tan",
8756
+ "exp" => "exp",
8757
+ "log" => "ln",
8758
+ "sinh" => "sinh",
8759
+ "cosh" => "cosh",
8760
+ "tanh" => "tanh",
8761
+ "asin" => "asin",
8762
+ "acos" => "acos",
8763
+ "atan" => "atan",
8764
+ "asinh" => "asinh",
8765
+ "acosh" => "acosh",
8766
+ "atanh" => "atanh",
8767
+ "cbrt" => "cbrt",
8768
+ "log2" => "log2",
8769
+ "log10" => "log10",
8770
+ _ => return None,
8771
+ })
8772
+ }
8773
+
8774
+ /// Does `body` contain a write through array param `p` (element field write / index write)?
8775
+ fn agg_fn_mutates_array(body: &Statement, p: &str) -> bool {
8776
+ let mut aliases: std::collections::HashSet<String> = std::collections::HashSet::new();
8777
+ Self::agg_collect_aliases(body, p, &mut aliases);
8778
+ Self::agg_stmt_writes(body, p, &aliases)
8779
+ }
8780
+
8781
+ fn agg_collect_aliases(s: &Statement, p: &str, out: &mut std::collections::HashSet<String>) {
8782
+ match s {
8783
+ Statement::VarDecl {
8784
+ name,
8785
+ init: Some(Expr::Index { object, index, .. }),
8786
+ ..
8787
+ } => {
8788
+ if matches!(object.as_ref(), Expr::Ident { name: o, .. } if o.as_ref() == p)
8789
+ && matches!(index.as_ref(), Expr::Ident { .. })
8790
+ {
8791
+ out.insert(name.to_string());
8792
+ }
6476
8793
  }
6477
- Expr::Unary { op, operand, .. } => {
6478
- matches!(op, UnaryOp::Neg | UnaryOp::Pos | UnaryOp::Not)
6479
- && Self::native_safe_expr(operand, params, cand)
8794
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
8795
+ statements.iter().for_each(|x| Self::agg_collect_aliases(x, p, out));
6480
8796
  }
6481
- Expr::Call { callee, args, .. } => {
6482
- let args_ok = args
6483
- .iter()
6484
- .all(|a| matches!(a, CallArg::Expr(e) if Self::native_safe_expr(e, params, cand)));
6485
- if !args_ok {
6486
- return false;
8797
+ Statement::If {
8798
+ then_branch,
8799
+ else_branch,
8800
+ ..
8801
+ } => {
8802
+ Self::agg_collect_aliases(then_branch, p, out);
8803
+ if let Some(e) = else_branch {
8804
+ Self::agg_collect_aliases(e, p, out);
6487
8805
  }
6488
- match callee.as_ref() {
6489
- Expr::Ident { name, .. } => cand.contains(name.as_ref()),
6490
- Expr::Member { object, prop: MemberProp::Name { name: m, .. }, .. } => {
6491
- matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
6492
- && args.len() == 1
6493
- && matches!(
6494
- m.as_ref(),
6495
- "sqrt" | "sin" | "cos" | "tan" | "abs" | "floor" | "ceil" | "exp"
6496
- | "trunc" | "log"
6497
- )
6498
- }
6499
- _ => false,
8806
+ }
8807
+ Statement::For { init, body, .. } => {
8808
+ if let Some(i) = init {
8809
+ Self::agg_collect_aliases(i, p, out);
6500
8810
  }
8811
+ Self::agg_collect_aliases(body, p, out);
6501
8812
  }
6502
- _ => false,
8813
+ Statement::While { body, .. }
8814
+ | Statement::DoWhile { body, .. }
8815
+ | Statement::ForOf { body, .. } => Self::agg_collect_aliases(body, p, out),
8816
+ _ => {}
6503
8817
  }
6504
8818
  }
6505
8819
 
6506
- /// Every `return` in `s` yields a numeric-shaped value, so a native `-> f64` body is sound.
6507
- /// Lets an unannotated-but-numeric-returning fn (e.g. `function fib(n) {...}` after M4 typed
6508
- /// the param) become M5-eligible.
6509
- fn returns_numeric(
8820
+ fn agg_stmt_writes(
6510
8821
  s: &Statement,
6511
- params: &std::collections::HashSet<String>,
6512
- cand: &std::collections::HashSet<String>,
8822
+ p: &str,
8823
+ aliases: &std::collections::HashSet<String>,
6513
8824
  ) -> bool {
6514
8825
  match s {
6515
8826
  Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
6516
- statements.iter().all(|x| Self::returns_numeric(x, params, cand))
6517
- }
6518
- Statement::Return { value, .. } => {
6519
- value.as_ref().is_some_and(|e| Self::numeric_shaped(e, params, cand))
6520
- }
6521
- Statement::If { then_branch, else_branch, .. } => {
6522
- Self::returns_numeric(then_branch, params, cand)
6523
- && else_branch.as_ref().is_none_or(|e| Self::returns_numeric(e, params, cand))
8827
+ statements.iter().any(|x| Self::agg_stmt_writes(x, p, aliases))
6524
8828
  }
6525
- Statement::While { body, .. } | Statement::For { body, .. } => {
6526
- Self::returns_numeric(body, params, cand)
8829
+ Statement::ExprStmt { expr, .. } => Self::agg_expr_writes(expr, p, aliases),
8830
+ Statement::If {
8831
+ then_branch,
8832
+ else_branch,
8833
+ ..
8834
+ } => {
8835
+ Self::agg_stmt_writes(then_branch, p, aliases)
8836
+ || else_branch
8837
+ .as_ref()
8838
+ .is_some_and(|e| Self::agg_stmt_writes(e, p, aliases))
6527
8839
  }
6528
- _ => true, // no return in this statement form
8840
+ Statement::For { body, .. }
8841
+ | Statement::While { body, .. }
8842
+ | Statement::DoWhile { body, .. }
8843
+ | Statement::ForOf { body, .. } => Self::agg_stmt_writes(body, p, aliases),
8844
+ _ => false,
6529
8845
  }
6530
8846
  }
6531
8847
 
6532
- /// `e` evaluates to a number: built from numeric params, number literals, ARITHMETIC binops
6533
- /// (comparisons/logical yield bool → excluded), numeric unary, conditionals, and calls to
6534
- /// eligible native fns / 1-arg Math.
6535
- fn numeric_shaped(
8848
+ fn agg_expr_writes(
6536
8849
  e: &Expr,
6537
- params: &std::collections::HashSet<String>,
6538
- cand: &std::collections::HashSet<String>,
8850
+ p: &str,
8851
+ aliases: &std::collections::HashSet<String>,
6539
8852
  ) -> bool {
6540
8853
  match e {
6541
- Expr::Literal { value: Literal::Number(_), .. } => true,
6542
- Expr::Ident { name, .. } => params.contains(name.as_ref()),
6543
- Expr::Binary { left, op, right, .. } => {
6544
- matches!(
6545
- op,
6546
- BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow
6547
- ) && Self::numeric_shaped(left, params, cand)
6548
- && Self::numeric_shaped(right, params, cand)
6549
- }
6550
- Expr::Unary { op, operand, .. } => {
6551
- matches!(op, UnaryOp::Neg | UnaryOp::Pos)
6552
- && Self::numeric_shaped(operand, params, cand)
6553
- }
6554
- Expr::Conditional { then_branch, else_branch, .. } => {
6555
- Self::numeric_shaped(then_branch, params, cand)
6556
- && Self::numeric_shaped(else_branch, params, cand)
6557
- }
6558
- Expr::Call { callee, .. } => match callee.as_ref() {
6559
- Expr::Ident { name, .. } => cand.contains(name.as_ref()),
6560
- Expr::Member { object, prop: MemberProp::Name { name: m, .. }, .. } => {
6561
- matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
6562
- && matches!(
6563
- m.as_ref(),
6564
- "sqrt" | "sin" | "cos" | "tan" | "abs" | "floor" | "ceil" | "exp"
6565
- | "trunc" | "log"
6566
- )
8854
+ Expr::MemberAssign { object, .. } => match object.as_ref() {
8855
+ Expr::Ident { name, .. } => aliases.contains(name.as_ref()),
8856
+ Expr::Index { object: io, .. } => {
8857
+ matches!(io.as_ref(), Expr::Ident { name, .. } if name.as_ref() == p)
6567
8858
  }
6568
8859
  _ => false,
6569
8860
  },
8861
+ Expr::IndexAssign { object, .. } => {
8862
+ matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == p)
8863
+ }
6570
8864
  _ => false,
6571
8865
  }
6572
8866
  }
6573
8867
 
6574
- /// Emit `fn name_native(p: f64, ...) -> f64 { ... }` (top level) for each eligible fn.
6575
- fn emit_native_fns(&mut self, statements: &[Statement]) -> Result<(), CompileError> {
6576
- for s in statements {
6577
- if let Statement::FunDecl { name, params, body, .. } = s {
6578
- if !self.native_fns.contains(name.as_ref()) {
6579
- continue;
6580
- }
6581
- let plist: Vec<String> = params
6582
- .iter()
6583
- .filter_map(|p| match p {
6584
- FunParam::Simple(tp) => {
6585
- Some(format!("mut {}: f64", Self::escape_ident(tp.name.as_ref())))
6586
- }
6587
- _ => None,
6588
- })
6589
- .collect();
6590
- self.type_context.push_scope();
6591
- for p in params {
6592
- if let FunParam::Simple(tp) = p {
6593
- self.type_context.define(tp.name.as_ref(), RustType::F64);
6594
- }
8868
+ /// Top-level `let` names whose initializer is a numeric constant the only globals safe to
8869
+ /// thread into an aggregate fn as a trailing `f64` param. A `let bodies = makeBodies()` or
8870
+ /// `let t0 = Date.now()` is excluded so it can never be mistyped as `f64`.
8871
+ fn collect_toplevel_global_lets(program: &Program) -> std::collections::HashSet<String> {
8872
+ let mut out: std::collections::HashSet<String> = std::collections::HashSet::new();
8873
+ for s in &program.statements {
8874
+ if let Statement::VarDecl {
8875
+ name,
8876
+ init: Some(e),
8877
+ ..
8878
+ } = s
8879
+ {
8880
+ if Self::expr_is_numeric_const(e, &out) {
8881
+ out.insert(name.to_string());
6595
8882
  }
6596
- self.writeln(&format!("fn {}_native({}) -> f64 {{", Self::escape_ident(name.as_ref()), plist.join(", ")));
6597
- self.indent += 1;
6598
- self.emit_native_fn_body(body)?;
6599
- // Functions that fall off the end without returning: JS yields undefined; an
6600
- // eligible numeric fn shouldn't, but emit a default to keep `-> f64` total.
6601
- self.writeln("0.0");
6602
- self.indent -= 1;
6603
- self.writeln("}");
6604
- self.type_context.pop_scope();
6605
8883
  }
6606
8884
  }
6607
- Ok(())
8885
+ out
6608
8886
  }
6609
8887
 
6610
- fn emit_native_fn_body(&mut self, stmt: &Statement) -> Result<(), CompileError> {
6611
- match stmt {
8888
+ /// Conservatively: a numeric literal, an arithmetic combination of such, or a reference to an
8889
+ /// already-proven numeric global (`numeric` carries the names accepted so far, in source order).
8890
+ fn expr_is_numeric_const(e: &Expr, numeric: &std::collections::HashSet<String>) -> bool {
8891
+ match e {
8892
+ Expr::Literal {
8893
+ value: Literal::Number(_),
8894
+ ..
8895
+ } => true,
8896
+ Expr::Ident { name, .. } => numeric.contains(name.as_ref()),
8897
+ Expr::Unary {
8898
+ op: UnaryOp::Neg | UnaryOp::Pos,
8899
+ operand,
8900
+ ..
8901
+ } => Self::expr_is_numeric_const(operand, numeric),
8902
+ Expr::Binary {
8903
+ left, op, right, ..
8904
+ } => {
8905
+ matches!(
8906
+ op,
8907
+ BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow
8908
+ ) && Self::expr_is_numeric_const(left, numeric)
8909
+ && Self::expr_is_numeric_const(right, numeric)
8910
+ }
8911
+ _ => false,
8912
+ }
8913
+ }
8914
+
8915
+ /// Captured globals a fn body references: free idents that are top-level globals, excluding
8916
+ /// the fn's own params, its locals, the other group-fn names, and the struct alias.
8917
+ fn agg_captured_globals(
8918
+ body: &Statement,
8919
+ params: &[FunParam],
8920
+ globals: &std::collections::HashSet<String>,
8921
+ group_fns: &std::collections::HashMap<String, AggFnSig>,
8922
+ self_name: &str,
8923
+ alias: &str,
8924
+ ) -> Vec<String> {
8925
+ let mut idents: std::collections::HashSet<String> = std::collections::HashSet::new();
8926
+ Self::collect_stmt_idents(body, &mut idents);
8927
+ let mut locals: std::collections::HashSet<String> = std::collections::HashSet::new();
8928
+ Self::collect_local_var_names(body, &mut locals);
8929
+ let pnames: std::collections::HashSet<String> = params
8930
+ .iter()
8931
+ .flat_map(|p| p.bound_names())
8932
+ .map(|n| n.to_string())
8933
+ .collect();
8934
+ let mut out: Vec<String> = idents
8935
+ .into_iter()
8936
+ .filter(|id| {
8937
+ globals.contains(id)
8938
+ && !pnames.contains(id)
8939
+ && !locals.contains(id)
8940
+ && !group_fns.contains_key(id)
8941
+ && id != self_name
8942
+ && id != alias
8943
+ })
8944
+ .collect();
8945
+ out.sort();
8946
+ out
8947
+ }
8948
+
8949
+ /// Does `s` contain a `return <value>` (vs only bare `return;` / no return)?
8950
+ fn stmt_returns_value(s: &Statement) -> bool {
8951
+ match s {
8952
+ Statement::Return { value, .. } => value.is_some(),
6612
8953
  Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
6613
- for s in statements {
6614
- self.emit_native_fn_body(s)?;
6615
- }
8954
+ statements.iter().any(Self::stmt_returns_value)
6616
8955
  }
6617
- Statement::Return { value, .. } => {
6618
- let e = value.as_ref().expect("eligible return has a value");
6619
- let (code, ty) = self.emit_typed_expr(e)?;
6620
- let f = if ty == RustType::F64 {
6621
- code
6622
- } else if ty == RustType::Value {
6623
- RustType::F64.from_value_expr(&code)
6624
- } else {
6625
- code
6626
- };
6627
- self.writeln(&format!("return {};", f));
8956
+ Statement::If {
8957
+ then_branch,
8958
+ else_branch,
8959
+ ..
8960
+ } => {
8961
+ Self::stmt_returns_value(then_branch)
8962
+ || else_branch.as_ref().is_some_and(|e| Self::stmt_returns_value(e))
6628
8963
  }
6629
- Statement::If { cond, then_branch, else_branch, .. } => {
6630
- let (c, ct) = self.emit_typed_expr(cond)?;
6631
- let c_bool = match ct {
6632
- RustType::Bool => c,
6633
- RustType::F64 => format!("({} != 0.0)", c),
6634
- _ => format!("{}.is_truthy()", c),
8964
+ Statement::For { body, .. }
8965
+ | Statement::While { body, .. }
8966
+ | Statement::DoWhile { body, .. }
8967
+ | Statement::ForOf { body, .. } => Self::stmt_returns_value(body),
8968
+ _ => false,
8969
+ }
8970
+ }
8971
+
8972
+ /// Lower an expression that JS will coerce to **int32** inside a bitwise/shift computation,
8973
+ /// staying in the integer domain instead of round-tripping every intermediate through `f64`.
8974
+ ///
8975
+ /// Returns `Ok(Some(code))` where `code` is an `i32`-typed Rust expression equal to
8976
+ /// `ToInt32(e)`, or `Ok(None)` if a leaf can't be proven `F64` (then the caller keeps the
8977
+ /// existing per-op lowering — purely additive, never a regression).
8978
+ ///
8979
+ /// This is behaviour-identical to the nested `to_int32`/`to_uint32` lowering: an intermediate
8980
+ /// `(i32 as f64)` immediately re-narrowed by `to_int32` is exact (every `i32` is representable
8981
+ /// in `f64`, and `to_int32` of a finite value recovers it), so erasing it changes nothing but
8982
+ /// the round-trips. Crucially, only bitwise/shift nodes recurse — an `f64` `*`/`+`/`-` node is
8983
+ /// a *leaf* here, so e.g. `(h * 16777619) >>> 0` keeps its `f64` multiply (the 2^53 rule: the
8984
+ /// product exceeds 2^53 and must round in `f64` *before* `ToUint32`, exactly as V8 does).
8985
+ fn emit_int32_operand(&mut self, e: &Expr) -> Result<Option<String>, CompileError> {
8986
+ if let Expr::Binary {
8987
+ left, op, right, ..
8988
+ } = e
8989
+ {
8990
+ let bitwise = matches!(
8991
+ op,
8992
+ BinOp::BitAnd
8993
+ | BinOp::BitOr
8994
+ | BinOp::BitXor
8995
+ | BinOp::Shl
8996
+ | BinOp::Shr
8997
+ | BinOp::UShr
8998
+ );
8999
+ if bitwise {
9000
+ let li = match self.emit_int32_operand(left)? {
9001
+ Some(c) => c,
9002
+ None => return Ok(None),
6635
9003
  };
6636
- self.writeln(&format!("if {} {{", c_bool));
6637
- self.indent += 1;
6638
- self.emit_native_fn_body(then_branch)?;
6639
- self.indent -= 1;
6640
- if let Some(eb) = else_branch {
6641
- self.writeln("} else {");
6642
- self.indent += 1;
6643
- self.emit_native_fn_body(eb)?;
6644
- self.indent -= 1;
6645
- }
6646
- self.writeln("}");
6647
- }
6648
- Statement::ExprStmt { expr, .. } => {
6649
- let (code, _) = self.emit_typed_expr(expr)?;
6650
- self.writeln(&format!("{};", code));
9004
+ let ri = match self.emit_int32_operand(right)? {
9005
+ Some(c) => c,
9006
+ None => return Ok(None),
9007
+ };
9008
+ // Shift counts: `(ri as u32)` shares its low 5 bits with `to_uint32(rhs)`, and
9009
+ // `wrapping_sh*` masks the count mod 32 — exactly JS's `count & 31`.
9010
+ let code = match op {
9011
+ BinOp::BitAnd => format!("({} & {})", li, ri),
9012
+ BinOp::BitOr => format!("({} | {})", li, ri),
9013
+ BinOp::BitXor => format!("({} ^ {})", li, ri),
9014
+ BinOp::Shl => format!("({}).wrapping_shl(({}) as u32)", li, ri),
9015
+ BinOp::Shr => format!("({}).wrapping_shr(({}) as u32)", li, ri),
9016
+ // `>>>` is a logical shift on the uint32 view; reinterpret back to `i32` to
9017
+ // stay in the integer domain (the unsigned value is recovered at the f64 edge).
9018
+ BinOp::UShr => {
9019
+ format!("((({}) as u32).wrapping_shr(({}) as u32) as i32)", li, ri)
9020
+ }
9021
+ _ => unreachable!(),
9022
+ };
9023
+ return Ok(Some(code));
9024
+ }
9025
+ }
9026
+ // Leaf: fold only when it is a plain `f64` (so `to_int32` applies directly). `to_int32`
9027
+ // keeps its `is_finite` guard here — a leaf may legitimately be NaN/±Infinity (→ 0).
9028
+ let (code, ty) = self.emit_typed_expr(e)?;
9029
+ if ty == RustType::F64 {
9030
+ // When the leaf is an ARITHMETIC node PROVABLY finite with `|x| < 2^62` (operands are
9031
+ // i32-register reads and finite literals — e.g. the FNV `h * 16777619` excursion), drop
9032
+ // the `is_finite` guard and Rust's saturating cast and truncate directly. Bit-identical
9033
+ // on this domain (`x as i64` truncates toward zero = JS ToInt32 truncation; `as i32` =
9034
+ // modulo 2^32), a few instructions cheaper per iteration. Emitted inline so the generated
9035
+ // crate needs no new runtime symbol. Any unproven leaf keeps the guarded `to_int32`.
9036
+ if matches!(e, Expr::Binary { .. }) && self.f64_finite_bounded_below_2pow62(e) {
9037
+ Ok(Some(format!(
9038
+ "(unsafe {{ ({}).to_int_unchecked::<i64>() }} as i32)",
9039
+ code
9040
+ )))
9041
+ } else {
9042
+ Ok(Some(format!("tishlang_runtime::to_int32({})", code)))
6651
9043
  }
6652
- _ => unreachable!("emit_native_fn_body: eligibility guarantees only handled statements"),
9044
+ } else if ty == RustType::I32 {
9045
+ // An `I32` loop-accumulator already holds its JS ToInt32 bit-pattern in an integer
9046
+ // register — feed it straight in, NO `to_int32` round-trip. This is the perf win: the
9047
+ // per-op `f64`→i32 narrowing across the hash loop collapses to a register read.
9048
+ Ok(Some(code))
9049
+ } else {
9050
+ Ok(None)
6653
9051
  }
6654
- Ok(())
6655
9052
  }
6656
9053
 
6657
9054
  fn emit_typed_expr(&mut self, expr: &Expr) -> Result<(String, RustType), CompileError> {
@@ -6697,7 +9094,64 @@ impl Codegen {
6697
9094
  let (l, lt) = self.emit_typed_expr(left)?;
6698
9095
  let (r, rt) = self.emit_typed_expr(right)?;
6699
9096
 
9097
+ // An `I32` loop-accumulator (the i32-loop-var lowering) used in a NON-bitwise
9098
+ // expression reads as its signed int32 value coerced to `f64` — every i32 is exact
9099
+ // in f64. Bitwise/shift parents never see this: they recurse into the raw AST via
9100
+ // `emit_int32_operand`, which reads the i32 register directly. So coercing here only
9101
+ // governs arithmetic/relational reads (e.g. the `h * 16777619` excursion), where the
9102
+ // operand must be f64 to keep JS Number semantics.
9103
+ let (l, lt) = if lt == RustType::I32 {
9104
+ (format!("(({}) as f64)", l), RustType::F64)
9105
+ } else {
9106
+ (l, lt)
9107
+ };
9108
+ let (r, rt) = if rt == RustType::I32 {
9109
+ (format!("(({}) as f64)", r), RustType::F64)
9110
+ } else {
9111
+ (r, rt)
9112
+ };
9113
+
6700
9114
  if let Some(result_ty) = RustType::result_type_of_binop(*op, &lt, &rt) {
9115
+ // Bitwise/shift over numbers: lower the *whole* chain in the int32 domain so
9116
+ // intermediate `to_int32`/`to_uint32`↔`f64` round-trips collapse (the win `>>>`
9117
+ // exists for — crypto/hashing loops). Only fires when every leaf proves `f64`;
9118
+ // otherwise we fall through to the per-op lowering below. Behaviour-identical
9119
+ // (see `emit_int32_operand`), and the gauntlet's `typed == boxed == node` check
9120
+ // gates any divergence.
9121
+ if matches!(
9122
+ op,
9123
+ BinOp::BitAnd
9124
+ | BinOp::BitOr
9125
+ | BinOp::BitXor
9126
+ | BinOp::Shl
9127
+ | BinOp::Shr
9128
+ | BinOp::UShr
9129
+ ) && result_ty == RustType::F64
9130
+ {
9131
+ if let Some(int_code) = self.emit_int32_operand(expr)? {
9132
+ // `>>>` yields a uint32 Number; the others yield a signed int32 Number.
9133
+ let f64_code = if matches!(op, BinOp::UShr) {
9134
+ format!("(({}) as u32 as f64)", int_code)
9135
+ } else {
9136
+ format!("(({}) as f64)", int_code)
9137
+ };
9138
+ return Ok((f64_code, RustType::F64));
9139
+ }
9140
+ }
9141
+ // Integer remainder (#174): `x % c` where the dividend `x` is a proven integer
9142
+ // in (-2^53, 2^53) and `c` a positive integer literal → `(x as i64) % c` instead
9143
+ // of `fmod`. Bit-identical (x is exactly an integer in f64; Rust `%` and `fmod`
9144
+ // both truncate toward zero), and far faster — the fmod in LCG/hash recurrences.
9145
+ if matches!(op, BinOp::Mod) && result_ty == RustType::F64 {
9146
+ if let Some(c) = Self::int_literal_value_of(right).filter(|&c| c > 0) {
9147
+ if self.int_range(left, &self.int_range_locals).is_some() {
9148
+ return Ok((
9149
+ format!("(((({}) as i64) % {}i64) as f64)", l, c),
9150
+ RustType::F64,
9151
+ ));
9152
+ }
9153
+ }
9154
+ }
6701
9155
  // Both sides are compatible native types → emit native op.
6702
9156
  let code = match op {
6703
9157
  BinOp::Add if result_ty == RustType::String => {
@@ -6753,6 +9207,37 @@ impl Codegen {
6753
9207
  return Ok((code, result_ty));
6754
9208
  }
6755
9209
 
9210
+ // Mixed numeric relational: one side is a native `f64`, the other a boxed `Value`
9211
+ // (e.g. nsieve's `while (k < n)` where `k` is f64 and the param `n` stayed boxed).
9212
+ // JS does a numeric comparison here — the f64 side forces ToNumber on the other —
9213
+ // so coerce the Value inline (`as_number().unwrap_or(NaN)`) and compare natively,
9214
+ // instead of boxing the f64 side and paying `ops::{lt,le,gt,ge}` + `Value::Bool` +
9215
+ // `is_truthy` every iteration. Behaviour-identical to that boxed path for every
9216
+ // input: a non-number Value coerces to NaN, so all comparisons are `false`, exactly
9217
+ // as `ops::*` returns `false` outside the (Number,Number)/(String,String) cases —
9218
+ // and (String,String) can't reach here since one side is f64.
9219
+ if matches!(op, BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge)
9220
+ && (lt == RustType::F64 || rt == RustType::F64)
9221
+ {
9222
+ let coerce = |code: &str, ty: &RustType| match ty {
9223
+ RustType::F64 => Some(code.to_string()),
9224
+ RustType::Value => {
9225
+ Some(format!("({}).as_number().unwrap_or(f64::NAN)", code))
9226
+ }
9227
+ _ => None,
9228
+ };
9229
+ if let (Some(lc), Some(rc)) = (coerce(&l, &lt), coerce(&r, &rt)) {
9230
+ let sym = match op {
9231
+ BinOp::Lt => "<",
9232
+ BinOp::Le => "<=",
9233
+ BinOp::Gt => ">",
9234
+ BinOp::Ge => ">=",
9235
+ _ => unreachable!(),
9236
+ };
9237
+ return Ok((format!("({} {} {})", lc, sym, rc), RustType::Bool));
9238
+ }
9239
+ }
9240
+
6756
9241
  // Fall back: convert both sides to Value and use the runtime.
6757
9242
  let lv = if lt.is_native() {
6758
9243
  lt.to_value_expr(&l)
@@ -6853,6 +9338,13 @@ impl Codegen {
6853
9338
  // skipping the boxed value_call per element. Only methods whose Rust f64 op
6854
9339
  // matches JS semantics (round half-up & sign(0) differ → left to the runtime).
6855
9340
  Expr::Call { callee, args, .. } => {
9341
+ // #177: a de-virtualized aggregate fn used in native arithmetic (e.g. `energy(bodies)`
9342
+ // feeding an f64 expression) → call `name_agg(..)` returning the native type.
9343
+ if !self.aggregate_fns.is_empty() {
9344
+ if let Some((code, ty)) = self.try_emit_toplevel_agg_call(callee, args, false)? {
9345
+ return Ok((code, ty));
9346
+ }
9347
+ }
6856
9348
  // M5: direct call to an eligible native fn -> `name_native(<native args>)`.
6857
9349
  if let Expr::Ident { name: fname, .. } = callee.as_ref() {
6858
9350
  if self.native_fns.contains(fname.as_ref()) {
@@ -7206,13 +9698,69 @@ impl Codegen {
7206
9698
  RustType::Value => RustType::F64.from_value_expr(&init_code),
7207
9699
  _ => return Ok(None),
7208
9700
  };
9701
+ let acc_esc = Self::escape_ident(acc.as_ref()).into_owned();
9702
+ let x_esc = Self::escape_ident(x.as_ref()).into_owned();
9703
+
9704
+ // ── i64 fast path (#174) ────────────────────────────────────────────────────────
9705
+ // When the receiver is an integer-literal array (element range known) and the body
9706
+ // lowers to native `i64` arithmetic with every node proven integral and < 2^53, run
9707
+ // the fold in `i64` — eliminating `fmod`/f64 round-trips in the hot loop (V8 keeps
9708
+ // these small-integer folds in int registers too). The accumulator's bounded integer
9709
+ // range is found by a small fixpoint seeded from the init's range; bit-identical to
9710
+ // the f64 fold because every intermediate is an exact integer < 2^53.
9711
+ if let Some(elem_r) = self.array_elem_ranges.get(recv_name).copied() {
9712
+ if let Some(init_r) = self.int_range(init_e, &self.int_range_locals) {
9713
+ let mut base = self.int_range_locals.clone();
9714
+ base.insert(x.to_string(), elem_r);
9715
+ let mut acc_r = init_r;
9716
+ let mut converged = false;
9717
+ for _ in 0..6 {
9718
+ let mut m = base.clone();
9719
+ m.insert(acc.to_string(), acc_r);
9720
+ match self.int_range(be, &m) {
9721
+ Some((blo, bhi)) => {
9722
+ let n = (acc_r.0.min(blo), acc_r.1.max(bhi));
9723
+ if n == acc_r {
9724
+ converged = true;
9725
+ break;
9726
+ }
9727
+ acc_r = n;
9728
+ }
9729
+ None => break,
9730
+ }
9731
+ }
9732
+ // Confirm the range is an inductive invariant, then emit the body in i64.
9733
+ let mut m = base.clone();
9734
+ m.insert(acc.to_string(), acc_r);
9735
+ let inductive = converged
9736
+ && matches!(self.int_range(be, &m),
9737
+ Some((blo, bhi)) if blo >= acc_r.0 && bhi <= acc_r.1);
9738
+ if inductive {
9739
+ let i64vars: HashSet<String> =
9740
+ std::iter::once(acc.to_string()).collect();
9741
+ self.type_context.push_scope();
9742
+ self.type_context.define(acc.as_ref(), RustType::F64);
9743
+ self.type_context.define(x.as_ref(), RustType::F64);
9744
+ let body_i64 = self.emit_i64(be, &i64vars, &m)?;
9745
+ self.type_context.pop_scope();
9746
+ if let Some(body_i64) = body_i64 {
9747
+ return Ok(Some((
9748
+ format!(
9749
+ "{{ let mut {acc}: i64 = (({init}) as i64); for {x} in {recv}.iter().copied() {{ {acc} = {body}; }} {acc} as f64 }}",
9750
+ acc = acc_esc, init = init_f64, x = x_esc, recv = recv, body = body_i64
9751
+ ),
9752
+ RustType::F64,
9753
+ )));
9754
+ }
9755
+ }
9756
+ }
9757
+ }
9758
+
7209
9759
  let (body_code, body_ty) =
7210
9760
  emit_with(self, &[(&acc, RustType::F64), (&x, RustType::F64)])?;
7211
9761
  if body_ty != RustType::F64 {
7212
9762
  return Ok(None);
7213
9763
  }
7214
- let acc_esc = Self::escape_ident(acc.as_ref()).into_owned();
7215
- let x_esc = Self::escape_ident(x.as_ref()).into_owned();
7216
9764
  Ok(Some((
7217
9765
  format!(
7218
9766
  "{{ let mut {acc}: f64 = {init}; for {x} in {recv}.iter().copied() {{ {acc} = {body}; }} {acc} }}",
@@ -7309,8 +9857,7 @@ impl Codegen {
7309
9857
  l, r
7310
9858
  ),
7311
9859
  BinOp::Pow => format!(
7312
- "Value::Number({{ let Value::Number(a) = &({}) else {{ panic!() }}; \
7313
- let Value::Number(b) = &({}) else {{ panic!() }}; a.powf(*b) }})",
9860
+ "Value::Number(tishlang_runtime::to_number_value(&({})).powf(tishlang_runtime::to_number_value(&({}))))",
7314
9861
  l, r
7315
9862
  ),
7316
9863
  BinOp::StrictEq => format!("Value::Bool({}.strict_eq(&{}))", l, r),
@@ -7319,14 +9866,18 @@ impl Codegen {
7319
9866
  BinOp::Le => format!("tishlang_runtime::ops::le(&{}, &{})", l, r),
7320
9867
  BinOp::Gt => format!("tishlang_runtime::ops::gt(&{}, &{})", l, r),
7321
9868
  BinOp::Ge => format!("tishlang_runtime::ops::ge(&{}, &{})", l, r),
7322
- BinOp::And => format!("Value::Bool({}.is_truthy() && {}.is_truthy())", l, r),
7323
- BinOp::Or => format!("Value::Bool({}.is_truthy() || {}.is_truthy())", l, r),
9869
+ // Short-circuit + value-returning && / || (JS, #240): yield the deciding OPERAND, not a
9870
+ // coerced boolean (`five() && 7` is `7`, not `true`). The right operand sits inside the
9871
+ // branch, so its side effects only run when reached. (Typed `bool && bool` uses Rust's
9872
+ // own `&&`/`||` above, where returning the bool already IS the operand.)
9873
+ BinOp::And => format!("{{ let __l = {}; if __l.is_truthy() {{ {} }} else {{ __l }} }}", l, r),
9874
+ BinOp::Or => format!("{{ let __l = {}; if __l.is_truthy() {{ __l }} else {{ {} }} }}", l, r),
7324
9875
  BinOp::BitAnd => Self::emit_bitwise_binop(l, r, "&"),
7325
9876
  BinOp::BitOr => Self::emit_bitwise_binop(l, r, "|"),
7326
9877
  BinOp::BitXor => Self::emit_bitwise_binop(l, r, "^"),
7327
- BinOp::Shl => Self::emit_shift_binop(l, r, "to_int32", "wrapping_shl"),
7328
- BinOp::Shr => Self::emit_shift_binop(l, r, "to_int32", "wrapping_shr"),
7329
- BinOp::UShr => Self::emit_shift_binop(l, r, "to_uint32", "wrapping_shr"),
9878
+ BinOp::Shl => Self::emit_shift_binop(l, r, "to_int32_value", "wrapping_shl"),
9879
+ BinOp::Shr => Self::emit_shift_binop(l, r, "to_int32_value", "wrapping_shr"),
9880
+ BinOp::UShr => Self::emit_shift_binop(l, r, "to_uint32_value", "wrapping_shr"),
7330
9881
  BinOp::In => format!("tish_in_operator(&{}, &{})", l, r),
7331
9882
  BinOp::Eq | BinOp::Ne => {
7332
9883
  return Err(CompileError::new(