@tishlang/tish 2.2.5 → 2.8.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 (53) 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 +1 -1
  4. package/crates/tish/src/cli_help.rs +12 -0
  5. package/crates/tish/src/main.rs +69 -11
  6. package/crates/tish_ast/src/ast.rs +12 -5
  7. package/crates/tish_build_utils/src/lib.rs +37 -0
  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 +11 -10
  17. package/crates/tish_compile/src/codegen.rs +1386 -42
  18. package/crates/tish_compile/src/infer.rs +16 -16
  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 +26 -9
  22. package/crates/tish_compile_js/src/codegen.rs +55 -4
  23. package/crates/tish_compile_js/src/tests_jsx.rs +44 -0
  24. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +1 -0
  25. package/crates/tish_core/Cargo.toml +2 -0
  26. package/crates/tish_core/src/json.rs +135 -34
  27. package/crates/tish_core/src/lib.rs +23 -1
  28. package/crates/tish_core/src/value.rs +64 -11
  29. package/crates/tish_eval/src/eval.rs +144 -197
  30. package/crates/tish_eval/src/natives.rs +45 -45
  31. package/crates/tish_eval/src/regex.rs +35 -27
  32. package/crates/tish_eval/src/value.rs +8 -0
  33. package/crates/tish_fmt/src/lib.rs +46 -2
  34. package/crates/tish_lint/src/bin/tish-lint.rs +17 -8
  35. package/crates/tish_lint/src/lib.rs +197 -21
  36. package/crates/tish_lsp/Cargo.toml +7 -1
  37. package/crates/tish_lsp/src/import_goto.rs +52 -7
  38. package/crates/tish_lsp/src/main.rs +913 -145
  39. package/crates/tish_opt/src/lib.rs +5 -3
  40. package/crates/tish_parser/src/lib.rs +23 -5
  41. package/crates/tish_parser/src/parser.rs +35 -35
  42. package/crates/tish_resolve/src/lib.rs +567 -17
  43. package/crates/tish_runtime/src/lib.rs +58 -18
  44. package/crates/tish_ui/src/jsx.rs +2 -2
  45. package/crates/tish_vm/src/jit.rs +212 -10
  46. package/crates/tish_vm/src/vm.rs +39 -18
  47. package/crates/tish_wasm/src/lib.rs +116 -22
  48. package/package.json +1 -1
  49. package/platform/darwin-arm64/tish +0 -0
  50. package/platform/darwin-x64/tish +0 -0
  51. package/platform/linux-arm64/tish +0 -0
  52. package/platform/linux-x64/tish +0 -0
  53. 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, .. }
@@ -782,6 +782,27 @@ struct Codegen {
782
782
  /// `RustType::Value`. This is the rust-AOT analogue of the VM array-JIT bailing to the
783
783
  /// interpreter on a non-numeric element. See `collect_demoted_numeric_locals`.
784
784
  demoted_numeric_locals: std::collections::HashSet<String>,
785
+ /// Integer-range lattice (#174): names of `f64` locals the analysis proves always hold an
786
+ /// integer within `[min, max]`, both strictly inside `(-2^53, 2^53)` so `as i64` is exact and
787
+ /// `i64` arithmetic is bit-identical to the `f64` the interpreter/VM use. Lets the codegen
788
+ /// lower e.g. `x % c` to a fast integer remainder instead of `fmod`. Conservative: a name absent
789
+ /// here is treated as unbounded. Populated by `collect_int_range_locals`.
790
+ int_range_locals: std::collections::HashMap<String, (i64, i64)>,
791
+ /// Integer-range lattice (#174): locals that are always INTEGER-valued (an `f64` with zero
792
+ /// fractional part), possibly of unbounded magnitude — unlike `int_range_locals`. Loop counters
793
+ /// (`i = i + 1`) qualify even though their magnitude isn't bounded. Used to prove a modulo
794
+ /// result like `r % 97` is integral, so it can seed a fold accumulator's bounded range.
795
+ int_valued_locals: std::collections::HashSet<String>,
796
+ /// Integer-range lattice (#174): `number[]` locals initialized from an array literal of integer
797
+ /// literals → the inclusive element range, both inside `(-2^53, 2^53)`. Bounds a native fold's
798
+ /// element variable so the fold body can lower to native `i64` arithmetic.
799
+ array_elem_ranges: std::collections::HashMap<String, (i64, i64)>,
800
+ /// i32-loop-var lowering: names of `number` accumulators a per-body analysis proved can live
801
+ /// in an `i32` register across a bitwise/hash hot loop (`h` in FNV) instead of round-tripping
802
+ /// `f64`↔`i32` every op. Each is declared `let mut h: i32`, every reassignment lowers via
803
+ /// `emit_int32_operand`, and reads coerce `(h as f64)`. See `collect_i32_loop_vars` for the
804
+ /// (strict) eligibility/soundness gate. Scoped per function body / top level.
805
+ i32_loop_vars: std::collections::HashSet<String>,
785
806
  /// Scopes of names whose Rust binding is actually `Rc<RefCell<_>>` (emitted at VarDecl).
786
807
  /// `refcell_wrapped_vars` alone is insufficient: it is set by prepasses before decl may run.
787
808
  rc_cell_storage_scopes: Vec<std::collections::HashSet<String>>,
@@ -836,6 +857,10 @@ impl Codegen {
836
857
  refcell_wrapped_vars: std::collections::HashSet::new(),
837
858
  native_fns: std::collections::HashSet::new(),
838
859
  demoted_numeric_locals: std::collections::HashSet::new(),
860
+ int_range_locals: std::collections::HashMap::new(),
861
+ int_valued_locals: std::collections::HashSet::new(),
862
+ array_elem_ranges: std::collections::HashMap::new(),
863
+ i32_loop_vars: std::collections::HashSet::new(),
839
864
  rc_cell_storage_scopes: vec![std::collections::HashSet::new()],
840
865
  usage_analyzer: None,
841
866
  type_context: TypeContext::new(),
@@ -1343,23 +1368,27 @@ impl Codegen {
1343
1368
 
1344
1369
  /// Generate code for a bitwise binary operation (`& | ^`). `to_int32` is JS ToInt32
1345
1370
  /// (modulo 2³², NaN/±Infinity → 0) — out-of-range operands wrap, not saturate.
1371
+ /// Boxed/`Value`-path bitwise op (`& | ^`). Uses the `*_value(&Value)` coercion helpers rather
1372
+ /// than a `let Value::Number(a) = &(..) else { panic!() }` block: the block bound `a`/`b`, so a
1373
+ /// nested bitwise operand (whose block *also* binds `a`/`b`) shadowed the outer binding and the
1374
+ /// generated code failed to compile (`error[E0308]`, `&&f64` vs `Value`). The helpers bind no
1375
+ /// name, so the ops compose at any nesting depth, and they coerce non-numbers to `NaN` (→ `0`)
1376
+ /// exactly like the interpreter/VM instead of panicking.
1346
1377
  fn emit_bitwise_binop(l: &str, r: &str, op: &str) -> String {
1347
1378
  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
1379
+ "Value::Number((tishlang_runtime::to_int32_value(&({})) {} tishlang_runtime::to_int32_value(&({}))) as f64)",
1380
+ l, op, r
1351
1381
  )
1352
1382
  }
1353
1383
 
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.
1384
+ /// Boxed/`Value`-path shift (`<< >> >>>`). `a_to` is the left-operand coercion helper
1385
+ /// (`to_int32_value` signed, `to_uint32_value` for the logical `>>>`); `method` is the
1386
+ /// `wrapping_sh*` call. Counts go through `to_uint32_value` then mask to 5 bits — exact JS
1387
+ /// semantics, panic-free, and composable (no name binding — see `emit_bitwise_binop`).
1357
1388
  fn emit_shift_binop(l: &str, r: &str, a_to: &str, method: &str) -> String {
1358
1389
  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
1390
+ "Value::Number(tishlang_runtime::{}(&({})).{}(tishlang_runtime::to_uint32_value(&({}))) as f64)",
1391
+ a_to, l, method, r
1363
1392
  )
1364
1393
  }
1365
1394
 
@@ -1435,7 +1464,7 @@ impl Codegen {
1435
1464
  self.write("use std::cell::RefCell;\n");
1436
1465
  self.write("use std::rc::Rc;\n");
1437
1466
  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");
1467
+ 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
1468
  if self.program_has_jsx {
1440
1469
  self.write("use tishlang_ui::{fragment_value, install_thread_local_host, native_create_root, native_use_state, ui_h, ui_text, HeadlessHost};\n");
1441
1470
  }
@@ -1519,6 +1548,12 @@ impl Codegen {
1519
1548
  // and panics on a JS string-concat result like `s = s + arr[i]`). See
1520
1549
  // `collect_demoted_numeric_locals` / `demoted_numeric_locals`.
1521
1550
  self.demoted_numeric_locals = self.collect_demoted_numeric_locals(&program.statements);
1551
+ self.int_valued_locals = Self::collect_int_valued_locals(&program.statements);
1552
+ self.int_range_locals = self.collect_int_range_locals(&program.statements);
1553
+ self.array_elem_ranges = Self::collect_array_elem_ranges(&program.statements);
1554
+ // i32-loop-var lowering: must run AFTER `int_range_locals` (the soundness backstop that
1555
+ // proves the accumulator stays an exact integer reinterpretable as i32).
1556
+ self.i32_loop_vars = self.collect_i32_loop_vars(&program.statements);
1522
1557
  if self.is_async {
1523
1558
  self.writeln("async fn run() -> Result<(), Box<dyn std::error::Error>> {");
1524
1559
  } else if self.emit_mode == crate::NativeEmitMode::EmbeddedLib {
@@ -1592,7 +1627,8 @@ impl Codegen {
1592
1627
  self.writeln(
1593
1628
  "(Arc::from(\"imul\"), Value::native(|args: &[Value]| tish_math_imul(args))),",
1594
1629
  );
1595
- // Hyperbolic / inverse-hyperbolic / cbrt / base-2/10 logs (issue #61).
1630
+ // Hyperbolic / inverse-hyperbolic / cbrt / base-2/10 logs (issue #61) + hypot/atan2 and the
1631
+ // inverse trig that were missing on the native Math but present on the vm (#247).
1596
1632
  for (name, func) in [
1597
1633
  ("sinh", "tish_math_sinh"),
1598
1634
  ("cosh", "tish_math_cosh"),
@@ -1603,6 +1639,11 @@ impl Codegen {
1603
1639
  ("cbrt", "tish_math_cbrt"),
1604
1640
  ("log2", "tish_math_log2"),
1605
1641
  ("log10", "tish_math_log10"),
1642
+ ("hypot", "tish_math_hypot"),
1643
+ ("atan2", "tish_math_atan2"),
1644
+ ("asin", "tish_math_asin"),
1645
+ ("acos", "tish_math_acos"),
1646
+ ("atan", "tish_math_atan"),
1606
1647
  ] {
1607
1648
  self.writeln(&format!(
1608
1649
  "(Arc::from(\"{name}\"), Value::native(|args: &[Value]| {func}(args))),"
@@ -1862,6 +1903,30 @@ impl Codegen {
1862
1903
  match expr {
1863
1904
  Expr::Assign { name, value, .. } => {
1864
1905
  let rust_type = self.type_context.get_type(name.as_ref());
1906
+ // i32-loop-var lowering: the accumulator lives in an `i32` register. Each
1907
+ // reassignment RHS is a bitwise/shift chain the gate proved lowers fully via
1908
+ // `emit_int32_operand` (a `>>> 0` result is u32 reinterpreted to i32; signed
1909
+ // bitwise ops yield i32 directly) — so store the i32 with NO `f64` round-trip.
1910
+ if rust_type == RustType::I32 {
1911
+ if let Some(int_code) = self.emit_int32_operand(value)? {
1912
+ let escaped = Self::escape_ident(name.as_ref());
1913
+ return Ok(format!("{} = ({}) as i32", escaped, int_code));
1914
+ }
1915
+ // Defensive: gate guarantees `Some`, but if a future RHS shape slips through,
1916
+ // fall back to a sound f64-narrowed store rather than miscompiling.
1917
+ let (val_code, val_ty) = self.emit_typed_expr(value)?;
1918
+ let v = if val_ty.is_native() {
1919
+ val_ty.to_value_expr(&val_code)
1920
+ } else {
1921
+ val_code
1922
+ };
1923
+ let escaped = Self::escape_ident(name.as_ref());
1924
+ return Ok(format!(
1925
+ "{} = {}",
1926
+ escaped,
1927
+ RustType::I32.from_value_expr(&v)
1928
+ ));
1929
+ }
1865
1930
  // String self-append `s = s + rhs` -> in-place push_str (amortized O(1)). The
1866
1931
  // general path boxes via `ops::add(Value::String(s.clone()), ...)` which clones
1867
1932
  // the whole string per concat -> O(n^2) string building. rhs must be String-typed.
@@ -2040,6 +2105,33 @@ impl Codegen {
2040
2105
  rust_type = RustType::Value;
2041
2106
  }
2042
2107
 
2108
+ // i32-loop-var lowering: a `number` accumulator the analysis proved can live in an
2109
+ // `i32` register across a bitwise/hash hot loop. Declare `let mut h: i32` with the
2110
+ // init reinterpreted via `u32` so a literal ≥ 2^31 keeps its JS ToInt32 bit-pattern.
2111
+ if rust_type == RustType::F64 && self.i32_loop_vars.contains(name.as_ref()) {
2112
+ let init_lit = init
2113
+ .as_ref()
2114
+ .and_then(|e| Self::int_literal_value_of(e));
2115
+ if let Some(v) = init_lit {
2116
+ rust_type = RustType::I32;
2117
+ self.type_context.define(name.as_ref(), rust_type.clone());
2118
+ let escaped_name = Self::escape_ident(name.as_ref());
2119
+ let mutability = if *mutable { "let mut" } else { "let" };
2120
+ // `v` is an exact integer (gate proved it); reinterpret its low 32 bits as
2121
+ // i32 = ToInt32(v), the same bit-pattern the bitwise path produces.
2122
+ self.writeln(&format!(
2123
+ "{} {}: i32 = ({}u32) as i32;",
2124
+ mutability,
2125
+ escaped_name,
2126
+ (v as i64 as u32)
2127
+ ));
2128
+ if let Some(scope) = self.outer_vars_stack.last_mut() {
2129
+ scope.push(name.to_string());
2130
+ }
2131
+ return Ok(());
2132
+ }
2133
+ }
2134
+
2043
2135
  // Track the variable type
2044
2136
  self.type_context.define(name.as_ref(), rust_type.clone());
2045
2137
 
@@ -3212,16 +3304,17 @@ impl Codegen {
3212
3304
  let o = self.emit_expr(operand)?;
3213
3305
  match op {
3214
3306
  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
- ),
3307
+ // `*_value(&Value)` coercion (no name binding) so unary ops compose over nested
3308
+ // bitwise/unary operands without the `let Value::Number(n) = &(..)` shadowing
3309
+ // miscompile, and coerce non-numbers to `NaN` like the interpreter/VM.
3310
+ UnaryOp::Neg => {
3311
+ format!("Value::Number(-tishlang_runtime::to_number_value(&({})))", o)
3312
+ }
3313
+ UnaryOp::Pos => {
3314
+ format!("Value::Number(tishlang_runtime::to_number_value(&({})))", o)
3315
+ }
3223
3316
  UnaryOp::BitNot => format!(
3224
- "Value::Number({{ let Value::Number(n) = &({}) else {{ panic!(\"Expected number\") }}; (!tishlang_runtime::to_int32(*n)) as f64 }})",
3317
+ "Value::Number((!tishlang_runtime::to_int32_value(&({}))) as f64)",
3225
3318
  o
3226
3319
  ),
3227
3320
  UnaryOp::Void => format!("{{ {}; Value::Null }}", o),
@@ -3429,9 +3522,10 @@ impl Codegen {
3429
3522
  }
3430
3523
  "split" => {
3431
3524
  let sep = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
3525
+ let limit = arg_exprs.get(1).cloned().unwrap_or_else(|| "Value::Null".to_string());
3432
3526
  return Ok(format!(
3433
- "tishlang_runtime::string_split(&{}, &{})",
3434
- obj_expr, sep
3527
+ "tishlang_runtime::string_split_limit(&{}, &{}, &{})",
3528
+ obj_expr, sep, limit
3435
3529
  ));
3436
3530
  }
3437
3531
  "trim" => {
@@ -3498,6 +3592,15 @@ impl Codegen {
3498
3592
  obj_expr, idx
3499
3593
  ));
3500
3594
  }
3595
+ "at" => {
3596
+ // `at` is on both String and Array; this match is by method name, so
3597
+ // dispatch on the runtime value type (#247).
3598
+ let idx = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Number(0.0)".to_string());
3599
+ return Ok(format!(
3600
+ "tishlang_runtime::value_at(&{}, &{})",
3601
+ obj_expr, idx
3602
+ ));
3603
+ }
3501
3604
  "charCodeAt" => {
3502
3605
  let idx = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Number(0.0)".to_string());
3503
3606
  return Ok(format!(
@@ -3599,6 +3702,20 @@ impl Codegen {
3599
3702
  obj_expr, callback
3600
3703
  ));
3601
3704
  }
3705
+ "findLast" => {
3706
+ let callback = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
3707
+ return Ok(format!(
3708
+ "tishlang_runtime::array_find_last(&{}, &{})",
3709
+ obj_expr, callback
3710
+ ));
3711
+ }
3712
+ "findLastIndex" => {
3713
+ let callback = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
3714
+ return Ok(format!(
3715
+ "tishlang_runtime::array_find_last_index(&{}, &{})",
3716
+ obj_expr, callback
3717
+ ));
3718
+ }
3602
3719
  "some" => {
3603
3720
  let callback = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
3604
3721
  return Ok(format!(
@@ -3844,7 +3961,13 @@ impl Codegen {
3844
3961
  for elem in elements {
3845
3962
  if let ArrayElement::Expr(expr) = elem {
3846
3963
  let v = self.emit_expr(expr)?;
3847
- if self.should_clone(expr) {
3964
+ // A `Value`-typed identifier (object, or a global like `NaN`/`Infinity`)
3965
+ // is emitted bare here, so moving it into the array breaks any later use
3966
+ // in the SAME expression — e.g. `[1, o].includes(o)` borrows `o` after the
3967
+ // array moved it. The scope-local last-use analysis can't see that reuse,
3968
+ // so clone every identifier element (cheap; these literals are cold, and
3969
+ // string/number idents already clone inside their `Value::*` conversion).
3970
+ if matches!(expr, Expr::Ident { .. }) || self.should_clone(expr) {
3848
3971
  els.push(format!("({}).clone()", v));
3849
3972
  } else {
3850
3973
  els.push(v);
@@ -3868,7 +3991,7 @@ impl Codegen {
3868
3991
  let mut parts = Vec::new();
3869
3992
  for prop in props {
3870
3993
  match prop {
3871
- ObjectProp::KeyValue(k, v) => {
3994
+ ObjectProp::KeyValue(k, v, _) => {
3872
3995
  let val = self.emit_expr(v)?;
3873
3996
  if self.should_clone(v) {
3874
3997
  parts.push(format!("_obj.insert(Arc::from({:?}), ({}).clone());", k.as_ref(), val));
@@ -3886,7 +4009,7 @@ impl Codegen {
3886
4009
  } else {
3887
4010
  let mut parts = Vec::new();
3888
4011
  for prop in props {
3889
- if let ObjectProp::KeyValue(k, v) = prop {
4012
+ if let ObjectProp::KeyValue(k, v, _) = prop {
3890
4013
  let val = self.emit_expr(v)?;
3891
4014
  if self.should_clone(v) {
3892
4015
  parts.push(format!("(Arc::from({:?}), ({}).clone())", k.as_ref(), val));
@@ -4553,7 +4676,7 @@ impl Codegen {
4553
4676
  Expr::Object { props, .. } => {
4554
4677
  for prop in props {
4555
4678
  match prop {
4556
- ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => {
4679
+ ObjectProp::KeyValue(_, e, _) | ObjectProp::Spread(e) => {
4557
4680
  Self::collect_expr_idents(e, idents)
4558
4681
  }
4559
4682
  }
@@ -4812,7 +4935,7 @@ impl Codegen {
4812
4935
  Expr::Object { props, .. } => {
4813
4936
  for prop in props {
4814
4937
  match prop {
4815
- ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => {
4938
+ ObjectProp::KeyValue(_, e, _) | ObjectProp::Spread(e) => {
4816
4939
  Self::collect_assigned_idents_in_expr(e, names);
4817
4940
  }
4818
4941
  }
@@ -5026,7 +5149,7 @@ impl Codegen {
5026
5149
  Expr::Object { props, .. } => {
5027
5150
  for prop in props {
5028
5151
  match prop {
5029
- ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => {
5152
+ ObjectProp::KeyValue(_, e, _) | ObjectProp::Spread(e) => {
5030
5153
  Self::collect_captured_block_vars_from_expr(e, block_vars, result);
5031
5154
  }
5032
5155
  }
@@ -5848,7 +5971,7 @@ impl Codegen {
5848
5971
  let mut bail = false;
5849
5972
  for prop in props {
5850
5973
  match prop {
5851
- ObjectProp::KeyValue(key, value) => {
5974
+ ObjectProp::KeyValue(key, value, _) => {
5852
5975
  if let Some(field_ty) = field_types.get(key.as_ref()) {
5853
5976
  let v = self.emit_native_expr(value, field_ty)?;
5854
5977
  field_inits.insert(crate::types::field_ident(key.as_ref()), v);
@@ -5905,6 +6028,19 @@ impl Codegen {
5905
6028
  }
5906
6029
  }
5907
6030
 
6031
+ // Fast path: when the native typed emitter already yields the target type, use its code
6032
+ // directly — skipping the `Value::Number(<expr>)` box that `from_value_expr` would
6033
+ // immediately unbox. This round-trip otherwise lands in hot loops: `let xt = x*x - y*y + x0`
6034
+ // (xt inferred f64) emitted `match &Value::Number(<expr>) { Value::Number(n) => *n,
6035
+ // _ => panic!() }` *every iteration*. `emit_typed_expr`'s contract guarantees `code` is a
6036
+ // value of `typed_ty` directly, so when it equals the target the code is exactly what we
6037
+ // want, unboxed. (Any other type falls through to the unchanged box-and-coerce path below.)
6038
+ if let Ok((typed_code, typed_ty)) = self.emit_typed_expr(expr) {
6039
+ if &typed_ty == target_type {
6040
+ return Ok(typed_code);
6041
+ }
6042
+ }
6043
+
5908
6044
  // Fall back to emit_expr + conversion
5909
6045
  let value_expr = self.emit_expr(expr)?;
5910
6046
  Ok(target_type.from_value_expr(&value_expr))
@@ -5972,6 +6108,985 @@ impl Codegen {
5972
6108
  demoted
5973
6109
  }
5974
6110
 
6111
+ // ── Integer-range lattice (#174) ────────────────────────────────────────────────────────────
6112
+ //
6113
+ // Prove an `f64` expression always holds an integer within `(-2^53, 2^53)`, so it can be
6114
+ // computed in `i64` with a result BIT-IDENTICAL to the `f64` the interpreter/VM produce. The
6115
+ // immediate payoff is `x % c` → an integer remainder instead of `fmod` (fmod is ~5-10× slower);
6116
+ // the lattice is sound by construction — every rule preserves "integer-valued AND within the
6117
+ // exact-`f64` range", and any unprovable form yields `None` (treated as unbounded → no rewrite).
6118
+ //
6119
+ // The classic win is a `% c`-bounded recurrence (e.g. an LCG `seed = (seed*A + C) % M`): the
6120
+ // modulo caps the result to `[0, M-1]` regardless of the dividend's size, so the fixpoint
6121
+ // converges and every intermediate stays well under 2^53.
6122
+
6123
+ /// Prove `e` is always an integer in `[min, max]` (inclusive), both inside `(-2^53, 2^53)`.
6124
+ /// `ranges` supplies proven bounds for in-scope locals. `None` = unprovable / unbounded.
6125
+ fn int_range(
6126
+ &self,
6127
+ e: &Expr,
6128
+ ranges: &HashMap<String, (i64, i64)>,
6129
+ ) -> Option<(i64, i64)> {
6130
+ const LIM: i64 = 1 << 53;
6131
+ let clamp = |lo: i64, hi: i64| -> Option<(i64, i64)> {
6132
+ if lo <= hi && lo > -LIM && hi < LIM {
6133
+ Some((lo, hi))
6134
+ } else {
6135
+ None
6136
+ }
6137
+ };
6138
+ match e {
6139
+ Expr::Literal {
6140
+ value: Literal::Number(n),
6141
+ ..
6142
+ } => Self::int_literal_value(*n).and_then(|v| clamp(v, v)),
6143
+ Expr::Ident { name, .. } => ranges.get(name.as_ref()).copied(),
6144
+ Expr::Unary {
6145
+ op: UnaryOp::Neg,
6146
+ operand,
6147
+ ..
6148
+ } => {
6149
+ let (lo, hi) = self.int_range(operand, ranges)?;
6150
+ clamp(-hi, -lo)
6151
+ }
6152
+ Expr::Binary {
6153
+ left, op, right, ..
6154
+ } => match op {
6155
+ // Bitwise & shift always yield an int32 — exact and far inside 2^53. A positive
6156
+ // literal `&`-mask tightens the upper bound (common: `h & 0xFF` → [0, 255]).
6157
+ BinOp::BitAnd => {
6158
+ let mask = Self::int_literal_value_of(left)
6159
+ .or_else(|| Self::int_literal_value_of(right));
6160
+ match mask {
6161
+ Some(m) if m >= 0 => clamp(0, m),
6162
+ _ => clamp(i32::MIN as i64, i32::MAX as i64),
6163
+ }
6164
+ }
6165
+ BinOp::BitOr | BinOp::BitXor | BinOp::Shl | BinOp::Shr => {
6166
+ clamp(i32::MIN as i64, i32::MAX as i64)
6167
+ }
6168
+ BinOp::UShr => clamp(0, u32::MAX as i64),
6169
+ // `x % c` (c a positive integer literal) — the result is an integer in (-c, c) when
6170
+ // the dividend is a proven integer; sign follows the dividend (JS `%` / Rust `%` both
6171
+ // truncate toward zero), so a non-negative dividend gives `[0, min(c-1, hi)]`.
6172
+ BinOp::Mod => {
6173
+ let c = Self::int_literal_value_of(right).filter(|&c| c > 0)?;
6174
+ // The dividend must be a proven INTEGER (so the result is integral); the modulo
6175
+ // then caps the magnitude to `< c` REGARDLESS of the dividend's size — a fixed
6176
+ // (not dividend-dependent) bound, so `% c`-driven recurrences converge in one
6177
+ // step. Sign follows the dividend (Rust `%` / JS `%` both truncate to zero).
6178
+ // The dividend is integral if it is range-bounded OR merely int-VALUED (e.g.
6179
+ // `r % 97` where `r` is a loop counter — integral but unbounded), the latter
6180
+ // giving the conservative two-sided `(-(c-1), c-1)`.
6181
+ if let Some((lo, _hi)) = self.int_range(left, ranges) {
6182
+ if lo >= 0 {
6183
+ clamp(0, c - 1)
6184
+ } else {
6185
+ clamp(-(c - 1), c - 1)
6186
+ }
6187
+ } else if self.expr_is_int_valued(left) {
6188
+ clamp(-(c - 1), c - 1)
6189
+ } else {
6190
+ None
6191
+ }
6192
+ }
6193
+ BinOp::Add => {
6194
+ let (la, ua) = self.int_range(left, ranges)?;
6195
+ let (lb, ub) = self.int_range(right, ranges)?;
6196
+ clamp(la + lb, ua + ub)
6197
+ }
6198
+ BinOp::Sub => {
6199
+ let (la, ua) = self.int_range(left, ranges)?;
6200
+ let (lb, ub) = self.int_range(right, ranges)?;
6201
+ clamp(la - ub, ua - lb)
6202
+ }
6203
+ BinOp::Mul => {
6204
+ let (la, ua) = self.int_range(left, ranges)?;
6205
+ let (lb, ub) = self.int_range(right, ranges)?;
6206
+ // Compute in i128 so corner products can't overflow before the 2^53 clamp.
6207
+ let p = [
6208
+ (la as i128) * (lb as i128),
6209
+ (la as i128) * (ub as i128),
6210
+ (ua as i128) * (lb as i128),
6211
+ (ua as i128) * (ub as i128),
6212
+ ];
6213
+ let lo = *p.iter().min().unwrap();
6214
+ let hi = *p.iter().max().unwrap();
6215
+ if lo > -(LIM as i128) && hi < (LIM as i128) {
6216
+ clamp(lo as i64, hi as i64)
6217
+ } else {
6218
+ None
6219
+ }
6220
+ }
6221
+ _ => None,
6222
+ },
6223
+ _ => None,
6224
+ }
6225
+ }
6226
+
6227
+ /// An integer-valued, exactly-`f64`-representable number from a numeric literal value.
6228
+ fn int_literal_value(n: f64) -> Option<i64> {
6229
+ if n.is_finite() && n.fract() == 0.0 && n.abs() < (1i64 << 53) as f64 {
6230
+ Some(n as i64)
6231
+ } else {
6232
+ None
6233
+ }
6234
+ }
6235
+
6236
+ /// As [`int_literal_value`] but for an `Expr` that is a numeric literal (else `None`).
6237
+ fn int_literal_value_of(e: &Expr) -> Option<i64> {
6238
+ match e {
6239
+ Expr::Literal {
6240
+ value: Literal::Number(n),
6241
+ ..
6242
+ } => Self::int_literal_value(*n),
6243
+ _ => None,
6244
+ }
6245
+ }
6246
+
6247
+ /// Names of `f64` locals provably integer-bounded within `(-2^53, 2^53)` across the whole
6248
+ /// program. Seeds from integer-literal initializers and literal-bounded `for` counters, then
6249
+ /// runs a join fixpoint over reassignments: a local keeps a bound only if its init and EVERY
6250
+ /// reassignment RHS are `int_range`-provable and the joined range stabilizes within a few
6251
+ /// rounds (else it is dropped = unbounded). Sound: a dropped local simply keeps the `f64` path.
6252
+ fn collect_int_range_locals(&self, stmts: &[Statement]) -> HashMap<String, (i64, i64)> {
6253
+ let mut ranges: HashMap<String, (i64, i64)> = HashMap::new();
6254
+ // Seed: `let x = <int literal>` and `for (let i = <int>; i < <int>; i++/i+=1)` counters.
6255
+ Self::seed_int_ranges(stmts, &mut ranges);
6256
+ if ranges.is_empty() {
6257
+ return ranges;
6258
+ }
6259
+ // All reassignments `(name, rhs)` — a local is bounded only if every one stays provable.
6260
+ let mut reassigns: Vec<(String, &Expr)> = Vec::new();
6261
+ Self::collect_reassignments_stmts(stmts, &mut reassigns);
6262
+
6263
+ // Phase A — join rounds: grow each seeded local's range toward a fixpoint. A reassignment
6264
+ // whose RHS is unprovable drops the local immediately. With the modulo cap fixed, `% c`
6265
+ // recurrences converge in ≤2 rounds; the round cap just bounds non-converging growth (those
6266
+ // are caught by phase B).
6267
+ for _round in 0..8 {
6268
+ let mut changed = false;
6269
+ let snapshot = ranges.clone();
6270
+ for (name, rhs) in &reassigns {
6271
+ let Some(&(clo, chi)) = snapshot.get(name.as_str()) else {
6272
+ continue;
6273
+ };
6274
+ match self.int_range(rhs, &snapshot) {
6275
+ Some((rlo, rhi)) => {
6276
+ let (nlo, nhi) = (clo.min(rlo), chi.max(rhi));
6277
+ if (nlo, nhi) != (clo, chi) {
6278
+ ranges.insert(name.clone(), (nlo, nhi));
6279
+ changed = true;
6280
+ }
6281
+ }
6282
+ None => {
6283
+ ranges.remove(name.as_str());
6284
+ changed = true;
6285
+ }
6286
+ }
6287
+ }
6288
+ if !changed {
6289
+ break;
6290
+ }
6291
+ }
6292
+ // Phase B — validate the result is an INDUCTIVE INVARIANT: a local keeps its range only if
6293
+ // every reassignment's RHS range (evaluated against the final map) stays within it. A local
6294
+ // that kept growing (e.g. `s = s + 1`, no cap) fails this and is dropped — and dropping it
6295
+ // can make other RHS unprovable, so iterate to a fixpoint. This is what makes the analysis
6296
+ // SOUND regardless of the round cap: only true fixpoints survive.
6297
+ loop {
6298
+ let mut dropped = false;
6299
+ let snapshot = ranges.clone();
6300
+ for (name, rhs) in &reassigns {
6301
+ let Some(&(clo, chi)) = snapshot.get(name.as_str()) else {
6302
+ continue;
6303
+ };
6304
+ let ok = matches!(self.int_range(rhs, &snapshot), Some((rlo, rhi)) if rlo >= clo && rhi <= chi);
6305
+ if !ok {
6306
+ ranges.remove(name.as_str());
6307
+ dropped = true;
6308
+ }
6309
+ }
6310
+ if !dropped {
6311
+ return ranges;
6312
+ }
6313
+ }
6314
+ }
6315
+
6316
+ // ── i32-loop-var lowering (bun/JSC-style integer-register hash accumulator) ─────────────────
6317
+ //
6318
+ // A `number` local `h` that (i) is declared `let h = <int literal>` immediately before a `for`,
6319
+ // (ii) is reassigned ONLY inside that loop by bitwise/shift expressions that lower fully in the
6320
+ // int32 domain, and (iii) whose every NUMERIC (non-bitwise) read happens where `h`'s JS value is
6321
+ // a *signed* int32 — can be kept in an `i32` register across the loop instead of round-tripping
6322
+ // `f64`↔`i32` on each op. The single excursion is an arithmetic node (`h * C`) that `int_range`
6323
+ // proves exceeds 2^53, so it stays `f64` (the multiply rounds in f64 *before* `ToUint32`, exactly
6324
+ // as V8 does). Soundness rests on:
6325
+ // • `int_range_locals` proving `h` is always an exact integer in (-2^53, 2^53) — the i32
6326
+ // register then holds precisely `ToInt32(h)`, and reads coerce `(h as f64)` = the signed
6327
+ // int32 value, while `>>> 0` boxings reinterpret the register as `u32`.
6328
+ // • the SIGNEDNESS pass below: after `^ & | << >>` `h` is signed-int32-valued; after `>>>`
6329
+ // (and at init, since the literal may exceed i32::MAX) it is uint32-valued. A *numeric* read
6330
+ // of `h` at a uint32-valued point would see the wrong sign → BAIL to the f64 path.
6331
+ // Anything unprovable bails → the existing f64 lowering, so this is purely additive.
6332
+
6333
+ /// `op` is a bitwise/shift operator (operands coerced to int32 by JS).
6334
+ fn is_bitwise_op(op: BinOp) -> bool {
6335
+ matches!(
6336
+ op,
6337
+ BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor | BinOp::Shl | BinOp::Shr | BinOp::UShr
6338
+ )
6339
+ }
6340
+
6341
+ /// `e` lowers FULLY in the int32 domain (top node bitwise/shift; every leaf is either a numeric
6342
+ /// literal, the loop var `h`, or an arithmetic/`Mod` subtree of int-provable numbers that becomes
6343
+ /// a single `f64` excursion re-narrowed by `to_int32`). Mirrors what `emit_int32_operand` will
6344
+ /// actually emit, so a `true` here means the reassignment really lowers without a per-op round
6345
+ /// trip. Conservative: unknown forms (calls, member access, etc.) → false.
6346
+ fn i32_chain_lowerable(&self, e: &Expr, var: &str) -> bool {
6347
+ match e {
6348
+ Expr::Binary { left, op, right, .. } if Self::is_bitwise_op(*op) => {
6349
+ self.i32_chain_lowerable(left, var) && self.i32_chain_lowerable(right, var)
6350
+ }
6351
+ // A non-bitwise node is a LEAF in the int32 chain: it must lower to a plain `f64` that
6352
+ // `to_int32` then narrows. Require it provably integer-valued (so the f64 is exact) —
6353
+ // either the var itself, an int literal, or an int-range/int-valued arithmetic subtree.
6354
+ _ => self.i32_leaf_is_f64(e, var),
6355
+ }
6356
+ }
6357
+
6358
+ /// An int32-chain LEAF that provably emits a plain `f64`: the loop var, an integer literal, or a
6359
+ /// `+ - * % / **`-arithmetic / unary subtree over numbers proven integer-valued (so `as f64` is
6360
+ /// exact and `to_int32` recovers the bit-pattern). Bitwise sub-nodes are handled by the caller.
6361
+ fn i32_leaf_is_f64(&self, e: &Expr, var: &str) -> bool {
6362
+ match e {
6363
+ Expr::Ident { name, .. } => name.as_ref() == var || self.expr_is_int_valued(e),
6364
+ Expr::Literal { value: Literal::Number(n), .. } => {
6365
+ n.is_finite() && n.fract() == 0.0
6366
+ }
6367
+ Expr::Unary { op: UnaryOp::Neg | UnaryOp::BitNot, operand, .. } => {
6368
+ self.i32_leaf_is_f64(operand, var)
6369
+ }
6370
+ Expr::Binary { left, op, right, .. } => match op {
6371
+ BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Mod => {
6372
+ self.i32_leaf_is_f64(left, var) && self.i32_leaf_is_f64(right, var)
6373
+ }
6374
+ op if Self::is_bitwise_op(*op) => {
6375
+ self.i32_chain_lowerable(left, var) && self.i32_chain_lowerable(right, var)
6376
+ }
6377
+ _ => false,
6378
+ },
6379
+ _ => false,
6380
+ }
6381
+ }
6382
+
6383
+ /// `e` provably evaluates to a FINITE f64 with `|e| < 2^62`, so `to_int32_unchecked` (no
6384
+ /// `is_finite` guard, no saturating cast) is sound for it. Handled shapes: an `I32`-register read
6385
+ /// (`|x| < 2^31`), a finite numeric literal, unary `-`, and `+ - *` over such operands (bounds
6386
+ /// combined; any branch unprovable ⇒ `None`). Bitwise/shift sub-nodes are NOT leaves here, so we
6387
+ /// don't descend into them (the caller's `to_int32`/`to_uint32` already bound those to 32 bits).
6388
+ fn f64_finite_bounded_below_2pow62(&self, e: &Expr) -> bool {
6389
+ self.f64_abs_bound(e).is_some_and(|b| b < 4.611686018427388e18) // 2^62
6390
+ }
6391
+
6392
+ /// Conservative magnitude bound for [`f64_finite_bounded_below_2pow62`]; `None` if not provable.
6393
+ fn f64_abs_bound(&self, e: &Expr) -> Option<f64> {
6394
+ match e {
6395
+ // An i32-register accumulator: its magnitude is `< 2^31`.
6396
+ Expr::Ident { name, .. }
6397
+ if self.type_context.get_type(name.as_ref()) == RustType::I32 =>
6398
+ {
6399
+ Some(2147483648.0) // 2^31
6400
+ }
6401
+ Expr::Literal { value: Literal::Number(n), .. } if n.is_finite() => Some(n.abs()),
6402
+ Expr::Unary { op: UnaryOp::Neg, operand, .. } => self.f64_abs_bound(operand),
6403
+ Expr::Binary { left, op, right, .. } => {
6404
+ let la = self.f64_abs_bound(left)?;
6405
+ let ra = self.f64_abs_bound(right)?;
6406
+ match op {
6407
+ BinOp::Add | BinOp::Sub => Some(la + ra),
6408
+ BinOp::Mul => Some(la * ra),
6409
+ _ => None,
6410
+ }
6411
+ }
6412
+ _ => None,
6413
+ }
6414
+ }
6415
+
6416
+ /// Walk `e` and decide whether `var` is read SAFELY given it is currently `signed`-int32-valued
6417
+ /// (`signed == false` ⇒ uint32-valued). A read of `var` directly under a bitwise/shift op is a
6418
+ /// register read (always safe); a read of `var` in any *numeric* position is safe only while
6419
+ /// `signed`. `bitwise_parent` tracks whether the immediate parent op is bitwise/shift. Returns
6420
+ /// `false` (bail) if any numeric read happens while not `signed`.
6421
+ fn i32_reads_ok(e: &Expr, var: &str, signed: bool, bitwise_parent: bool) -> bool {
6422
+ match e {
6423
+ Expr::Ident { name, .. } if name.as_ref() == var => bitwise_parent || signed,
6424
+ Expr::Binary { left, op, right, .. } => {
6425
+ let bw = Self::is_bitwise_op(*op);
6426
+ Self::i32_reads_ok(left, var, signed, bw)
6427
+ && Self::i32_reads_ok(right, var, signed, bw)
6428
+ }
6429
+ Expr::Unary { operand, .. } => Self::i32_reads_ok(operand, var, signed, false),
6430
+ _ => {
6431
+ // Any other read of `var` (call arg, member, index, ternary, …) is a numeric/opaque
6432
+ // use: only bitwise-parent or signed positions pass; otherwise bail if it mentions
6433
+ // `var` at all (conservative — we can't track signedness through opaque forms).
6434
+ if Self::collect_idents_of(e).contains(var) {
6435
+ bitwise_parent || signed
6436
+ } else {
6437
+ true
6438
+ }
6439
+ }
6440
+ }
6441
+ }
6442
+
6443
+ fn collect_idents_of(e: &Expr) -> HashSet<String> {
6444
+ let mut idents = HashSet::new();
6445
+ Self::collect_expr_idents(e, &mut idents);
6446
+ idents
6447
+ }
6448
+
6449
+ /// EVERY read of `var` outside its own body-assignment RHSs must be a register (bitwise) read —
6450
+ /// e.g. the final `return h >>> 0`. Body-assignment RHSs (`var = <rhs>`) are vetted by the
6451
+ /// ordered signedness pass, so this walker SKIPS the RHS of an assignment whose target is `var`,
6452
+ /// and rejects any other numeric (non-bitwise) read of `var` anywhere in `stmts`.
6453
+ fn i32_only_bitwise_reads_outside_assigns(stmts: &[Statement], var: &str) -> bool {
6454
+ stmts
6455
+ .iter()
6456
+ .all(|s| Self::i32_external_reads_ok_stmt(s, var))
6457
+ }
6458
+
6459
+ fn i32_external_reads_ok_stmt(s: &Statement, var: &str) -> bool {
6460
+ let mut ok = true;
6461
+ Self::for_each_stmt_expr(s, &mut |e| {
6462
+ if !ok {
6463
+ return;
6464
+ }
6465
+ ok &= Self::i32_external_reads_ok_expr(e, var, false);
6466
+ });
6467
+ ok
6468
+ }
6469
+
6470
+ /// As `i32_reads_ok` with `signed = false` (the strictest state), but a `var = <rhs>` assignment
6471
+ /// node has its RHS reads SKIPPED — those are the loop assignments, vetted by the ordered pass.
6472
+ fn i32_external_reads_ok_expr(e: &Expr, var: &str, bitwise_parent: bool) -> bool {
6473
+ match e {
6474
+ // The write target name is not a read; its RHS is vetted by the ordered signedness pass.
6475
+ Expr::Assign { name, value, .. } if name.as_ref() == var => {
6476
+ // The RHS may itself contain *nested* assigns to OTHER vars referencing `var`, but
6477
+ // those would have failed the "single-writer" check; the RHS `var` reads are the
6478
+ // ordered-pass's job, so don't re-check them here.
6479
+ let _ = value;
6480
+ true
6481
+ }
6482
+ Expr::Ident { name, .. } if name.as_ref() == var => bitwise_parent,
6483
+ Expr::Binary { left, op, right, .. } => {
6484
+ let bw = Self::is_bitwise_op(*op);
6485
+ Self::i32_external_reads_ok_expr(left, var, bw)
6486
+ && Self::i32_external_reads_ok_expr(right, var, bw)
6487
+ }
6488
+ Expr::Unary { operand, .. } => Self::i32_external_reads_ok_expr(operand, var, false),
6489
+ _ => {
6490
+ if Self::collect_idents_of(e).contains(var) {
6491
+ bitwise_parent
6492
+ } else {
6493
+ true
6494
+ }
6495
+ }
6496
+ }
6497
+ }
6498
+
6499
+ /// Collect every `number` accumulator eligible for i32-register loop lowering. Scans every
6500
+ /// statement list (top level + nested blocks/loops/fn bodies); the eligibility gate itself uses
6501
+ /// the whole-program (name-keyed) reassignment set, so a name with any writer outside its loop
6502
+ /// body bails. Soundness is per-name, not per-scope, which the strict gate guarantees.
6503
+ fn collect_i32_loop_vars(&self, stmts: &[Statement]) -> HashSet<String> {
6504
+ let mut out = HashSet::new();
6505
+ self.collect_i32_loop_vars_in(stmts, stmts, &mut out);
6506
+ out
6507
+ }
6508
+
6509
+ /// `stmts` is the statement list currently being scanned for the decl-then-`for` pattern;
6510
+ /// `root` is the whole program, used by the gate's whole-program writer/reader checks.
6511
+ fn collect_i32_loop_vars_in(
6512
+ &self,
6513
+ stmts: &[Statement],
6514
+ root: &[Statement],
6515
+ out: &mut HashSet<String>,
6516
+ ) {
6517
+ // `let h = <int>` directly followed by a `for` whose body reassigns `h`.
6518
+ for win in stmts.windows(2) {
6519
+ if let (
6520
+ Statement::VarDecl {
6521
+ name,
6522
+ mutable: true,
6523
+ init: Some(init),
6524
+ ..
6525
+ },
6526
+ Statement::For { body, .. },
6527
+ ) = (&win[0], &win[1])
6528
+ {
6529
+ if Self::int_literal_value_of(init).is_some()
6530
+ && self.i32_loop_var_eligible(name.as_ref(), body, root)
6531
+ {
6532
+ out.insert(name.to_string());
6533
+ }
6534
+ }
6535
+ }
6536
+ // Recurse into nested statement lists (each block / fn body / loop body is scanned).
6537
+ for s in stmts {
6538
+ Self::for_each_child_stmt_list(s, &mut |list| {
6539
+ self.collect_i32_loop_vars_in(list, root, out)
6540
+ });
6541
+ }
6542
+ }
6543
+
6544
+ /// Eligibility gate for the i32-register lowering of `var`, declared just before `for (…) body`.
6545
+ /// All bail conditions keep the existing f64 path (purely additive). `var` qualifies iff:
6546
+ /// (a) `int_range` proves it always holds an exact integer in (-2^53, 2^53);
6547
+ /// (b) it is not closure-captured into a cell;
6548
+ /// (c) it is written ONLY by the assignments inside `body`, each a bitwise/shift expr that
6549
+ /// lowers fully in the int32 domain;
6550
+ /// (d) the forward signedness pass over those assignments admits every numeric read of `var`;
6551
+ /// (e) every read of `var` OUTSIDE those assignment RHSs is a register (bitwise) read.
6552
+ fn i32_loop_var_eligible(&self, var: &str, body: &Statement, root: &[Statement]) -> bool {
6553
+ // (a)
6554
+ if !self.int_range_locals.contains_key(var) {
6555
+ return false;
6556
+ }
6557
+ // (b)
6558
+ if self.refcell_wrapped_vars.contains(var) {
6559
+ return false;
6560
+ }
6561
+ // (c) reassignments to `var` inside the loop body, in source order.
6562
+ let mut body_assigns: Vec<&Expr> = Vec::new();
6563
+ Self::collect_ordered_assigns_to(body, var, &mut body_assigns);
6564
+ if body_assigns.is_empty() {
6565
+ return false;
6566
+ }
6567
+ for rhs in &body_assigns {
6568
+ let top_bitwise = matches!(rhs, Expr::Binary { op, .. } if Self::is_bitwise_op(*op));
6569
+ if !top_bitwise || !self.i32_chain_lowerable(rhs, var) {
6570
+ return false;
6571
+ }
6572
+ }
6573
+ // `var` must have NO writer outside this loop body — whole-program count must match.
6574
+ let mut all_assigns: Vec<(String, &Expr)> = Vec::new();
6575
+ Self::collect_reassignments_stmts(root, &mut all_assigns);
6576
+ let total_writes = all_assigns.iter().filter(|(n, _)| n == var).count();
6577
+ if total_writes != body_assigns.len() {
6578
+ return false;
6579
+ }
6580
+ // (d) SIGNEDNESS pass. Init value may exceed i32::MAX ⇒ start uint32-valued. Each RHS is read
6581
+ // against the CURRENT signedness; new signedness follows the top op (`>>>` → unsigned).
6582
+ let mut signed = false;
6583
+ for rhs in &body_assigns {
6584
+ if !Self::i32_reads_ok(rhs, var, signed, false) {
6585
+ return false;
6586
+ }
6587
+ signed = !matches!(rhs, Expr::Binary { op: BinOp::UShr, .. });
6588
+ }
6589
+ // (e) Every other read of `var` in the program must be a register (bitwise) read.
6590
+ if !Self::i32_only_bitwise_reads_outside_assigns(root, var) {
6591
+ return false;
6592
+ }
6593
+ true
6594
+ }
6595
+
6596
+ /// Collect, in source order, the RHS of every top-level `var = <rhs>` assignment to `var`
6597
+ /// reachable in `body` (descending blocks/if/loops but NOT into nested fn bodies).
6598
+ fn collect_ordered_assigns_to<'a>(s: &'a Statement, var: &str, out: &mut Vec<&'a Expr>) {
6599
+ match s {
6600
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
6601
+ for st in statements {
6602
+ Self::collect_ordered_assigns_to(st, var, out);
6603
+ }
6604
+ }
6605
+ Statement::ExprStmt { expr, .. } => {
6606
+ if let Expr::Assign { name, value, .. } = expr {
6607
+ if name.as_ref() == var {
6608
+ out.push(value.as_ref());
6609
+ }
6610
+ }
6611
+ }
6612
+ Statement::If { then_branch, else_branch, .. } => {
6613
+ Self::collect_ordered_assigns_to(then_branch, var, out);
6614
+ if let Some(e) = else_branch {
6615
+ Self::collect_ordered_assigns_to(e, var, out);
6616
+ }
6617
+ }
6618
+ Statement::For { body, .. }
6619
+ | Statement::ForOf { body, .. }
6620
+ | Statement::While { body, .. }
6621
+ | Statement::DoWhile { body, .. } => {
6622
+ Self::collect_ordered_assigns_to(body, var, out)
6623
+ }
6624
+ _ => {}
6625
+ }
6626
+ }
6627
+
6628
+ /// Invoke `f` with every nested *statement list* directly reachable from `s` (blocks, `if`
6629
+ /// branches, loop bodies, fn bodies). Used to scan each lexical scope for the decl-then-`for`
6630
+ /// pattern. Branch/loop bodies are single `Statement`s, passed as 1-element slices.
6631
+ fn for_each_child_stmt_list(s: &Statement, f: &mut dyn FnMut(&[Statement])) {
6632
+ match s {
6633
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
6634
+ f(statements)
6635
+ }
6636
+ Statement::If { then_branch, else_branch, .. } => {
6637
+ f(std::slice::from_ref(then_branch));
6638
+ if let Some(e) = else_branch {
6639
+ f(std::slice::from_ref(e));
6640
+ }
6641
+ }
6642
+ Statement::For { body, .. }
6643
+ | Statement::ForOf { body, .. }
6644
+ | Statement::While { body, .. }
6645
+ | Statement::DoWhile { body, .. }
6646
+ | Statement::FunDecl { body, .. } => f(std::slice::from_ref(body)),
6647
+ Statement::Switch { cases, default_body, .. } => {
6648
+ for (_, body) in cases {
6649
+ f(body);
6650
+ }
6651
+ if let Some(b) = default_body {
6652
+ f(b);
6653
+ }
6654
+ }
6655
+ Statement::Try { body, catch_body, finally_body, .. } => {
6656
+ f(std::slice::from_ref(body));
6657
+ if let Some(b) = catch_body {
6658
+ f(std::slice::from_ref(b));
6659
+ }
6660
+ if let Some(b) = finally_body {
6661
+ f(std::slice::from_ref(b));
6662
+ }
6663
+ }
6664
+ _ => {}
6665
+ }
6666
+ }
6667
+
6668
+ /// Invoke `f` on every top-level expression of `s`, recursing through nested control-flow
6669
+ /// statements (blocks, if, loops, switch, try, return/throw). `f` is responsible for recursing
6670
+ /// into each expression's own subtree. Does NOT descend into nested fn-decl bodies (a different
6671
+ /// lexical scope; a captured loop var would be RefCell-bailed before reaching here).
6672
+ fn for_each_stmt_expr(s: &Statement, f: &mut dyn FnMut(&Expr)) {
6673
+ match s {
6674
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
6675
+ for st in statements {
6676
+ Self::for_each_stmt_expr(st, f);
6677
+ }
6678
+ }
6679
+ Statement::VarDecl { init: Some(e), .. } => f(e),
6680
+ Statement::VarDeclDestructure { init, .. } => f(init),
6681
+ Statement::ExprStmt { expr, .. } => f(expr),
6682
+ Statement::If { cond, then_branch, else_branch, .. } => {
6683
+ f(cond);
6684
+ Self::for_each_stmt_expr(then_branch, f);
6685
+ if let Some(e) = else_branch {
6686
+ Self::for_each_stmt_expr(e, f);
6687
+ }
6688
+ }
6689
+ Statement::While { cond, body, .. } => {
6690
+ f(cond);
6691
+ Self::for_each_stmt_expr(body, f);
6692
+ }
6693
+ Statement::DoWhile { body, cond, .. } => {
6694
+ Self::for_each_stmt_expr(body, f);
6695
+ f(cond);
6696
+ }
6697
+ Statement::For { init, cond, update, body, .. } => {
6698
+ if let Some(i) = init {
6699
+ Self::for_each_stmt_expr(i, f);
6700
+ }
6701
+ if let Some(c) = cond {
6702
+ f(c);
6703
+ }
6704
+ if let Some(u) = update {
6705
+ f(u);
6706
+ }
6707
+ Self::for_each_stmt_expr(body, f);
6708
+ }
6709
+ Statement::ForOf { iterable, body, .. } => {
6710
+ f(iterable);
6711
+ Self::for_each_stmt_expr(body, f);
6712
+ }
6713
+ Statement::Return { value: Some(e), .. } => f(e),
6714
+ Statement::Throw { value, .. } => f(value),
6715
+ Statement::Switch { expr, cases, default_body, .. } => {
6716
+ f(expr);
6717
+ for (g, body) in cases {
6718
+ if let Some(g) = g {
6719
+ f(g);
6720
+ }
6721
+ for st in body {
6722
+ Self::for_each_stmt_expr(st, f);
6723
+ }
6724
+ }
6725
+ if let Some(b) = default_body {
6726
+ for st in b {
6727
+ Self::for_each_stmt_expr(st, f);
6728
+ }
6729
+ }
6730
+ }
6731
+ Statement::Try { body, catch_body, finally_body, .. } => {
6732
+ Self::for_each_stmt_expr(body, f);
6733
+ if let Some(b) = catch_body {
6734
+ Self::for_each_stmt_expr(b, f);
6735
+ }
6736
+ if let Some(b) = finally_body {
6737
+ Self::for_each_stmt_expr(b, f);
6738
+ }
6739
+ }
6740
+ // A nested `function f(){…}` body is a separate scope: a loop var read there would be a
6741
+ // capture (RefCell-bailed) or a shadow (different binding) — don't descend.
6742
+ _ => {}
6743
+ }
6744
+ }
6745
+
6746
+ /// Seed integer ranges: integer-literal `let` initializers and literal-bounded `for` counters.
6747
+ fn seed_int_ranges(stmts: &[Statement], out: &mut HashMap<String, (i64, i64)>) {
6748
+ for s in stmts {
6749
+ match s {
6750
+ Statement::VarDecl {
6751
+ name,
6752
+ init: Some(e),
6753
+ ..
6754
+ } => {
6755
+ if let Some(v) = Self::int_literal_value_of(e) {
6756
+ out.insert(name.to_string(), (v, v));
6757
+ }
6758
+ }
6759
+ Statement::For {
6760
+ init, cond, body, ..
6761
+ } => {
6762
+ // `for (let i = <int>; i < <int>; ...)` → counter `i` ∈ [start, end-1].
6763
+ if let (
6764
+ Some(Statement::VarDecl {
6765
+ name,
6766
+ init: Some(istart),
6767
+ ..
6768
+ }),
6769
+ Some(Expr::Binary {
6770
+ left,
6771
+ op: BinOp::Lt,
6772
+ right,
6773
+ ..
6774
+ }),
6775
+ ) = (init.as_deref(), cond.as_ref())
6776
+ {
6777
+ if let (Some(start), Some(end)) = (
6778
+ Self::int_literal_value_of(istart),
6779
+ Self::int_literal_value_of(right),
6780
+ ) {
6781
+ if matches!(left.as_ref(), Expr::Ident { name: cn, .. } if cn.as_ref() == name.as_ref())
6782
+ && end > start
6783
+ && end - 1 < (1i64 << 53)
6784
+ {
6785
+ out.insert(name.to_string(), (start, end - 1));
6786
+ }
6787
+ }
6788
+ }
6789
+ Self::seed_int_ranges(std::slice::from_ref(body), out);
6790
+ }
6791
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
6792
+ Self::seed_int_ranges(statements, out)
6793
+ }
6794
+ Statement::If {
6795
+ then_branch,
6796
+ else_branch,
6797
+ ..
6798
+ } => {
6799
+ Self::seed_int_ranges(std::slice::from_ref(then_branch), out);
6800
+ if let Some(e) = else_branch {
6801
+ Self::seed_int_ranges(std::slice::from_ref(e), out);
6802
+ }
6803
+ }
6804
+ Statement::While { body, .. } | Statement::DoWhile { body, .. } => {
6805
+ Self::seed_int_ranges(std::slice::from_ref(body), out)
6806
+ }
6807
+ Statement::FunDecl { body, .. } => Self::seed_int_ranges(std::slice::from_ref(body), out),
6808
+ _ => {}
6809
+ }
6810
+ }
6811
+ }
6812
+
6813
+ /// `e` is provably INTEGER-valued (zero fractional part at runtime), per `set` for locals.
6814
+ /// Closed under `+ - * %` (modulo by a positive integer literal), unary `- ~`, bitwise/shift,
6815
+ /// and integer literals — so a loop counter (`0`, then `i + 1`) stays integral. Magnitude is
6816
+ /// NOT tracked (that is `int_range`'s job); this only certifies integrality.
6817
+ fn is_int_valued(e: &Expr, set: &HashSet<String>) -> bool {
6818
+ match e {
6819
+ Expr::Literal {
6820
+ value: Literal::Number(n),
6821
+ ..
6822
+ } => n.is_finite() && n.fract() == 0.0,
6823
+ Expr::Ident { name, .. } => set.contains(name.as_ref()),
6824
+ Expr::Unary {
6825
+ op: UnaryOp::Neg | UnaryOp::BitNot,
6826
+ operand,
6827
+ ..
6828
+ } => Self::is_int_valued(operand, set),
6829
+ Expr::Binary {
6830
+ left, op, right, ..
6831
+ } => match op {
6832
+ BinOp::Add | BinOp::Sub | BinOp::Mul => {
6833
+ Self::is_int_valued(left, set) && Self::is_int_valued(right, set)
6834
+ }
6835
+ BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor | BinOp::Shl | BinOp::Shr
6836
+ | BinOp::UShr => true,
6837
+ BinOp::Mod => {
6838
+ Self::int_literal_value_of(right).is_some_and(|c| c != 0)
6839
+ && Self::is_int_valued(left, set)
6840
+ }
6841
+ _ => false,
6842
+ },
6843
+ _ => false,
6844
+ }
6845
+ }
6846
+
6847
+ /// `self`-bound [`is_int_valued`] against the computed `int_valued_locals`.
6848
+ fn expr_is_int_valued(&self, e: &Expr) -> bool {
6849
+ Self::is_int_valued(e, &self.int_valued_locals)
6850
+ }
6851
+
6852
+ /// Locals that are always integer-valued. Greatest-fixpoint: assume every `let` local is
6853
+ /// integral, then drop any whose initializer or any reassignment RHS is not `is_int_valued`
6854
+ /// under the current set, until stable. Sound: dropping only ever removes names, and `+ - * %`
6855
+ /// preserve integrality even past 2^53 (the f64 result still has zero fractional part).
6856
+ fn collect_int_valued_locals(stmts: &[Statement]) -> HashSet<String> {
6857
+ // All declared local names (candidates).
6858
+ let mut names: HashSet<String> = HashSet::new();
6859
+ Self::collect_local_decl_names(stmts, &mut names);
6860
+ // Init/reassignment expressions per name.
6861
+ let mut defs: Vec<(String, &Expr)> = Vec::new();
6862
+ Self::collect_int_valued_defs(stmts, &mut defs);
6863
+ let mut reassigns: Vec<(String, &Expr)> = Vec::new();
6864
+ Self::collect_reassignments_stmts(stmts, &mut reassigns);
6865
+ loop {
6866
+ let mut changed = false;
6867
+ for (name, e) in defs.iter().chain(reassigns.iter()) {
6868
+ if names.contains(name.as_str()) && !Self::is_int_valued(e, &names) {
6869
+ names.remove(name.as_str());
6870
+ changed = true;
6871
+ }
6872
+ }
6873
+ if !changed {
6874
+ return names;
6875
+ }
6876
+ }
6877
+ }
6878
+
6879
+ /// Every `let`-declared local name (recursing through all nested statements).
6880
+ fn collect_local_decl_names(stmts: &[Statement], out: &mut HashSet<String>) {
6881
+ for s in stmts {
6882
+ match s {
6883
+ Statement::VarDecl { name, .. } => {
6884
+ out.insert(name.to_string());
6885
+ }
6886
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
6887
+ Self::collect_local_decl_names(statements, out)
6888
+ }
6889
+ Statement::If {
6890
+ then_branch,
6891
+ else_branch,
6892
+ ..
6893
+ } => {
6894
+ Self::collect_local_decl_names(std::slice::from_ref(then_branch), out);
6895
+ if let Some(e) = else_branch {
6896
+ Self::collect_local_decl_names(std::slice::from_ref(e), out);
6897
+ }
6898
+ }
6899
+ Statement::While { body, .. } | Statement::DoWhile { body, .. } => {
6900
+ Self::collect_local_decl_names(std::slice::from_ref(body), out)
6901
+ }
6902
+ Statement::For { init, body, .. } => {
6903
+ if let Some(i) = init {
6904
+ Self::collect_local_decl_names(std::slice::from_ref(i), out);
6905
+ }
6906
+ Self::collect_local_decl_names(std::slice::from_ref(body), out);
6907
+ }
6908
+ Statement::FunDecl { body, .. } => {
6909
+ Self::collect_local_decl_names(std::slice::from_ref(body), out)
6910
+ }
6911
+ _ => {}
6912
+ }
6913
+ }
6914
+ }
6915
+
6916
+ /// `(name, init-expr)` for every `let name = <init>` (recursing), for the int-valued fixpoint.
6917
+ fn collect_int_valued_defs<'a>(stmts: &'a [Statement], out: &mut Vec<(String, &'a Expr)>) {
6918
+ for s in stmts {
6919
+ match s {
6920
+ Statement::VarDecl {
6921
+ name,
6922
+ init: Some(e),
6923
+ ..
6924
+ } => out.push((name.to_string(), e)),
6925
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
6926
+ Self::collect_int_valued_defs(statements, out)
6927
+ }
6928
+ Statement::If {
6929
+ then_branch,
6930
+ else_branch,
6931
+ ..
6932
+ } => {
6933
+ Self::collect_int_valued_defs(std::slice::from_ref(then_branch), out);
6934
+ if let Some(e) = else_branch {
6935
+ Self::collect_int_valued_defs(std::slice::from_ref(e), out);
6936
+ }
6937
+ }
6938
+ Statement::While { body, .. } | Statement::DoWhile { body, .. } => {
6939
+ Self::collect_int_valued_defs(std::slice::from_ref(body), out)
6940
+ }
6941
+ Statement::For { init, body, .. } => {
6942
+ if let Some(i) = init {
6943
+ Self::collect_int_valued_defs(std::slice::from_ref(i), out);
6944
+ }
6945
+ Self::collect_int_valued_defs(std::slice::from_ref(body), out);
6946
+ }
6947
+ Statement::FunDecl { body, .. } => {
6948
+ Self::collect_int_valued_defs(std::slice::from_ref(body), out)
6949
+ }
6950
+ _ => {}
6951
+ }
6952
+ }
6953
+ }
6954
+
6955
+ /// Map `number[]` locals initialized from an array literal of integer literals → the inclusive
6956
+ /// element range, both inside `(-2^53, 2^53)`.
6957
+ fn collect_array_elem_ranges(stmts: &[Statement]) -> HashMap<String, (i64, i64)> {
6958
+ let mut out = HashMap::new();
6959
+ Self::array_elem_ranges_walk(stmts, &mut out);
6960
+ out
6961
+ }
6962
+
6963
+ fn array_elem_ranges_walk(stmts: &[Statement], out: &mut HashMap<String, (i64, i64)>) {
6964
+ for s in stmts {
6965
+ match s {
6966
+ Statement::VarDecl {
6967
+ name,
6968
+ init: Some(Expr::Array { elements, .. }),
6969
+ ..
6970
+ } => {
6971
+ let mut lo = i64::MAX;
6972
+ let mut hi = i64::MIN;
6973
+ let mut ok = !elements.is_empty();
6974
+ for el in elements {
6975
+ match el {
6976
+ ArrayElement::Expr(e) => match Self::int_literal_value_of(e) {
6977
+ Some(v) => {
6978
+ lo = lo.min(v);
6979
+ hi = hi.max(v);
6980
+ }
6981
+ None => {
6982
+ ok = false;
6983
+ break;
6984
+ }
6985
+ },
6986
+ _ => {
6987
+ ok = false;
6988
+ break;
6989
+ }
6990
+ }
6991
+ }
6992
+ if ok {
6993
+ out.insert(name.to_string(), (lo, hi));
6994
+ }
6995
+ }
6996
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
6997
+ Self::array_elem_ranges_walk(statements, out)
6998
+ }
6999
+ Statement::If {
7000
+ then_branch,
7001
+ else_branch,
7002
+ ..
7003
+ } => {
7004
+ Self::array_elem_ranges_walk(std::slice::from_ref(then_branch), out);
7005
+ if let Some(e) = else_branch {
7006
+ Self::array_elem_ranges_walk(std::slice::from_ref(e), out);
7007
+ }
7008
+ }
7009
+ Statement::While { body, .. } | Statement::DoWhile { body, .. } => {
7010
+ Self::array_elem_ranges_walk(std::slice::from_ref(body), out)
7011
+ }
7012
+ Statement::For { body, .. } => {
7013
+ Self::array_elem_ranges_walk(std::slice::from_ref(body), out)
7014
+ }
7015
+ Statement::FunDecl { body, .. } => {
7016
+ Self::array_elem_ranges_walk(std::slice::from_ref(body), out)
7017
+ }
7018
+ _ => {}
7019
+ }
7020
+ }
7021
+ }
7022
+
7023
+ /// Emit `e` as a native `i64` expression, used inside a native fold whose accumulator is `i64`.
7024
+ /// Returns `None` (caller keeps the `f64` fold) unless `e` is provably integer AND magnitude-
7025
+ /// bounded `< 2^53` at every node — so the `i64` arithmetic is bit-identical to the `f64` the
7026
+ /// interpreter/VM produce. `i64vars` are names already bound as `i64` (emitted bare); any other
7027
+ /// operand must be a bounded `f64` (emitted via `emit_typed_expr` then `as i64`, exact for
7028
+ /// integers < 2^53). Handles `+ - *` and `% <pos int literal>`; bails on anything else.
7029
+ fn emit_i64(
7030
+ &mut self,
7031
+ e: &Expr,
7032
+ i64vars: &HashSet<String>,
7033
+ ranges: &HashMap<String, (i64, i64)>,
7034
+ ) -> Result<Option<String>, CompileError> {
7035
+ // Whole-node bound (proves integrality + < 2^53). Without it, i64 could diverge from f64.
7036
+ if self.int_range(e, ranges).is_none() {
7037
+ return Ok(None);
7038
+ }
7039
+ if let Expr::Literal {
7040
+ value: Literal::Number(n),
7041
+ ..
7042
+ } = e
7043
+ {
7044
+ return Ok(Self::int_literal_value(*n).map(|v| format!("{}i64", v)));
7045
+ }
7046
+ if let Expr::Ident { name, .. } = e {
7047
+ if i64vars.contains(name.as_ref()) {
7048
+ return Ok(Some(Self::escape_ident(name.as_ref()).into_owned()));
7049
+ }
7050
+ }
7051
+ if let Expr::Binary {
7052
+ left, op, right, ..
7053
+ } = e
7054
+ {
7055
+ match op {
7056
+ BinOp::Add | BinOp::Sub | BinOp::Mul => {
7057
+ let (Some(l), Some(r)) = (
7058
+ self.emit_i64(left, i64vars, ranges)?,
7059
+ self.emit_i64(right, i64vars, ranges)?,
7060
+ ) else {
7061
+ return Ok(None);
7062
+ };
7063
+ let sym = match op {
7064
+ BinOp::Add => "+",
7065
+ BinOp::Sub => "-",
7066
+ _ => "*",
7067
+ };
7068
+ return Ok(Some(format!("({} {} {})", l, sym, r)));
7069
+ }
7070
+ BinOp::Mod => {
7071
+ if let Some(c) = Self::int_literal_value_of(right).filter(|&c| c > 0) {
7072
+ if let Some(l) = self.emit_i64(left, i64vars, ranges)? {
7073
+ return Ok(Some(format!("({} % {}i64)", l, c)));
7074
+ }
7075
+ }
7076
+ return Ok(None);
7077
+ }
7078
+ _ => return Ok(None),
7079
+ }
7080
+ }
7081
+ // Fallback: a bounded non-i64-var leaf (e.g. the f64 element variable) → cast once.
7082
+ let (code, ty) = self.emit_typed_expr(e)?;
7083
+ if ty == RustType::F64 {
7084
+ Ok(Some(format!("(({}) as i64)", code)))
7085
+ } else {
7086
+ Ok(None)
7087
+ }
7088
+ }
7089
+
5975
7090
  /// Record every annotated `VarDecl`/param name → its native `RustType`, recursing through all
5976
7091
  /// nested statements (loops, ifs, blocks, switch/try, function bodies). Flat; last write wins.
5977
7092
  fn collect_annotated_types(
@@ -6247,7 +7362,7 @@ impl Codegen {
6247
7362
  Expr::Object { props, .. } => {
6248
7363
  for p in props {
6249
7364
  match p {
6250
- ObjectProp::KeyValue(_, v) => Self::collect_reassignments_expr(v, out),
7365
+ ObjectProp::KeyValue(_, v, _) => Self::collect_reassignments_expr(v, out),
6251
7366
  ObjectProp::Spread(x) => Self::collect_reassignments_expr(x, out),
6252
7367
  }
6253
7368
  }
@@ -6654,6 +7769,88 @@ impl Codegen {
6654
7769
  Ok(())
6655
7770
  }
6656
7771
 
7772
+ /// Lower an expression that JS will coerce to **int32** inside a bitwise/shift computation,
7773
+ /// staying in the integer domain instead of round-tripping every intermediate through `f64`.
7774
+ ///
7775
+ /// Returns `Ok(Some(code))` where `code` is an `i32`-typed Rust expression equal to
7776
+ /// `ToInt32(e)`, or `Ok(None)` if a leaf can't be proven `F64` (then the caller keeps the
7777
+ /// existing per-op lowering — purely additive, never a regression).
7778
+ ///
7779
+ /// This is behaviour-identical to the nested `to_int32`/`to_uint32` lowering: an intermediate
7780
+ /// `(i32 as f64)` immediately re-narrowed by `to_int32` is exact (every `i32` is representable
7781
+ /// in `f64`, and `to_int32` of a finite value recovers it), so erasing it changes nothing but
7782
+ /// the round-trips. Crucially, only bitwise/shift nodes recurse — an `f64` `*`/`+`/`-` node is
7783
+ /// a *leaf* here, so e.g. `(h * 16777619) >>> 0` keeps its `f64` multiply (the 2^53 rule: the
7784
+ /// product exceeds 2^53 and must round in `f64` *before* `ToUint32`, exactly as V8 does).
7785
+ fn emit_int32_operand(&mut self, e: &Expr) -> Result<Option<String>, CompileError> {
7786
+ if let Expr::Binary {
7787
+ left, op, right, ..
7788
+ } = e
7789
+ {
7790
+ let bitwise = matches!(
7791
+ op,
7792
+ BinOp::BitAnd
7793
+ | BinOp::BitOr
7794
+ | BinOp::BitXor
7795
+ | BinOp::Shl
7796
+ | BinOp::Shr
7797
+ | BinOp::UShr
7798
+ );
7799
+ if bitwise {
7800
+ let li = match self.emit_int32_operand(left)? {
7801
+ Some(c) => c,
7802
+ None => return Ok(None),
7803
+ };
7804
+ let ri = match self.emit_int32_operand(right)? {
7805
+ Some(c) => c,
7806
+ None => return Ok(None),
7807
+ };
7808
+ // Shift counts: `(ri as u32)` shares its low 5 bits with `to_uint32(rhs)`, and
7809
+ // `wrapping_sh*` masks the count mod 32 — exactly JS's `count & 31`.
7810
+ let code = match op {
7811
+ BinOp::BitAnd => format!("({} & {})", li, ri),
7812
+ BinOp::BitOr => format!("({} | {})", li, ri),
7813
+ BinOp::BitXor => format!("({} ^ {})", li, ri),
7814
+ BinOp::Shl => format!("({}).wrapping_shl(({}) as u32)", li, ri),
7815
+ BinOp::Shr => format!("({}).wrapping_shr(({}) as u32)", li, ri),
7816
+ // `>>>` is a logical shift on the uint32 view; reinterpret back to `i32` to
7817
+ // stay in the integer domain (the unsigned value is recovered at the f64 edge).
7818
+ BinOp::UShr => {
7819
+ format!("((({}) as u32).wrapping_shr(({}) as u32) as i32)", li, ri)
7820
+ }
7821
+ _ => unreachable!(),
7822
+ };
7823
+ return Ok(Some(code));
7824
+ }
7825
+ }
7826
+ // Leaf: fold only when it is a plain `f64` (so `to_int32` applies directly). `to_int32`
7827
+ // keeps its `is_finite` guard here — a leaf may legitimately be NaN/±Infinity (→ 0).
7828
+ let (code, ty) = self.emit_typed_expr(e)?;
7829
+ if ty == RustType::F64 {
7830
+ // When the leaf is an ARITHMETIC node PROVABLY finite with `|x| < 2^62` (operands are
7831
+ // i32-register reads and finite literals — e.g. the FNV `h * 16777619` excursion), drop
7832
+ // the `is_finite` guard and Rust's saturating cast and truncate directly. Bit-identical
7833
+ // on this domain (`x as i64` truncates toward zero = JS ToInt32 truncation; `as i32` =
7834
+ // modulo 2^32), a few instructions cheaper per iteration. Emitted inline so the generated
7835
+ // crate needs no new runtime symbol. Any unproven leaf keeps the guarded `to_int32`.
7836
+ if matches!(e, Expr::Binary { .. }) && self.f64_finite_bounded_below_2pow62(e) {
7837
+ Ok(Some(format!(
7838
+ "(unsafe {{ ({}).to_int_unchecked::<i64>() }} as i32)",
7839
+ code
7840
+ )))
7841
+ } else {
7842
+ Ok(Some(format!("tishlang_runtime::to_int32({})", code)))
7843
+ }
7844
+ } else if ty == RustType::I32 {
7845
+ // An `I32` loop-accumulator already holds its JS ToInt32 bit-pattern in an integer
7846
+ // register — feed it straight in, NO `to_int32` round-trip. This is the perf win: the
7847
+ // per-op `f64`→i32 narrowing across the hash loop collapses to a register read.
7848
+ Ok(Some(code))
7849
+ } else {
7850
+ Ok(None)
7851
+ }
7852
+ }
7853
+
6657
7854
  fn emit_typed_expr(&mut self, expr: &Expr) -> Result<(String, RustType), CompileError> {
6658
7855
  match expr {
6659
7856
  // ── literals ─────────────────────────────────────────────────────────
@@ -6697,7 +7894,64 @@ impl Codegen {
6697
7894
  let (l, lt) = self.emit_typed_expr(left)?;
6698
7895
  let (r, rt) = self.emit_typed_expr(right)?;
6699
7896
 
7897
+ // An `I32` loop-accumulator (the i32-loop-var lowering) used in a NON-bitwise
7898
+ // expression reads as its signed int32 value coerced to `f64` — every i32 is exact
7899
+ // in f64. Bitwise/shift parents never see this: they recurse into the raw AST via
7900
+ // `emit_int32_operand`, which reads the i32 register directly. So coercing here only
7901
+ // governs arithmetic/relational reads (e.g. the `h * 16777619` excursion), where the
7902
+ // operand must be f64 to keep JS Number semantics.
7903
+ let (l, lt) = if lt == RustType::I32 {
7904
+ (format!("(({}) as f64)", l), RustType::F64)
7905
+ } else {
7906
+ (l, lt)
7907
+ };
7908
+ let (r, rt) = if rt == RustType::I32 {
7909
+ (format!("(({}) as f64)", r), RustType::F64)
7910
+ } else {
7911
+ (r, rt)
7912
+ };
7913
+
6700
7914
  if let Some(result_ty) = RustType::result_type_of_binop(*op, &lt, &rt) {
7915
+ // Bitwise/shift over numbers: lower the *whole* chain in the int32 domain so
7916
+ // intermediate `to_int32`/`to_uint32`↔`f64` round-trips collapse (the win `>>>`
7917
+ // exists for — crypto/hashing loops). Only fires when every leaf proves `f64`;
7918
+ // otherwise we fall through to the per-op lowering below. Behaviour-identical
7919
+ // (see `emit_int32_operand`), and the gauntlet's `typed == boxed == node` check
7920
+ // gates any divergence.
7921
+ if matches!(
7922
+ op,
7923
+ BinOp::BitAnd
7924
+ | BinOp::BitOr
7925
+ | BinOp::BitXor
7926
+ | BinOp::Shl
7927
+ | BinOp::Shr
7928
+ | BinOp::UShr
7929
+ ) && result_ty == RustType::F64
7930
+ {
7931
+ if let Some(int_code) = self.emit_int32_operand(expr)? {
7932
+ // `>>>` yields a uint32 Number; the others yield a signed int32 Number.
7933
+ let f64_code = if matches!(op, BinOp::UShr) {
7934
+ format!("(({}) as u32 as f64)", int_code)
7935
+ } else {
7936
+ format!("(({}) as f64)", int_code)
7937
+ };
7938
+ return Ok((f64_code, RustType::F64));
7939
+ }
7940
+ }
7941
+ // Integer remainder (#174): `x % c` where the dividend `x` is a proven integer
7942
+ // in (-2^53, 2^53) and `c` a positive integer literal → `(x as i64) % c` instead
7943
+ // of `fmod`. Bit-identical (x is exactly an integer in f64; Rust `%` and `fmod`
7944
+ // both truncate toward zero), and far faster — the fmod in LCG/hash recurrences.
7945
+ if matches!(op, BinOp::Mod) && result_ty == RustType::F64 {
7946
+ if let Some(c) = Self::int_literal_value_of(right).filter(|&c| c > 0) {
7947
+ if self.int_range(left, &self.int_range_locals).is_some() {
7948
+ return Ok((
7949
+ format!("(((({}) as i64) % {}i64) as f64)", l, c),
7950
+ RustType::F64,
7951
+ ));
7952
+ }
7953
+ }
7954
+ }
6701
7955
  // Both sides are compatible native types → emit native op.
6702
7956
  let code = match op {
6703
7957
  BinOp::Add if result_ty == RustType::String => {
@@ -6753,6 +8007,37 @@ impl Codegen {
6753
8007
  return Ok((code, result_ty));
6754
8008
  }
6755
8009
 
8010
+ // Mixed numeric relational: one side is a native `f64`, the other a boxed `Value`
8011
+ // (e.g. nsieve's `while (k < n)` where `k` is f64 and the param `n` stayed boxed).
8012
+ // JS does a numeric comparison here — the f64 side forces ToNumber on the other —
8013
+ // so coerce the Value inline (`as_number().unwrap_or(NaN)`) and compare natively,
8014
+ // instead of boxing the f64 side and paying `ops::{lt,le,gt,ge}` + `Value::Bool` +
8015
+ // `is_truthy` every iteration. Behaviour-identical to that boxed path for every
8016
+ // input: a non-number Value coerces to NaN, so all comparisons are `false`, exactly
8017
+ // as `ops::*` returns `false` outside the (Number,Number)/(String,String) cases —
8018
+ // and (String,String) can't reach here since one side is f64.
8019
+ if matches!(op, BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge)
8020
+ && (lt == RustType::F64 || rt == RustType::F64)
8021
+ {
8022
+ let coerce = |code: &str, ty: &RustType| match ty {
8023
+ RustType::F64 => Some(code.to_string()),
8024
+ RustType::Value => {
8025
+ Some(format!("({}).as_number().unwrap_or(f64::NAN)", code))
8026
+ }
8027
+ _ => None,
8028
+ };
8029
+ if let (Some(lc), Some(rc)) = (coerce(&l, &lt), coerce(&r, &rt)) {
8030
+ let sym = match op {
8031
+ BinOp::Lt => "<",
8032
+ BinOp::Le => "<=",
8033
+ BinOp::Gt => ">",
8034
+ BinOp::Ge => ">=",
8035
+ _ => unreachable!(),
8036
+ };
8037
+ return Ok((format!("({} {} {})", lc, sym, rc), RustType::Bool));
8038
+ }
8039
+ }
8040
+
6756
8041
  // Fall back: convert both sides to Value and use the runtime.
6757
8042
  let lv = if lt.is_native() {
6758
8043
  lt.to_value_expr(&l)
@@ -7206,13 +8491,69 @@ impl Codegen {
7206
8491
  RustType::Value => RustType::F64.from_value_expr(&init_code),
7207
8492
  _ => return Ok(None),
7208
8493
  };
8494
+ let acc_esc = Self::escape_ident(acc.as_ref()).into_owned();
8495
+ let x_esc = Self::escape_ident(x.as_ref()).into_owned();
8496
+
8497
+ // ── i64 fast path (#174) ────────────────────────────────────────────────────────
8498
+ // When the receiver is an integer-literal array (element range known) and the body
8499
+ // lowers to native `i64` arithmetic with every node proven integral and < 2^53, run
8500
+ // the fold in `i64` — eliminating `fmod`/f64 round-trips in the hot loop (V8 keeps
8501
+ // these small-integer folds in int registers too). The accumulator's bounded integer
8502
+ // range is found by a small fixpoint seeded from the init's range; bit-identical to
8503
+ // the f64 fold because every intermediate is an exact integer < 2^53.
8504
+ if let Some(elem_r) = self.array_elem_ranges.get(recv_name).copied() {
8505
+ if let Some(init_r) = self.int_range(init_e, &self.int_range_locals) {
8506
+ let mut base = self.int_range_locals.clone();
8507
+ base.insert(x.to_string(), elem_r);
8508
+ let mut acc_r = init_r;
8509
+ let mut converged = false;
8510
+ for _ in 0..6 {
8511
+ let mut m = base.clone();
8512
+ m.insert(acc.to_string(), acc_r);
8513
+ match self.int_range(be, &m) {
8514
+ Some((blo, bhi)) => {
8515
+ let n = (acc_r.0.min(blo), acc_r.1.max(bhi));
8516
+ if n == acc_r {
8517
+ converged = true;
8518
+ break;
8519
+ }
8520
+ acc_r = n;
8521
+ }
8522
+ None => break,
8523
+ }
8524
+ }
8525
+ // Confirm the range is an inductive invariant, then emit the body in i64.
8526
+ let mut m = base.clone();
8527
+ m.insert(acc.to_string(), acc_r);
8528
+ let inductive = converged
8529
+ && matches!(self.int_range(be, &m),
8530
+ Some((blo, bhi)) if blo >= acc_r.0 && bhi <= acc_r.1);
8531
+ if inductive {
8532
+ let i64vars: HashSet<String> =
8533
+ std::iter::once(acc.to_string()).collect();
8534
+ self.type_context.push_scope();
8535
+ self.type_context.define(acc.as_ref(), RustType::F64);
8536
+ self.type_context.define(x.as_ref(), RustType::F64);
8537
+ let body_i64 = self.emit_i64(be, &i64vars, &m)?;
8538
+ self.type_context.pop_scope();
8539
+ if let Some(body_i64) = body_i64 {
8540
+ return Ok(Some((
8541
+ format!(
8542
+ "{{ let mut {acc}: i64 = (({init}) as i64); for {x} in {recv}.iter().copied() {{ {acc} = {body}; }} {acc} as f64 }}",
8543
+ acc = acc_esc, init = init_f64, x = x_esc, recv = recv, body = body_i64
8544
+ ),
8545
+ RustType::F64,
8546
+ )));
8547
+ }
8548
+ }
8549
+ }
8550
+ }
8551
+
7209
8552
  let (body_code, body_ty) =
7210
8553
  emit_with(self, &[(&acc, RustType::F64), (&x, RustType::F64)])?;
7211
8554
  if body_ty != RustType::F64 {
7212
8555
  return Ok(None);
7213
8556
  }
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
8557
  Ok(Some((
7217
8558
  format!(
7218
8559
  "{{ let mut {acc}: f64 = {init}; for {x} in {recv}.iter().copied() {{ {acc} = {body}; }} {acc} }}",
@@ -7309,8 +8650,7 @@ impl Codegen {
7309
8650
  l, r
7310
8651
  ),
7311
8652
  BinOp::Pow => format!(
7312
- "Value::Number({{ let Value::Number(a) = &({}) else {{ panic!() }}; \
7313
- let Value::Number(b) = &({}) else {{ panic!() }}; a.powf(*b) }})",
8653
+ "Value::Number(tishlang_runtime::to_number_value(&({})).powf(tishlang_runtime::to_number_value(&({}))))",
7314
8654
  l, r
7315
8655
  ),
7316
8656
  BinOp::StrictEq => format!("Value::Bool({}.strict_eq(&{}))", l, r),
@@ -7319,14 +8659,18 @@ impl Codegen {
7319
8659
  BinOp::Le => format!("tishlang_runtime::ops::le(&{}, &{})", l, r),
7320
8660
  BinOp::Gt => format!("tishlang_runtime::ops::gt(&{}, &{})", l, r),
7321
8661
  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),
8662
+ // Short-circuit + value-returning && / || (JS, #240): yield the deciding OPERAND, not a
8663
+ // coerced boolean (`five() && 7` is `7`, not `true`). The right operand sits inside the
8664
+ // branch, so its side effects only run when reached. (Typed `bool && bool` uses Rust's
8665
+ // own `&&`/`||` above, where returning the bool already IS the operand.)
8666
+ BinOp::And => format!("{{ let __l = {}; if __l.is_truthy() {{ {} }} else {{ __l }} }}", l, r),
8667
+ BinOp::Or => format!("{{ let __l = {}; if __l.is_truthy() {{ __l }} else {{ {} }} }}", l, r),
7324
8668
  BinOp::BitAnd => Self::emit_bitwise_binop(l, r, "&"),
7325
8669
  BinOp::BitOr => Self::emit_bitwise_binop(l, r, "|"),
7326
8670
  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"),
8671
+ BinOp::Shl => Self::emit_shift_binop(l, r, "to_int32_value", "wrapping_shl"),
8672
+ BinOp::Shr => Self::emit_shift_binop(l, r, "to_int32_value", "wrapping_shr"),
8673
+ BinOp::UShr => Self::emit_shift_binop(l, r, "to_uint32_value", "wrapping_shr"),
7330
8674
  BinOp::In => format!("tish_in_operator(&{}, &{})", l, r),
7331
8675
  BinOp::Eq | BinOp::Ne => {
7332
8676
  return Err(CompileError::new(