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