@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.
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +5 -1
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/src/cli_help.rs +12 -0
- package/crates/tish/src/main.rs +69 -11
- package/crates/tish_ast/src/ast.rs +12 -5
- package/crates/tish_build_utils/src/lib.rs +37 -0
- package/crates/tish_builtins/src/array.rs +82 -1
- package/crates/tish_builtins/src/globals.rs +50 -16
- package/crates/tish_builtins/src/math.rs +63 -9
- package/crates/tish_builtins/src/number.rs +20 -1
- package/crates/tish_builtins/src/string.rs +68 -4
- package/crates/tish_builtins/src/typedarrays.rs +23 -16
- package/crates/tish_bytecode/src/compiler.rs +94 -28
- package/crates/tish_bytecode/tests/break_continue_bytecode.rs +19 -0
- package/crates/tish_compile/src/check.rs +11 -10
- package/crates/tish_compile/src/codegen.rs +1386 -42
- package/crates/tish_compile/src/infer.rs +16 -16
- package/crates/tish_compile/src/lib.rs +38 -0
- package/crates/tish_compile/src/resolve.rs +3 -2
- package/crates/tish_compile/src/types.rs +26 -9
- package/crates/tish_compile_js/src/codegen.rs +55 -4
- package/crates/tish_compile_js/src/tests_jsx.rs +44 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +1 -0
- package/crates/tish_core/Cargo.toml +2 -0
- package/crates/tish_core/src/json.rs +135 -34
- package/crates/tish_core/src/lib.rs +23 -1
- package/crates/tish_core/src/value.rs +64 -11
- package/crates/tish_eval/src/eval.rs +144 -197
- package/crates/tish_eval/src/natives.rs +45 -45
- package/crates/tish_eval/src/regex.rs +35 -27
- package/crates/tish_eval/src/value.rs +8 -0
- package/crates/tish_fmt/src/lib.rs +46 -2
- package/crates/tish_lint/src/bin/tish-lint.rs +17 -8
- package/crates/tish_lint/src/lib.rs +197 -21
- package/crates/tish_lsp/Cargo.toml +7 -1
- package/crates/tish_lsp/src/import_goto.rs +52 -7
- package/crates/tish_lsp/src/main.rs +913 -145
- package/crates/tish_opt/src/lib.rs +5 -3
- package/crates/tish_parser/src/lib.rs +23 -5
- package/crates/tish_parser/src/parser.rs +35 -35
- package/crates/tish_resolve/src/lib.rs +567 -17
- package/crates/tish_runtime/src/lib.rs +58 -18
- package/crates/tish_ui/src/jsx.rs +2 -2
- package/crates/tish_vm/src/jit.rs +212 -10
- package/crates/tish_vm/src/vm.rs +39 -18
- package/crates/tish_wasm/src/lib.rs +116 -22
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- 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(
|
|
1349
|
-
|
|
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
|
-
///
|
|
1355
|
-
/// (`
|
|
1356
|
-
/// call. Counts go through `
|
|
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({{
|
|
1360
|
-
|
|
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
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
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(
|
|
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::
|
|
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
|
-
|
|
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, <, &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, <), 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(
|
|
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
|
-
|
|
7323
|
-
|
|
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, "
|
|
7328
|
-
BinOp::Shr => Self::emit_shift_binop(l, r, "
|
|
7329
|
-
BinOp::UShr => Self::emit_shift_binop(l, r, "
|
|
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(
|