@tishlang/tish 2.8.0 → 2.10.1
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/tish/Cargo.toml +3 -1
- package/crates/tish/src/cli_help.rs +34 -0
- package/crates/tish/src/main.rs +138 -3
- package/crates/tish/tests/integration_test.rs +173 -0
- package/crates/tish_compile/src/codegen.rs +1569 -107
- package/crates/tish_compile/src/infer.rs +1946 -52
- package/crates/tish_compile/tests/perf_codegen_169_173.rs +139 -0
- package/crates/tish_compile_js/Cargo.toml +3 -0
- package/crates/tish_compile_js/src/codegen.rs +414 -5
- package/crates/tish_compile_js/src/lib.rs +3 -1
- package/crates/tish_compile_js/src/tests_jsx.rs +354 -1
- 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
|
@@ -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
|
|
@@ -856,6 +905,10 @@ impl Codegen {
|
|
|
856
905
|
outer_vars_stack: vec![Vec::new()], // Start with module-level scope
|
|
857
906
|
refcell_wrapped_vars: std::collections::HashSet::new(),
|
|
858
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,
|
|
859
912
|
demoted_numeric_locals: std::collections::HashSet::new(),
|
|
860
913
|
int_range_locals: std::collections::HashMap::new(),
|
|
861
914
|
int_valued_locals: std::collections::HashSet::new(),
|
|
@@ -1542,6 +1595,13 @@ impl Codegen {
|
|
|
1542
1595
|
self.writeln("");
|
|
1543
1596
|
}
|
|
1544
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
|
+
}
|
|
1545
1605
|
// Soundness pass — must run after type aliases + `native_fns` are known (both feed the
|
|
1546
1606
|
// native-type oracle): find `number`-typed locals a reassignment can turn non-numeric so
|
|
1547
1607
|
// `VarDecl` lowers them as boxed `Value` rather than native `f64` (else the store coerces
|
|
@@ -1836,6 +1896,11 @@ impl Codegen {
|
|
|
1836
1896
|
let top_level_funcs = self.prescan_function_decls(&program.statements);
|
|
1837
1897
|
*self.function_scope_stack.last_mut().unwrap() = top_level_funcs.clone();
|
|
1838
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
|
+
}
|
|
1839
1904
|
let escaped = Self::escape_ident(func_name);
|
|
1840
1905
|
self.writeln(&format!(
|
|
1841
1906
|
"let {}_cell: VmRef<Value> = VmRef::new(Value::Null);",
|
|
@@ -2032,6 +2097,148 @@ impl Codegen {
|
|
|
2032
2097
|
self.emit_expr(expr)
|
|
2033
2098
|
}
|
|
2034
2099
|
|
|
2100
|
+
/// Is `update` a `+1` step on `var` (`var++`, `++var`, `var += 1`, or `var = var + 1`)?
|
|
2101
|
+
fn is_increment_of(update: &Expr, var: &str) -> bool {
|
|
2102
|
+
match update {
|
|
2103
|
+
Expr::PostfixInc { name, .. } | Expr::PrefixInc { name, .. } => name.as_ref() == var,
|
|
2104
|
+
Expr::CompoundAssign {
|
|
2105
|
+
name,
|
|
2106
|
+
op: CompoundOp::Add,
|
|
2107
|
+
value,
|
|
2108
|
+
..
|
|
2109
|
+
} => name.as_ref() == var && Self::int_literal_value_of(value) == Some(1),
|
|
2110
|
+
Expr::Assign { name, value, .. } => {
|
|
2111
|
+
name.as_ref() == var
|
|
2112
|
+
&& matches!(
|
|
2113
|
+
value.as_ref(),
|
|
2114
|
+
Expr::Binary { left, op: BinOp::Add, right, .. }
|
|
2115
|
+
if matches!(left.as_ref(), Expr::Ident { name: l, .. } if l.as_ref() == var)
|
|
2116
|
+
&& Self::int_literal_value_of(right) == Some(1)
|
|
2117
|
+
)
|
|
2118
|
+
}
|
|
2119
|
+
_ => false,
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
/// #173: detect a fill loop `for (let i = 0; i < N; i++) { a.push(K) }` over a native `Vec<T>`
|
|
2124
|
+
/// and emit it as a single bulk `a.extend(std::iter::repeat(K).take((N) as usize))` — one
|
|
2125
|
+
/// allocation instead of N per-element pushes that repeatedly realloc as the Vec grows. Returns
|
|
2126
|
+
/// `Ok(true)` when the fused form was emitted (the caller then skips the normal loop).
|
|
2127
|
+
///
|
|
2128
|
+
/// Sound only when `N` is a proven, side-effect-free integer (so the bulk count matches the loop
|
|
2129
|
+
/// iteration count exactly, including the truncating `as usize` for `0`/negative) and `K` is a
|
|
2130
|
+
/// constant of the element type (no per-element variation). Any miss returns `Ok(false)` and the
|
|
2131
|
+
/// normal loop is emitted — correctness over coverage.
|
|
2132
|
+
fn try_emit_native_fill_loop(
|
|
2133
|
+
&mut self,
|
|
2134
|
+
init: Option<&Statement>,
|
|
2135
|
+
cond: Option<&Expr>,
|
|
2136
|
+
update: Option<&Expr>,
|
|
2137
|
+
body: &Statement,
|
|
2138
|
+
) -> Result<bool, CompileError> {
|
|
2139
|
+
// init: `let i = 0`
|
|
2140
|
+
let (
|
|
2141
|
+
Some(Statement::VarDecl {
|
|
2142
|
+
name: i_name,
|
|
2143
|
+
init: Some(i_init),
|
|
2144
|
+
..
|
|
2145
|
+
}),
|
|
2146
|
+
Some(cond),
|
|
2147
|
+
Some(update),
|
|
2148
|
+
) = (init, cond, update)
|
|
2149
|
+
else {
|
|
2150
|
+
return Ok(false);
|
|
2151
|
+
};
|
|
2152
|
+
if Self::int_literal_value_of(i_init) != Some(0) {
|
|
2153
|
+
return Ok(false);
|
|
2154
|
+
}
|
|
2155
|
+
// cond: `i < N`
|
|
2156
|
+
let Expr::Binary {
|
|
2157
|
+
left,
|
|
2158
|
+
op: BinOp::Lt,
|
|
2159
|
+
right: bound,
|
|
2160
|
+
..
|
|
2161
|
+
} = cond
|
|
2162
|
+
else {
|
|
2163
|
+
return Ok(false);
|
|
2164
|
+
};
|
|
2165
|
+
let Expr::Ident { name: c_name, .. } = left.as_ref() else {
|
|
2166
|
+
return Ok(false);
|
|
2167
|
+
};
|
|
2168
|
+
if c_name.as_ref() != i_name.as_ref() {
|
|
2169
|
+
return Ok(false);
|
|
2170
|
+
}
|
|
2171
|
+
// update: `i++` / `++i` / `i += 1` / `i = i + 1`
|
|
2172
|
+
if !Self::is_increment_of(update, i_name.as_ref()) {
|
|
2173
|
+
return Ok(false);
|
|
2174
|
+
}
|
|
2175
|
+
// body: exactly one statement `a.push(K)`
|
|
2176
|
+
let push_stmt = match body {
|
|
2177
|
+
Statement::Block { statements, .. } if statements.len() == 1 => &statements[0],
|
|
2178
|
+
Statement::ExprStmt { .. } => body,
|
|
2179
|
+
_ => return Ok(false),
|
|
2180
|
+
};
|
|
2181
|
+
let Statement::ExprStmt {
|
|
2182
|
+
expr: Expr::Call { callee, args, .. },
|
|
2183
|
+
..
|
|
2184
|
+
} = push_stmt
|
|
2185
|
+
else {
|
|
2186
|
+
return Ok(false);
|
|
2187
|
+
};
|
|
2188
|
+
let Expr::Member {
|
|
2189
|
+
object,
|
|
2190
|
+
prop: MemberProp::Name { name: method, .. },
|
|
2191
|
+
optional: false,
|
|
2192
|
+
..
|
|
2193
|
+
} = callee.as_ref()
|
|
2194
|
+
else {
|
|
2195
|
+
return Ok(false);
|
|
2196
|
+
};
|
|
2197
|
+
if method.as_ref() != "push" || args.len() != 1 {
|
|
2198
|
+
return Ok(false);
|
|
2199
|
+
}
|
|
2200
|
+
let Expr::Ident { name: arr_name, .. } = object.as_ref() else {
|
|
2201
|
+
return Ok(false);
|
|
2202
|
+
};
|
|
2203
|
+
// `a` must be a native `Vec<T>`. A closure-captured (RefCell) Vec would need a borrow_mut;
|
|
2204
|
+
// skip it (rare) and keep the plain loop.
|
|
2205
|
+
let RustType::Vec(elem) = self.type_context.get_type(arr_name.as_ref()) else {
|
|
2206
|
+
return Ok(false);
|
|
2207
|
+
};
|
|
2208
|
+
if self.refcell_wrapped_vars.contains(arr_name.as_ref()) {
|
|
2209
|
+
return Ok(false);
|
|
2210
|
+
}
|
|
2211
|
+
let CallArg::Expr(k_expr) = &args[0] else {
|
|
2212
|
+
return Ok(false);
|
|
2213
|
+
};
|
|
2214
|
+
// K must be a constant literal of the element type (no per-element variation, no `i` ref).
|
|
2215
|
+
let k_code = match (&*elem, k_expr) {
|
|
2216
|
+
(RustType::F64, Expr::Literal { value: Literal::Number(n), .. }) => Self::f64_lit(*n),
|
|
2217
|
+
(RustType::Bool, Expr::Literal { value: Literal::Bool(b), .. }) => format!("{}", b),
|
|
2218
|
+
_ => return Ok(false),
|
|
2219
|
+
};
|
|
2220
|
+
// N must be a proven, side-effect-free integer: an integer literal or an int-range local.
|
|
2221
|
+
let n_code = match bound.as_ref() {
|
|
2222
|
+
Expr::Literal {
|
|
2223
|
+
value: Literal::Number(_),
|
|
2224
|
+
..
|
|
2225
|
+
} if Self::int_literal_value_of(bound).is_some() => self.emit_typed_expr(bound)?.0,
|
|
2226
|
+
Expr::Ident { name, .. }
|
|
2227
|
+
if self.int_range_locals.contains_key(name.as_ref())
|
|
2228
|
+
&& self.type_context.get_type(name.as_ref()) == RustType::F64 =>
|
|
2229
|
+
{
|
|
2230
|
+
Self::escape_ident(name.as_ref()).into_owned()
|
|
2231
|
+
}
|
|
2232
|
+
_ => return Ok(false),
|
|
2233
|
+
};
|
|
2234
|
+
let arr_esc = Self::escape_ident(arr_name.as_ref()).into_owned();
|
|
2235
|
+
self.writeln(&format!(
|
|
2236
|
+
"{}.extend(std::iter::repeat({}).take(({}) as usize));",
|
|
2237
|
+
arr_esc, k_code, n_code
|
|
2238
|
+
));
|
|
2239
|
+
Ok(true)
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2035
2242
|
fn emit_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
|
|
2036
2243
|
match stmt {
|
|
2037
2244
|
Statement::Block { statements, .. } => {
|
|
@@ -2135,7 +2342,11 @@ impl Codegen {
|
|
|
2135
2342
|
// Track the variable type
|
|
2136
2343
|
self.type_context.define(name.as_ref(), rust_type.clone());
|
|
2137
2344
|
|
|
2138
|
-
|
|
2345
|
+
// #177: the unboxed `Vec<TishStruct>` local is threaded `&mut` into the aggregate
|
|
2346
|
+
// operators (`advance`/`offsetMomentum`), so it must be `mut` even when never
|
|
2347
|
+
// directly reassigned in source.
|
|
2348
|
+
let force_mut = self.aggregate_array_locals.contains(name.as_ref());
|
|
2349
|
+
let mutability = if *mutable || force_mut { "let mut" } else { "let" };
|
|
2139
2350
|
let escaped_name = Self::escape_ident(name.as_ref());
|
|
2140
2351
|
|
|
2141
2352
|
if rust_type.is_native() {
|
|
@@ -2355,6 +2566,18 @@ impl Codegen {
|
|
|
2355
2566
|
body,
|
|
2356
2567
|
..
|
|
2357
2568
|
} => {
|
|
2569
|
+
// #173: fuse a fill loop `for (let i = 0; i < N; i++) { a.push(K) }` over a native
|
|
2570
|
+
// `Vec<T>` into a single bulk `extend` — one allocation instead of N per-element
|
|
2571
|
+
// pushes (which repeatedly realloc as the Vec grows). Sound only when `N` is a proven,
|
|
2572
|
+
// side-effect-free integer; otherwise the normal loop is emitted below.
|
|
2573
|
+
if self.try_emit_native_fill_loop(
|
|
2574
|
+
init.as_deref(),
|
|
2575
|
+
cond.as_ref(),
|
|
2576
|
+
update.as_ref(),
|
|
2577
|
+
body,
|
|
2578
|
+
)? {
|
|
2579
|
+
return Ok(());
|
|
2580
|
+
}
|
|
2358
2581
|
self.writeln("{");
|
|
2359
2582
|
self.indent += 1;
|
|
2360
2583
|
if let Some(i) = init {
|
|
@@ -2605,6 +2828,15 @@ impl Codegen {
|
|
|
2605
2828
|
span,
|
|
2606
2829
|
..
|
|
2607
2830
|
} => {
|
|
2831
|
+
// #177: this function was de-virtualized into a native aggregate free fn
|
|
2832
|
+
// (`<name>_agg`, emitted before `run()`); all call sites were routed there.
|
|
2833
|
+
// Skip the boxed closure entirely — its body now references unboxed structs
|
|
2834
|
+
// that no longer fit the boxed `Value` ABI.
|
|
2835
|
+
if self.aggregate_alias.is_some()
|
|
2836
|
+
&& self.aggregate_fns.contains_key(name.as_ref())
|
|
2837
|
+
{
|
|
2838
|
+
return Ok(());
|
|
2839
|
+
}
|
|
2608
2840
|
// Use Rc<RefCell<>> pattern to allow recursive function calls
|
|
2609
2841
|
// The function can reference itself through the cell
|
|
2610
2842
|
let name_raw = name.as_ref();
|
|
@@ -3321,6 +3553,14 @@ impl Codegen {
|
|
|
3321
3553
|
}
|
|
3322
3554
|
}
|
|
3323
3555
|
Expr::Call { callee, args, .. } => {
|
|
3556
|
+
// #177: route a top-level call to a de-virtualized aggregate fn. Void fns
|
|
3557
|
+
// (`advance`/`offsetMomentum`) emit `name_agg(&mut bodies, …)` (a `()` statement);
|
|
3558
|
+
// an f64-returning fn (`energy`) is boxed back into `Value::Number` for this path.
|
|
3559
|
+
if !self.aggregate_fns.is_empty() {
|
|
3560
|
+
if let Some((code, _)) = self.try_emit_toplevel_agg_call(callee, args, true)? {
|
|
3561
|
+
return Ok(code);
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3324
3564
|
// Typed-struct shortcut for `JSON.stringify(typedValue)`.
|
|
3325
3565
|
// When the single arg has a known native type that owns a
|
|
3326
3566
|
// hand-rolled `_tish_write_json` (struct or `Vec<struct>`),
|
|
@@ -5897,6 +6137,16 @@ impl Codegen {
|
|
|
5897
6137
|
expr: &Expr,
|
|
5898
6138
|
target_type: &RustType,
|
|
5899
6139
|
) -> Result<String, CompileError> {
|
|
6140
|
+
// #177: `let bodies = makeBodies()` — route the array-factory call to its native free fn
|
|
6141
|
+
// returning `Vec<TishStruct_alias>` directly (no boxed `Value::Array` round-trip).
|
|
6142
|
+
if !self.aggregate_fns.is_empty() {
|
|
6143
|
+
if let Expr::Call { callee, args, .. } = expr {
|
|
6144
|
+
if let Some((code, _)) = self.try_emit_toplevel_agg_call(callee, args, false)? {
|
|
6145
|
+
return Ok(code);
|
|
6146
|
+
}
|
|
6147
|
+
}
|
|
6148
|
+
}
|
|
6149
|
+
|
|
5900
6150
|
// Try to emit literals directly as native types
|
|
5901
6151
|
if let Expr::Literal { value, .. } = expr {
|
|
5902
6152
|
match (target_type, value) {
|
|
@@ -7441,6 +7691,13 @@ impl Codegen {
|
|
|
7441
7691
|
..
|
|
7442
7692
|
} => {
|
|
7443
7693
|
if let Expr::Ident { name: var_name, .. } = object.as_ref() {
|
|
7694
|
+
// #173: `vec.length` on a native `Vec<_>` is a native `f64` (the emitter lowers it
|
|
7695
|
+
// to `(vec.len() as f64)`), so a local fed by `arr.length` stays native.
|
|
7696
|
+
if let Some(RustType::Vec(_)) = env.get(var_name.as_ref()) {
|
|
7697
|
+
if prop_name.as_ref() == "length" {
|
|
7698
|
+
return RustType::F64;
|
|
7699
|
+
}
|
|
7700
|
+
}
|
|
7444
7701
|
if let Some(RustType::Named { fields, .. }) = env.get(var_name.as_ref()) {
|
|
7445
7702
|
if let Some((_, field_ty)) =
|
|
7446
7703
|
fields.iter().find(|(k, _)| k.as_ref() == prop_name.as_ref())
|
|
@@ -7481,6 +7738,12 @@ impl Codegen {
|
|
|
7481
7738
|
}
|
|
7482
7739
|
}
|
|
7483
7740
|
}
|
|
7741
|
+
// #169: a fused native `Vec<f64>` reduce produces an `f64` (the emitter lowers it via
|
|
7742
|
+
// `native_vec_hof_for_call`). Model it here so an accumulator fed by `xs.reduce(...)`
|
|
7743
|
+
// is not wrongly demoted to a boxed `Value`. Conservative: any miss → `Value`.
|
|
7744
|
+
if let Some(t) = self.native_vec_reduce_result_type(callee, args, env) {
|
|
7745
|
+
return t;
|
|
7746
|
+
}
|
|
7484
7747
|
RustType::Value
|
|
7485
7748
|
}
|
|
7486
7749
|
// Unary, Conditional, etc. are not modelled by `emit_typed_expr` (it boxes them), so a
|
|
@@ -7490,6 +7753,81 @@ impl Codegen {
|
|
|
7490
7753
|
}
|
|
7491
7754
|
}
|
|
7492
7755
|
|
|
7756
|
+
/// #169: read-only mirror of [`try_native_vec_hof`]'s `reduce` preconditions, for
|
|
7757
|
+
/// [`expr_native_type`]. Returns `Some(F64)` exactly when a `xs.reduce((acc, x) => body, init)`
|
|
7758
|
+
/// call would fuse to a native `f64` fold (so an accumulator it feeds stays native instead of
|
|
7759
|
+
/// being demoted to a boxed `Value`). Any uncertainty returns `None` — the oracle never claims
|
|
7760
|
+
/// `F64` where the emitter would box, which is what keeps the demotion analysis sound.
|
|
7761
|
+
fn native_vec_reduce_result_type(
|
|
7762
|
+
&self,
|
|
7763
|
+
callee: &Expr,
|
|
7764
|
+
args: &[CallArg],
|
|
7765
|
+
env: &HashMap<String, RustType>,
|
|
7766
|
+
) -> Option<RustType> {
|
|
7767
|
+
if std::env::var("TISH_NATIVE_HOF").is_err() {
|
|
7768
|
+
return None;
|
|
7769
|
+
}
|
|
7770
|
+
let Expr::Member {
|
|
7771
|
+
object,
|
|
7772
|
+
prop: MemberProp::Name { name: method, .. },
|
|
7773
|
+
optional: false,
|
|
7774
|
+
..
|
|
7775
|
+
} = callee
|
|
7776
|
+
else {
|
|
7777
|
+
return None;
|
|
7778
|
+
};
|
|
7779
|
+
if method.as_ref() != "reduce" {
|
|
7780
|
+
return None;
|
|
7781
|
+
}
|
|
7782
|
+
let Expr::Ident { name: recv_name, .. } = object.as_ref() else {
|
|
7783
|
+
return None;
|
|
7784
|
+
};
|
|
7785
|
+
// Receiver must be a native `Vec<f64>` (`.copied()` needs a `Copy` element).
|
|
7786
|
+
match env.get(recv_name.as_ref()) {
|
|
7787
|
+
Some(RustType::Vec(inner)) if **inner == RustType::F64 => {}
|
|
7788
|
+
_ => return None,
|
|
7789
|
+
}
|
|
7790
|
+
// `reduce(callback, init)` with a simple-param expression-body arrow that does not touch the
|
|
7791
|
+
// receiver (an alias inside the closure would break the `.iter()` borrow).
|
|
7792
|
+
if args.len() != 2 {
|
|
7793
|
+
return None;
|
|
7794
|
+
}
|
|
7795
|
+
let Some(CallArg::Expr(Expr::ArrowFunction { params, body, .. })) = args.first() else {
|
|
7796
|
+
return None;
|
|
7797
|
+
};
|
|
7798
|
+
if params.len() != 2 {
|
|
7799
|
+
return None;
|
|
7800
|
+
}
|
|
7801
|
+
let (FunParam::Simple(acc_p), FunParam::Simple(x_p)) = (¶ms[0], ¶ms[1]) else {
|
|
7802
|
+
return None;
|
|
7803
|
+
};
|
|
7804
|
+
if acc_p.default.is_some() || x_p.default.is_some() {
|
|
7805
|
+
return None;
|
|
7806
|
+
}
|
|
7807
|
+
let ArrowBody::Expr(be) = body else {
|
|
7808
|
+
return None;
|
|
7809
|
+
};
|
|
7810
|
+
if crate::infer::pi_mentions(be, recv_name.as_ref()) {
|
|
7811
|
+
return None;
|
|
7812
|
+
}
|
|
7813
|
+
// The init must be native-numeric, and the body must lower to `f64` with both closure params
|
|
7814
|
+
// bound `f64` — exactly the emitter's preconditions, evaluated read-only.
|
|
7815
|
+
let CallArg::Expr(init_e) = &args[1] else {
|
|
7816
|
+
return None;
|
|
7817
|
+
};
|
|
7818
|
+
if self.expr_native_type(init_e, env) != RustType::F64 {
|
|
7819
|
+
return None;
|
|
7820
|
+
}
|
|
7821
|
+
let mut benv = env.clone();
|
|
7822
|
+
benv.insert(acc_p.name.to_string(), RustType::F64);
|
|
7823
|
+
benv.insert(x_p.name.to_string(), RustType::F64);
|
|
7824
|
+
if self.expr_native_type(be, &benv) == RustType::F64 {
|
|
7825
|
+
Some(RustType::F64)
|
|
7826
|
+
} else {
|
|
7827
|
+
None
|
|
7828
|
+
}
|
|
7829
|
+
}
|
|
7830
|
+
|
|
7493
7831
|
/// Names of top-level fns eligible for a parallel native `fn f_native(f64,..)->f64`:
|
|
7494
7832
|
/// non-async, every param `: number` (no default), `: number` return, and a native-safe
|
|
7495
7833
|
/// body (only block/if/return/expr-stmt over native exprs + calls to other eligible fns or
|
|
@@ -7769,121 +8107,1225 @@ impl Codegen {
|
|
|
7769
8107
|
Ok(())
|
|
7770
8108
|
}
|
|
7771
8109
|
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
if
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
None => return Ok(None),
|
|
7807
|
-
};
|
|
7808
|
-
// Shift counts: `(ri as u32)` shares its low 5 bits with `to_uint32(rhs)`, and
|
|
7809
|
-
// `wrapping_sh*` masks the count mod 32 — exactly JS's `count & 31`.
|
|
7810
|
-
let code = match op {
|
|
7811
|
-
BinOp::BitAnd => format!("({} & {})", li, ri),
|
|
7812
|
-
BinOp::BitOr => format!("({} | {})", li, ri),
|
|
7813
|
-
BinOp::BitXor => format!("({} ^ {})", li, ri),
|
|
7814
|
-
BinOp::Shl => format!("({}).wrapping_shl(({}) as u32)", li, ri),
|
|
7815
|
-
BinOp::Shr => format!("({}).wrapping_shr(({}) as u32)", li, ri),
|
|
7816
|
-
// `>>>` is a logical shift on the uint32 view; reinterpret back to `i32` to
|
|
7817
|
-
// stay in the integer domain (the unsigned value is recovered at the f64 edge).
|
|
7818
|
-
BinOp::UShr => {
|
|
7819
|
-
format!("((({}) as u32).wrapping_shr(({}) as u32) as i32)", li, ri)
|
|
7820
|
-
}
|
|
7821
|
-
_ => unreachable!(),
|
|
7822
|
-
};
|
|
7823
|
-
return Ok(Some(code));
|
|
7824
|
-
}
|
|
7825
|
-
}
|
|
7826
|
-
// Leaf: fold only when it is a plain `f64` (so `to_int32` applies directly). `to_int32`
|
|
7827
|
-
// keeps its `is_finite` guard here — a leaf may legitimately be NaN/±Infinity (→ 0).
|
|
7828
|
-
let (code, ty) = self.emit_typed_expr(e)?;
|
|
7829
|
-
if ty == RustType::F64 {
|
|
7830
|
-
// When the leaf is an ARITHMETIC node PROVABLY finite with `|x| < 2^62` (operands are
|
|
7831
|
-
// i32-register reads and finite literals — e.g. the FNV `h * 16777619` excursion), drop
|
|
7832
|
-
// the `is_finite` guard and Rust's saturating cast and truncate directly. Bit-identical
|
|
7833
|
-
// on this domain (`x as i64` truncates toward zero = JS ToInt32 truncation; `as i32` =
|
|
7834
|
-
// modulo 2^32), a few instructions cheaper per iteration. Emitted inline so the generated
|
|
7835
|
-
// crate needs no new runtime symbol. Any unproven leaf keeps the guarded `to_int32`.
|
|
7836
|
-
if matches!(e, Expr::Binary { .. }) && self.f64_finite_bounded_below_2pow62(e) {
|
|
7837
|
-
Ok(Some(format!(
|
|
7838
|
-
"(unsafe {{ ({}).to_int_unchecked::<i64>() }} as i32)",
|
|
7839
|
-
code
|
|
7840
|
-
)))
|
|
7841
|
-
} else {
|
|
7842
|
-
Ok(Some(format!("tishlang_runtime::to_int32({})", code)))
|
|
7843
|
-
}
|
|
7844
|
-
} else if ty == RustType::I32 {
|
|
7845
|
-
// An `I32` loop-accumulator already holds its JS ToInt32 bit-pattern in an integer
|
|
7846
|
-
// register — feed it straight in, NO `to_int32` round-trip. This is the perf win: the
|
|
7847
|
-
// per-op `f64`→i32 narrowing across the hash loop collapses to a register read.
|
|
7848
|
-
Ok(Some(code))
|
|
7849
|
-
} else {
|
|
7850
|
-
Ok(None)
|
|
8110
|
+
// ====================================================================================
|
|
8111
|
+
// #177 (S-E / S-F): interprocedural aggregate (unboxed struct + Vec<Struct>) free fns.
|
|
8112
|
+
//
|
|
8113
|
+
// The infer front-end (S-0..S-D, behind TISH_AGGREGATE_INFER) stamps a struct alias and
|
|
8114
|
+
// `: alias` / `: alias[]` annotations onto the nbody-shape factory/array/operator fns iff a
|
|
8115
|
+
// whole-program candidacy predicate holds (monomorphic all-f64 struct, no `===`/escape/
|
|
8116
|
+
// reshape, write-permitting field ops). Here we consume that: emit each such fn as a native
|
|
8117
|
+
// Rust free fn over `Vec<TishStruct_alias>` threaded by `&mut`/`&`, with element access by
|
|
8118
|
+
// index (so JS reference aliasing — `let bi = bodies[i]; bi.vx = …` — lowers to in-place
|
|
8119
|
+
// `bodies[i].vx = …` and the mutation persists, exactly like the boxed `Value::Array`).
|
|
8120
|
+
//
|
|
8121
|
+
// SOUNDNESS: this is the codegen-side gate. We re-run `analyze_aggregate` to recover the
|
|
8122
|
+
// verdict, then emit every fn into a scratch buffer; if ANY construct can't be lowered the
|
|
8123
|
+
// whole path is disabled (the fns + the call hooks fall back to the boxed closures, byte-
|
|
8124
|
+
// identical to flag-off). So we never half-wire the feature or miscompile.
|
|
8125
|
+
// ====================================================================================
|
|
8126
|
+
|
|
8127
|
+
/// Compute the aggregate group from the stamped `: alias` / `: alias[]` annotations the infer
|
|
8128
|
+
/// front-end wrote (the infer→codegen contract — re-running `analyze_aggregate` on the already
|
|
8129
|
+
/// stamped program is NOT idempotent), emit the native free fns, and (on full success) record
|
|
8130
|
+
/// the routing state so call sites de-virtualize. On any failure the state is left empty (path
|
|
8131
|
+
/// disabled) and nothing is appended to the output.
|
|
8132
|
+
fn setup_aggregate_fns(&mut self, program: &Program) {
|
|
8133
|
+
let dbg = std::env::var("TISH_AGG_DEBUG").is_ok();
|
|
8134
|
+
// The unboxed struct alias: the (unique) name `A` used as an `A[]` array param of some fn,
|
|
8135
|
+
// and registered as an all-`Copy`-field struct alias.
|
|
8136
|
+
let Some(alias) = self.detect_aggregate_alias(program) else {
|
|
8137
|
+
if dbg {
|
|
8138
|
+
eprintln!("[agg] detect_aggregate_alias = None; aliases={:?}", self.type_aliases.keys().collect::<Vec<_>>());
|
|
8139
|
+
}
|
|
8140
|
+
return;
|
|
8141
|
+
};
|
|
8142
|
+
if dbg {
|
|
8143
|
+
eprintln!("[agg] alias = {}", alias);
|
|
7851
8144
|
}
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
fn emit_typed_expr(&mut self, expr: &Expr) -> Result<(String, RustType), CompileError> {
|
|
7855
|
-
match expr {
|
|
7856
|
-
// ── literals ─────────────────────────────────────────────────────────
|
|
7857
|
-
Expr::Literal { value, .. } => match value {
|
|
7858
|
-
Literal::Number(n) => Ok((Self::f64_lit(*n), RustType::F64)),
|
|
7859
|
-
Literal::String(s) => {
|
|
7860
|
-
Ok((format!("{:?}.to_string()", s.as_ref()), RustType::String))
|
|
7861
|
-
}
|
|
7862
|
-
Literal::Bool(b) => Ok((format!("{}", b), RustType::Bool)),
|
|
7863
|
-
Literal::Null => Ok(("Value::Null".to_string(), RustType::Value)),
|
|
7864
|
-
},
|
|
8145
|
+
// Top-level numeric global `let` names available to capture as trailing params.
|
|
8146
|
+
let globals = Self::collect_toplevel_global_lets(program);
|
|
7865
8147
|
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
8148
|
+
// Build a signature for every fn in the group from its stamped annotations.
|
|
8149
|
+
let mut sigs: std::collections::HashMap<String, AggFnSig> = std::collections::HashMap::new();
|
|
8150
|
+
let mut decls: Vec<(String, Vec<FunParam>, Statement)> = Vec::new();
|
|
8151
|
+
for s in &program.statements {
|
|
8152
|
+
if let Statement::FunDecl {
|
|
8153
|
+
async_: false,
|
|
8154
|
+
name,
|
|
8155
|
+
params,
|
|
8156
|
+
rest_param: None,
|
|
8157
|
+
return_type,
|
|
8158
|
+
body,
|
|
8159
|
+
..
|
|
8160
|
+
} = s
|
|
8161
|
+
{
|
|
8162
|
+
let nm = name.to_string();
|
|
8163
|
+
// The array param: a `Simple`-param annotated `: alias[]`.
|
|
8164
|
+
let array_pi = params.iter().position(|p| {
|
|
8165
|
+
matches!(p, FunParam::Simple(tp)
|
|
8166
|
+
if Self::ann_is_array_of(tp.type_ann.as_ref(), &alias))
|
|
8167
|
+
});
|
|
8168
|
+
// Return shape from the stamped return type, else from the body.
|
|
8169
|
+
let ret = if Self::ann_is_simple(return_type.as_ref(), &alias) {
|
|
8170
|
+
AggRet::Struct
|
|
8171
|
+
} else if Self::ann_is_array_of(return_type.as_ref(), &alias) {
|
|
8172
|
+
AggRet::ArrayOfStruct
|
|
8173
|
+
} else if array_pi.is_some() {
|
|
8174
|
+
if Self::stmt_returns_value(body) {
|
|
8175
|
+
AggRet::F64
|
|
7873
8176
|
} else {
|
|
7874
|
-
|
|
8177
|
+
AggRet::Unit
|
|
7875
8178
|
}
|
|
7876
8179
|
} else {
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
8180
|
+
// Not a factory and takes no array param → not in the group.
|
|
8181
|
+
continue;
|
|
8182
|
+
};
|
|
8183
|
+
let array_pname = array_pi.and_then(|pi| match params.get(pi) {
|
|
8184
|
+
Some(FunParam::Simple(tp)) => Some(tp.name.to_string()),
|
|
8185
|
+
_ => None,
|
|
8186
|
+
});
|
|
8187
|
+
let is_mut = array_pname
|
|
8188
|
+
.as_deref()
|
|
8189
|
+
.map(|p| Self::agg_fn_mutates_array(body, p))
|
|
8190
|
+
.unwrap_or(false);
|
|
8191
|
+
// Per-source-param kind.
|
|
8192
|
+
let mut sig_params: Vec<(String, AggParamKind)> = Vec::new();
|
|
8193
|
+
let mut ok = true;
|
|
8194
|
+
for (pi, p) in params.iter().enumerate() {
|
|
8195
|
+
match p {
|
|
8196
|
+
FunParam::Simple(tp) => {
|
|
8197
|
+
let kind = if Some(pi) == array_pi {
|
|
8198
|
+
AggParamKind::Array { is_mut }
|
|
8199
|
+
} else {
|
|
8200
|
+
// Scalar param: must be annotated `: number` (→ f64).
|
|
8201
|
+
let ty = tp
|
|
8202
|
+
.type_ann
|
|
8203
|
+
.as_ref()
|
|
8204
|
+
.map(|t| {
|
|
8205
|
+
crate::types::RustType::from_annotation_with_aliases(
|
|
8206
|
+
t,
|
|
8207
|
+
&self.type_aliases,
|
|
8208
|
+
)
|
|
8209
|
+
})
|
|
8210
|
+
.unwrap_or(RustType::Value);
|
|
8211
|
+
if ty != RustType::F64 {
|
|
8212
|
+
ok = false;
|
|
8213
|
+
break;
|
|
8214
|
+
}
|
|
8215
|
+
AggParamKind::Scalar(ty)
|
|
8216
|
+
};
|
|
8217
|
+
sig_params.push((tp.name.to_string(), kind));
|
|
8218
|
+
}
|
|
8219
|
+
_ => {
|
|
8220
|
+
ok = false;
|
|
8221
|
+
break;
|
|
8222
|
+
}
|
|
7882
8223
|
}
|
|
7883
8224
|
}
|
|
8225
|
+
if !ok {
|
|
8226
|
+
continue;
|
|
8227
|
+
}
|
|
8228
|
+
// Captured globals: free idents in the body that are top-level numeric globals and
|
|
8229
|
+
// not params/locals/group-fn names.
|
|
8230
|
+
let captured = Self::agg_captured_globals(body, params, &globals, &sigs, &nm, &alias);
|
|
8231
|
+
sigs.insert(
|
|
8232
|
+
nm.clone(),
|
|
8233
|
+
AggFnSig {
|
|
8234
|
+
params: sig_params,
|
|
8235
|
+
captured,
|
|
8236
|
+
ret,
|
|
8237
|
+
},
|
|
8238
|
+
);
|
|
8239
|
+
decls.push((nm, params.clone(), (**body).clone()));
|
|
7884
8240
|
}
|
|
8241
|
+
}
|
|
8242
|
+
if dbg {
|
|
8243
|
+
eprintln!("[agg] sigs = {:?}", sigs.keys().collect::<Vec<_>>());
|
|
8244
|
+
}
|
|
8245
|
+
if sigs.is_empty() {
|
|
8246
|
+
return;
|
|
8247
|
+
}
|
|
7885
8248
|
|
|
7886
|
-
|
|
8249
|
+
// Top-level array locals: a `let bodies: alias[] = …` VarDecl.
|
|
8250
|
+
let array_locals: std::collections::HashSet<String> = program
|
|
8251
|
+
.statements
|
|
8252
|
+
.iter()
|
|
8253
|
+
.filter_map(|s| match s {
|
|
8254
|
+
Statement::VarDecl { name, type_ann, .. }
|
|
8255
|
+
if Self::ann_is_array_of(type_ann.as_ref(), &alias) =>
|
|
8256
|
+
{
|
|
8257
|
+
Some(name.to_string())
|
|
8258
|
+
}
|
|
8259
|
+
_ => None,
|
|
8260
|
+
})
|
|
8261
|
+
.collect();
|
|
8262
|
+
|
|
8263
|
+
// Tentatively commit the routing state so `emit_agg_*` can resolve nested group calls.
|
|
8264
|
+
self.aggregate_alias = Some(alias.clone());
|
|
8265
|
+
self.aggregate_fns = sigs;
|
|
8266
|
+
self.aggregate_array_locals = array_locals;
|
|
8267
|
+
|
|
8268
|
+
// Emit every fn into a scratch buffer; on any failure, roll back (disable the path).
|
|
8269
|
+
let saved = std::mem::take(&mut self.output);
|
|
8270
|
+
let saved_indent = self.indent;
|
|
8271
|
+
self.indent = 0;
|
|
8272
|
+
let mut all_ok = true;
|
|
8273
|
+
for (nm, params, body) in &decls {
|
|
8274
|
+
if self.emit_aggregate_fn(nm, params, body).is_err() {
|
|
8275
|
+
all_ok = false;
|
|
8276
|
+
break;
|
|
8277
|
+
}
|
|
8278
|
+
}
|
|
8279
|
+
let emitted = std::mem::replace(&mut self.output, saved);
|
|
8280
|
+
self.indent = saved_indent;
|
|
8281
|
+
if dbg {
|
|
8282
|
+
eprintln!("[agg] all_ok = {} array_locals = {:?}", all_ok, self.aggregate_array_locals);
|
|
8283
|
+
}
|
|
8284
|
+
if all_ok {
|
|
8285
|
+
self.output.push_str(&emitted);
|
|
8286
|
+
self.writeln("");
|
|
8287
|
+
} else {
|
|
8288
|
+
// Roll back: disable the aggregate path entirely.
|
|
8289
|
+
self.aggregate_alias = None;
|
|
8290
|
+
self.aggregate_fns.clear();
|
|
8291
|
+
self.aggregate_array_locals.clear();
|
|
8292
|
+
}
|
|
8293
|
+
}
|
|
8294
|
+
|
|
8295
|
+
/// Emit one aggregate fn as a native Rust free fn (`fn name_agg(..) -> ..`).
|
|
8296
|
+
fn emit_aggregate_fn(
|
|
8297
|
+
&mut self,
|
|
8298
|
+
name: &str,
|
|
8299
|
+
_params: &[FunParam],
|
|
8300
|
+
body: &Statement,
|
|
8301
|
+
) -> Result<(), CompileError> {
|
|
8302
|
+
let sig = self
|
|
8303
|
+
.aggregate_fns
|
|
8304
|
+
.get(name)
|
|
8305
|
+
.cloned()
|
|
8306
|
+
.expect("emit_aggregate_fn: sig present");
|
|
8307
|
+
let alias = self
|
|
8308
|
+
.aggregate_alias
|
|
8309
|
+
.clone()
|
|
8310
|
+
.expect("emit_aggregate_fn: alias present");
|
|
8311
|
+
let struct_ty = crate::types::named_struct_ident(&alias);
|
|
8312
|
+
|
|
8313
|
+
// Build the param list + register types in a fresh scope.
|
|
8314
|
+
self.type_context.push_scope();
|
|
8315
|
+
let mut plist: Vec<String> = Vec::new();
|
|
8316
|
+
let mut array_param: Option<String> = None;
|
|
8317
|
+
for (pname, kind) in &sig.params {
|
|
8318
|
+
let esc = Self::escape_ident(pname).into_owned();
|
|
8319
|
+
match kind {
|
|
8320
|
+
AggParamKind::Array { is_mut } => {
|
|
8321
|
+
let r = if *is_mut { "&mut " } else { "&" };
|
|
8322
|
+
plist.push(format!("{}: {}Vec<{}>", esc, r, struct_ty));
|
|
8323
|
+
array_param = Some(pname.clone());
|
|
8324
|
+
}
|
|
8325
|
+
AggParamKind::Scalar(ty) => {
|
|
8326
|
+
plist.push(format!("{}: {}", esc, ty.to_rust_type_str()));
|
|
8327
|
+
self.type_context.define(pname, ty.clone());
|
|
8328
|
+
}
|
|
8329
|
+
}
|
|
8330
|
+
}
|
|
8331
|
+
for g in &sig.captured {
|
|
8332
|
+
plist.push(format!("{}: f64", Self::escape_ident(g)));
|
|
8333
|
+
self.type_context.define(g, RustType::F64);
|
|
8334
|
+
}
|
|
8335
|
+
let ret_str = match sig.ret {
|
|
8336
|
+
AggRet::Struct => format!(" -> {}", struct_ty),
|
|
8337
|
+
AggRet::ArrayOfStruct => format!(" -> Vec<{}>", struct_ty),
|
|
8338
|
+
AggRet::F64 => " -> f64".to_string(),
|
|
8339
|
+
AggRet::Unit => String::new(),
|
|
8340
|
+
};
|
|
8341
|
+
self.writeln("#[allow(non_snake_case, unused)]");
|
|
8342
|
+
self.writeln(&format!(
|
|
8343
|
+
"fn {}_agg({}){} {{",
|
|
8344
|
+
Self::escape_ident(name),
|
|
8345
|
+
plist.join(", "),
|
|
8346
|
+
ret_str
|
|
8347
|
+
));
|
|
8348
|
+
self.indent += 1;
|
|
8349
|
+
let prev_ret = self.agg_cur_ret.replace(sig.ret.clone());
|
|
8350
|
+
let mut aliases: std::collections::HashMap<String, String> = std::collections::HashMap::new();
|
|
8351
|
+
let res = self.emit_agg_stmt(body, array_param.as_deref(), &mut aliases);
|
|
8352
|
+
self.agg_cur_ret = prev_ret;
|
|
8353
|
+
self.indent -= 1;
|
|
8354
|
+
self.writeln("}");
|
|
8355
|
+
self.type_context.pop_scope();
|
|
8356
|
+
res
|
|
8357
|
+
}
|
|
8358
|
+
|
|
8359
|
+
/// Emit a statement inside an aggregate fn body. `arr` is the array param name (if any);
|
|
8360
|
+
/// `aliases` maps element-alias locals (`let bi = bodies[i]`) to their index-var name.
|
|
8361
|
+
fn emit_agg_stmt(
|
|
8362
|
+
&mut self,
|
|
8363
|
+
stmt: &Statement,
|
|
8364
|
+
arr: Option<&str>,
|
|
8365
|
+
aliases: &mut std::collections::HashMap<String, String>,
|
|
8366
|
+
) -> Result<(), CompileError> {
|
|
8367
|
+
match stmt {
|
|
8368
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
8369
|
+
for s in statements {
|
|
8370
|
+
self.emit_agg_stmt(s, arr, aliases)?;
|
|
8371
|
+
}
|
|
8372
|
+
Ok(())
|
|
8373
|
+
}
|
|
8374
|
+
Statement::VarDecl { name, init, .. } => {
|
|
8375
|
+
let init = init
|
|
8376
|
+
.as_ref()
|
|
8377
|
+
.ok_or_else(|| CompileError::new("agg: uninit let", None))?;
|
|
8378
|
+
// Element alias: `let bi = bodies[i]` (i a bare ident) → record, emit nothing.
|
|
8379
|
+
if let Expr::Index { object, index, .. } = init {
|
|
8380
|
+
if let (Expr::Ident { name: on, .. }, Expr::Ident { name: iv, .. }) =
|
|
8381
|
+
(object.as_ref(), index.as_ref())
|
|
8382
|
+
{
|
|
8383
|
+
if Some(on.as_ref()) == arr {
|
|
8384
|
+
aliases.insert(name.to_string(), iv.to_string());
|
|
8385
|
+
return Ok(());
|
|
8386
|
+
}
|
|
8387
|
+
}
|
|
8388
|
+
}
|
|
8389
|
+
let (code, ty) = self.emit_agg_expr(init, arr, aliases)?;
|
|
8390
|
+
self.type_context.define(name, ty);
|
|
8391
|
+
self.writeln(&format!("let mut {} = {};", Self::escape_ident(name), code));
|
|
8392
|
+
Ok(())
|
|
8393
|
+
}
|
|
8394
|
+
Statement::ExprStmt { expr, .. } => {
|
|
8395
|
+
let code = self.emit_agg_assign(expr, arr, aliases)?;
|
|
8396
|
+
self.writeln(&format!("{};", code));
|
|
8397
|
+
Ok(())
|
|
8398
|
+
}
|
|
8399
|
+
Statement::Return { value, .. } => {
|
|
8400
|
+
let ret = self
|
|
8401
|
+
.agg_cur_ret
|
|
8402
|
+
.clone()
|
|
8403
|
+
.unwrap_or(AggRet::Unit);
|
|
8404
|
+
match (&ret, value) {
|
|
8405
|
+
(AggRet::Unit, _) => {
|
|
8406
|
+
self.writeln("return;");
|
|
8407
|
+
Ok(())
|
|
8408
|
+
}
|
|
8409
|
+
(AggRet::F64, Some(e)) => {
|
|
8410
|
+
let (code, ty) = self.emit_agg_expr(e, arr, aliases)?;
|
|
8411
|
+
let f = if ty == RustType::F64 {
|
|
8412
|
+
code
|
|
8413
|
+
} else {
|
|
8414
|
+
return Err(CompileError::new("agg: non-f64 return", None));
|
|
8415
|
+
};
|
|
8416
|
+
self.writeln(&format!("return {};", f));
|
|
8417
|
+
Ok(())
|
|
8418
|
+
}
|
|
8419
|
+
(AggRet::Struct, Some(e)) => {
|
|
8420
|
+
let code = self.emit_agg_struct_literal(e, arr, aliases)?;
|
|
8421
|
+
self.writeln(&format!("return {};", code));
|
|
8422
|
+
Ok(())
|
|
8423
|
+
}
|
|
8424
|
+
(AggRet::ArrayOfStruct, Some(e)) => {
|
|
8425
|
+
let code = self.emit_agg_array_literal(e, arr, aliases)?;
|
|
8426
|
+
self.writeln(&format!("return {};", code));
|
|
8427
|
+
Ok(())
|
|
8428
|
+
}
|
|
8429
|
+
_ => Err(CompileError::new("agg: bad return", None)),
|
|
8430
|
+
}
|
|
8431
|
+
}
|
|
8432
|
+
Statement::For {
|
|
8433
|
+
init,
|
|
8434
|
+
cond,
|
|
8435
|
+
update,
|
|
8436
|
+
body,
|
|
8437
|
+
..
|
|
8438
|
+
} => {
|
|
8439
|
+
// `for (init; cond; update) body` → `{ init; while cond { body; update; } }`.
|
|
8440
|
+
// The candidacy predicate forbids `break`/`continue` reaching this emitter (the
|
|
8441
|
+
// codegen gate below bails on them), so the lowering is faithful.
|
|
8442
|
+
self.writeln("{");
|
|
8443
|
+
self.indent += 1;
|
|
8444
|
+
if let Some(i) = init {
|
|
8445
|
+
self.emit_agg_stmt(i, arr, aliases)?;
|
|
8446
|
+
}
|
|
8447
|
+
let cond_code = match cond {
|
|
8448
|
+
Some(c) => {
|
|
8449
|
+
let (code, _) = self.emit_agg_expr(c, arr, aliases)?;
|
|
8450
|
+
code
|
|
8451
|
+
}
|
|
8452
|
+
None => "true".to_string(),
|
|
8453
|
+
};
|
|
8454
|
+
self.writeln(&format!("while {} {{", cond_code));
|
|
8455
|
+
self.indent += 1;
|
|
8456
|
+
self.emit_agg_stmt(body, arr, aliases)?;
|
|
8457
|
+
if let Some(u) = update {
|
|
8458
|
+
let ucode = self.emit_agg_assign(u, arr, aliases)?;
|
|
8459
|
+
self.writeln(&format!("{};", ucode));
|
|
8460
|
+
}
|
|
8461
|
+
self.indent -= 1;
|
|
8462
|
+
self.writeln("}");
|
|
8463
|
+
self.indent -= 1;
|
|
8464
|
+
self.writeln("}");
|
|
8465
|
+
Ok(())
|
|
8466
|
+
}
|
|
8467
|
+
_ => Err(CompileError::new("agg: unsupported statement", None)),
|
|
8468
|
+
}
|
|
8469
|
+
}
|
|
8470
|
+
|
|
8471
|
+
/// Emit an assignment / increment expression in statement position inside an aggregate fn.
|
|
8472
|
+
fn emit_agg_assign(
|
|
8473
|
+
&mut self,
|
|
8474
|
+
expr: &Expr,
|
|
8475
|
+
arr: Option<&str>,
|
|
8476
|
+
aliases: &std::collections::HashMap<String, String>,
|
|
8477
|
+
) -> Result<String, CompileError> {
|
|
8478
|
+
match expr {
|
|
8479
|
+
// `px = <f64>` — scalar local/param assign.
|
|
8480
|
+
Expr::Assign { name, value, .. } => {
|
|
8481
|
+
let (v, _) = self.emit_agg_expr(value, arr, aliases)?;
|
|
8482
|
+
Ok(format!("{} = {}", Self::escape_ident(name), v))
|
|
8483
|
+
}
|
|
8484
|
+
Expr::CompoundAssign {
|
|
8485
|
+
name, op, value, ..
|
|
8486
|
+
} => {
|
|
8487
|
+
let (v, _) = self.emit_agg_expr(value, arr, aliases)?;
|
|
8488
|
+
let op_str = match op {
|
|
8489
|
+
CompoundOp::Add => "+=",
|
|
8490
|
+
CompoundOp::Sub => "-=",
|
|
8491
|
+
CompoundOp::Mul => "*=",
|
|
8492
|
+
CompoundOp::Div => "/=",
|
|
8493
|
+
CompoundOp::Mod => "%=",
|
|
8494
|
+
};
|
|
8495
|
+
Ok(format!("{} {} {}", Self::escape_ident(name), op_str, v))
|
|
8496
|
+
}
|
|
8497
|
+
// `i++` / `++i` / `i--` / `--i` (loop update) → native f64 step.
|
|
8498
|
+
Expr::PostfixInc { name, .. } | Expr::PrefixInc { name, .. } => {
|
|
8499
|
+
Ok(format!("{} += 1f64", Self::escape_ident(name)))
|
|
8500
|
+
}
|
|
8501
|
+
Expr::PostfixDec { name, .. } | Expr::PrefixDec { name, .. } => {
|
|
8502
|
+
Ok(format!("{} -= 1f64", Self::escape_ident(name)))
|
|
8503
|
+
}
|
|
8504
|
+
// `bi.vx = <f64>` (alias) / `bodies[i].vx = <f64>` (direct index) field write.
|
|
8505
|
+
Expr::MemberAssign {
|
|
8506
|
+
object,
|
|
8507
|
+
prop,
|
|
8508
|
+
value,
|
|
8509
|
+
..
|
|
8510
|
+
} => {
|
|
8511
|
+
let field = crate::types::field_ident(prop.as_ref());
|
|
8512
|
+
let place = self.emit_agg_place(object, arr, aliases)?;
|
|
8513
|
+
let (v, _) = self.emit_agg_expr(value, arr, aliases)?;
|
|
8514
|
+
Ok(format!("{}.{} = {}", place, field, v))
|
|
8515
|
+
}
|
|
8516
|
+
_ => Err(CompileError::new("agg: unsupported statement expr", None)),
|
|
8517
|
+
}
|
|
8518
|
+
}
|
|
8519
|
+
|
|
8520
|
+
/// Emit the array-element place for a field write target: an alias ident `bi` or `bodies[i]`.
|
|
8521
|
+
fn emit_agg_place(
|
|
8522
|
+
&mut self,
|
|
8523
|
+
object: &Expr,
|
|
8524
|
+
arr: Option<&str>,
|
|
8525
|
+
aliases: &std::collections::HashMap<String, String>,
|
|
8526
|
+
) -> Result<String, CompileError> {
|
|
8527
|
+
match object {
|
|
8528
|
+
Expr::Ident { name, .. } => {
|
|
8529
|
+
if let Some(idxvar) = aliases.get(name.as_ref()) {
|
|
8530
|
+
let a = arr.ok_or_else(|| CompileError::new("agg: alias no array", None))?;
|
|
8531
|
+
return Ok(format!(
|
|
8532
|
+
"{}[({}) as usize]",
|
|
8533
|
+
Self::escape_ident(a),
|
|
8534
|
+
Self::escape_ident(idxvar)
|
|
8535
|
+
));
|
|
8536
|
+
}
|
|
8537
|
+
Err(CompileError::new("agg: bad write target", None))
|
|
8538
|
+
}
|
|
8539
|
+
Expr::Index { object: io, index, .. } => {
|
|
8540
|
+
if let Expr::Ident { name: on, .. } = io.as_ref() {
|
|
8541
|
+
if Some(on.as_ref()) == arr {
|
|
8542
|
+
let (idx, _) = self.emit_agg_expr(index, arr, aliases)?;
|
|
8543
|
+
return Ok(format!("{}[({}) as usize]", Self::escape_ident(on.as_ref()), idx));
|
|
8544
|
+
}
|
|
8545
|
+
}
|
|
8546
|
+
Err(CompileError::new("agg: bad index write target", None))
|
|
8547
|
+
}
|
|
8548
|
+
_ => Err(CompileError::new("agg: bad write target", None)),
|
|
8549
|
+
}
|
|
8550
|
+
}
|
|
8551
|
+
|
|
8552
|
+
/// Emit a (scalar / bool) expression inside an aggregate fn body.
|
|
8553
|
+
fn emit_agg_expr(
|
|
8554
|
+
&mut self,
|
|
8555
|
+
e: &Expr,
|
|
8556
|
+
arr: Option<&str>,
|
|
8557
|
+
aliases: &std::collections::HashMap<String, String>,
|
|
8558
|
+
) -> Result<(String, RustType), CompileError> {
|
|
8559
|
+
match e {
|
|
8560
|
+
Expr::Literal {
|
|
8561
|
+
value: Literal::Number(n),
|
|
8562
|
+
..
|
|
8563
|
+
} => Ok((Self::f64_lit(*n), RustType::F64)),
|
|
8564
|
+
Expr::Literal {
|
|
8565
|
+
value: Literal::Bool(b),
|
|
8566
|
+
..
|
|
8567
|
+
} => Ok((format!("{}", b), RustType::Bool)),
|
|
8568
|
+
Expr::Ident { name, .. } => {
|
|
8569
|
+
let ty = self.type_context.get_type(name.as_ref());
|
|
8570
|
+
Ok((Self::escape_ident(name.as_ref()).into_owned(), ty))
|
|
8571
|
+
}
|
|
8572
|
+
Expr::Unary {
|
|
8573
|
+
op: UnaryOp::Neg,
|
|
8574
|
+
operand,
|
|
8575
|
+
..
|
|
8576
|
+
} => {
|
|
8577
|
+
let (o, _) = self.emit_agg_expr(operand, arr, aliases)?;
|
|
8578
|
+
Ok((format!("(-({}))", o), RustType::F64))
|
|
8579
|
+
}
|
|
8580
|
+
Expr::Unary {
|
|
8581
|
+
op: UnaryOp::Pos,
|
|
8582
|
+
operand,
|
|
8583
|
+
..
|
|
8584
|
+
} => {
|
|
8585
|
+
let (o, _) = self.emit_agg_expr(operand, arr, aliases)?;
|
|
8586
|
+
Ok((format!("({})", o), RustType::F64))
|
|
8587
|
+
}
|
|
8588
|
+
Expr::Binary {
|
|
8589
|
+
left, op, right, ..
|
|
8590
|
+
} => {
|
|
8591
|
+
let (l, _) = self.emit_agg_expr(left, arr, aliases)?;
|
|
8592
|
+
let (r, _) = self.emit_agg_expr(right, arr, aliases)?;
|
|
8593
|
+
let (code, ty) = match op {
|
|
8594
|
+
BinOp::Add => (format!("({} + {})", l, r), RustType::F64),
|
|
8595
|
+
BinOp::Sub => (format!("({} - {})", l, r), RustType::F64),
|
|
8596
|
+
BinOp::Mul => (format!("({} * {})", l, r), RustType::F64),
|
|
8597
|
+
BinOp::Div => (format!("({} / {})", l, r), RustType::F64),
|
|
8598
|
+
BinOp::Mod => (format!("({} % {})", l, r), RustType::F64),
|
|
8599
|
+
BinOp::Pow => (format!("({}).powf({})", l, r), RustType::F64),
|
|
8600
|
+
BinOp::Lt => (format!("({} < {})", l, r), RustType::Bool),
|
|
8601
|
+
BinOp::Le => (format!("({} <= {})", l, r), RustType::Bool),
|
|
8602
|
+
BinOp::Gt => (format!("({} > {})", l, r), RustType::Bool),
|
|
8603
|
+
BinOp::Ge => (format!("({} >= {})", l, r), RustType::Bool),
|
|
8604
|
+
_ => {
|
|
8605
|
+
return Err(CompileError::new("agg: unsupported binop", None))
|
|
8606
|
+
}
|
|
8607
|
+
};
|
|
8608
|
+
Ok((code, ty))
|
|
8609
|
+
}
|
|
8610
|
+
Expr::Member {
|
|
8611
|
+
object,
|
|
8612
|
+
prop: MemberProp::Name { name: m, .. },
|
|
8613
|
+
optional: false,
|
|
8614
|
+
..
|
|
8615
|
+
} => {
|
|
8616
|
+
if let Expr::Ident { name: on, .. } = object.as_ref() {
|
|
8617
|
+
// `bodies.length` → `(len() as f64)`.
|
|
8618
|
+
if Some(on.as_ref()) == arr && m.as_ref() == "length" {
|
|
8619
|
+
return Ok((
|
|
8620
|
+
format!("({}.len() as f64)", Self::escape_ident(on.as_ref())),
|
|
8621
|
+
RustType::F64,
|
|
8622
|
+
));
|
|
8623
|
+
}
|
|
8624
|
+
// `bi.field` (element alias) → `bodies[i].field`.
|
|
8625
|
+
if let Some(idxvar) = aliases.get(on.as_ref()) {
|
|
8626
|
+
let a = arr
|
|
8627
|
+
.ok_or_else(|| CompileError::new("agg: alias no array", None))?;
|
|
8628
|
+
return Ok((
|
|
8629
|
+
format!(
|
|
8630
|
+
"{}[({}) as usize].{}",
|
|
8631
|
+
Self::escape_ident(a),
|
|
8632
|
+
Self::escape_ident(idxvar),
|
|
8633
|
+
crate::types::field_ident(m.as_ref())
|
|
8634
|
+
),
|
|
8635
|
+
RustType::F64,
|
|
8636
|
+
));
|
|
8637
|
+
}
|
|
8638
|
+
// `localStruct.field` (a `Named` local).
|
|
8639
|
+
let ty = self.type_context.get_type(on.as_ref());
|
|
8640
|
+
if let RustType::Named { fields, .. } = &ty {
|
|
8641
|
+
if let Some((_, ft)) =
|
|
8642
|
+
fields.iter().find(|(k, _)| k.as_ref() == m.as_ref())
|
|
8643
|
+
{
|
|
8644
|
+
return Ok((
|
|
8645
|
+
format!(
|
|
8646
|
+
"{}.{}",
|
|
8647
|
+
Self::escape_ident(on.as_ref()),
|
|
8648
|
+
crate::types::field_ident(m.as_ref())
|
|
8649
|
+
),
|
|
8650
|
+
ft.clone(),
|
|
8651
|
+
));
|
|
8652
|
+
}
|
|
8653
|
+
}
|
|
8654
|
+
}
|
|
8655
|
+
// `bodies[i].field` read.
|
|
8656
|
+
if let Expr::Index { object: io, index, .. } = object.as_ref() {
|
|
8657
|
+
if let Expr::Ident { name: on, .. } = io.as_ref() {
|
|
8658
|
+
if Some(on.as_ref()) == arr {
|
|
8659
|
+
let (idx, _) = self.emit_agg_expr(index, arr, aliases)?;
|
|
8660
|
+
return Ok((
|
|
8661
|
+
format!(
|
|
8662
|
+
"{}[({}) as usize].{}",
|
|
8663
|
+
Self::escape_ident(on.as_ref()),
|
|
8664
|
+
idx,
|
|
8665
|
+
crate::types::field_ident(m.as_ref())
|
|
8666
|
+
),
|
|
8667
|
+
RustType::F64,
|
|
8668
|
+
));
|
|
8669
|
+
}
|
|
8670
|
+
}
|
|
8671
|
+
}
|
|
8672
|
+
Err(CompileError::new("agg: unsupported member", None))
|
|
8673
|
+
}
|
|
8674
|
+
Expr::Call { callee, args, .. } => {
|
|
8675
|
+
// Nested call to another group fn (e.g. `body(...)` from `makeBodies`).
|
|
8676
|
+
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
8677
|
+
if self.aggregate_fns.contains_key(fname.as_ref()) {
|
|
8678
|
+
let (code, ret) =
|
|
8679
|
+
self.emit_agg_group_call(fname.as_ref(), args, arr, aliases)?;
|
|
8680
|
+
let ty = match ret {
|
|
8681
|
+
AggRet::F64 => RustType::F64,
|
|
8682
|
+
AggRet::Struct => {
|
|
8683
|
+
let alias = self.aggregate_alias.clone().unwrap();
|
|
8684
|
+
RustType::Named {
|
|
8685
|
+
name: alias.as_str().into(),
|
|
8686
|
+
fields: Vec::new(),
|
|
8687
|
+
}
|
|
8688
|
+
}
|
|
8689
|
+
_ => {
|
|
8690
|
+
return Err(CompileError::new(
|
|
8691
|
+
"agg: call return not usable here",
|
|
8692
|
+
None,
|
|
8693
|
+
))
|
|
8694
|
+
}
|
|
8695
|
+
};
|
|
8696
|
+
return Ok((code, ty));
|
|
8697
|
+
}
|
|
8698
|
+
}
|
|
8699
|
+
// `Math.<fn>(x)` clean f64-method intrinsics.
|
|
8700
|
+
if let Expr::Member {
|
|
8701
|
+
object,
|
|
8702
|
+
prop: MemberProp::Name { name: method, .. },
|
|
8703
|
+
..
|
|
8704
|
+
} = callee.as_ref()
|
|
8705
|
+
{
|
|
8706
|
+
if matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
|
|
8707
|
+
{
|
|
8708
|
+
if let Some(m) = Self::agg_clean_math_method(method.as_ref()) {
|
|
8709
|
+
if let [CallArg::Expr(a)] = args.as_slice() {
|
|
8710
|
+
let (ac, _) = self.emit_agg_expr(a, arr, aliases)?;
|
|
8711
|
+
return Ok((format!("({}).{}()", ac, m), RustType::F64));
|
|
8712
|
+
}
|
|
8713
|
+
}
|
|
8714
|
+
}
|
|
8715
|
+
}
|
|
8716
|
+
Err(CompileError::new("agg: unsupported call", None))
|
|
8717
|
+
}
|
|
8718
|
+
Expr::Conditional {
|
|
8719
|
+
cond,
|
|
8720
|
+
then_branch,
|
|
8721
|
+
else_branch,
|
|
8722
|
+
..
|
|
8723
|
+
} => {
|
|
8724
|
+
let (c, _) = self.emit_agg_expr(cond, arr, aliases)?;
|
|
8725
|
+
let (t, _) = self.emit_agg_expr(then_branch, arr, aliases)?;
|
|
8726
|
+
let (el, _) = self.emit_agg_expr(else_branch, arr, aliases)?;
|
|
8727
|
+
Ok((format!("(if {} {{ {} }} else {{ {} }})", c, t, el), RustType::F64))
|
|
8728
|
+
}
|
|
8729
|
+
_ => Err(CompileError::new("agg: unsupported expr", None)),
|
|
8730
|
+
}
|
|
8731
|
+
}
|
|
8732
|
+
|
|
8733
|
+
/// Emit a `return { ... }` object literal as a native struct literal (the `body()` factory).
|
|
8734
|
+
fn emit_agg_struct_literal(
|
|
8735
|
+
&mut self,
|
|
8736
|
+
e: &Expr,
|
|
8737
|
+
arr: Option<&str>,
|
|
8738
|
+
aliases: &std::collections::HashMap<String, String>,
|
|
8739
|
+
) -> Result<String, CompileError> {
|
|
8740
|
+
let alias = self.aggregate_alias.clone().unwrap();
|
|
8741
|
+
let struct_ty = crate::types::named_struct_ident(&alias);
|
|
8742
|
+
let fields = match self.type_aliases.get(&alias) {
|
|
8743
|
+
Some(RustType::Named { fields, .. }) | Some(RustType::Object(fields)) => fields.clone(),
|
|
8744
|
+
_ => return Err(CompileError::new("agg: alias not a struct", None)),
|
|
8745
|
+
};
|
|
8746
|
+
let Expr::Object { props, .. } = e else {
|
|
8747
|
+
return Err(CompileError::new("agg: struct return not literal", None));
|
|
8748
|
+
};
|
|
8749
|
+
use std::collections::HashMap;
|
|
8750
|
+
let mut by_key: HashMap<String, &Expr> = HashMap::new();
|
|
8751
|
+
for p in props {
|
|
8752
|
+
match p {
|
|
8753
|
+
ObjectProp::KeyValue(k, v, _) => {
|
|
8754
|
+
by_key.insert(k.to_string(), v);
|
|
8755
|
+
}
|
|
8756
|
+
ObjectProp::Spread(_) => {
|
|
8757
|
+
return Err(CompileError::new("agg: struct spread", None))
|
|
8758
|
+
}
|
|
8759
|
+
}
|
|
8760
|
+
}
|
|
8761
|
+
let mut inits: Vec<String> = Vec::new();
|
|
8762
|
+
for (k, _) in &fields {
|
|
8763
|
+
let v = by_key
|
|
8764
|
+
.get(k.as_ref())
|
|
8765
|
+
.ok_or_else(|| CompileError::new("agg: struct missing field", None))?;
|
|
8766
|
+
let (code, _) = self.emit_agg_expr(v, arr, aliases)?;
|
|
8767
|
+
inits.push(format!("{}: {}", crate::types::field_ident(k.as_ref()), code));
|
|
8768
|
+
}
|
|
8769
|
+
Ok(format!("{} {{ {} }}", struct_ty, inits.join(", ")))
|
|
8770
|
+
}
|
|
8771
|
+
|
|
8772
|
+
/// Emit a `return [a, b, c]` array literal of struct-typed idents as `vec![a, b, c]`.
|
|
8773
|
+
fn emit_agg_array_literal(
|
|
8774
|
+
&mut self,
|
|
8775
|
+
e: &Expr,
|
|
8776
|
+
_arr: Option<&str>,
|
|
8777
|
+
_aliases: &std::collections::HashMap<String, String>,
|
|
8778
|
+
) -> Result<String, CompileError> {
|
|
8779
|
+
let Expr::Array { elements, .. } = e else {
|
|
8780
|
+
return Err(CompileError::new("agg: array return not literal", None));
|
|
8781
|
+
};
|
|
8782
|
+
let mut items: Vec<String> = Vec::new();
|
|
8783
|
+
for el in elements {
|
|
8784
|
+
match el {
|
|
8785
|
+
ArrayElement::Expr(Expr::Ident { name, .. }) => {
|
|
8786
|
+
items.push(Self::escape_ident(name.as_ref()).into_owned());
|
|
8787
|
+
}
|
|
8788
|
+
_ => {
|
|
8789
|
+
return Err(CompileError::new(
|
|
8790
|
+
"agg: array element not a struct ident",
|
|
8791
|
+
None,
|
|
8792
|
+
))
|
|
8793
|
+
}
|
|
8794
|
+
}
|
|
8795
|
+
}
|
|
8796
|
+
Ok(format!("vec![{}]", items.join(", ")))
|
|
8797
|
+
}
|
|
8798
|
+
|
|
8799
|
+
/// Emit a direct call to a group fn, threading the array by `&mut`/`&` plus captured globals.
|
|
8800
|
+
/// Returns the call code and the callee's return shape. `arr`/`aliases` describe the CALLER's
|
|
8801
|
+
/// context (so an array arg can be the caller's array param, though nbody only passes scalars
|
|
8802
|
+
/// across nested group calls).
|
|
8803
|
+
fn emit_agg_group_call(
|
|
8804
|
+
&mut self,
|
|
8805
|
+
name: &str,
|
|
8806
|
+
args: &[CallArg],
|
|
8807
|
+
arr: Option<&str>,
|
|
8808
|
+
aliases: &std::collections::HashMap<String, String>,
|
|
8809
|
+
) -> Result<(String, AggRet), CompileError> {
|
|
8810
|
+
let sig = self
|
|
8811
|
+
.aggregate_fns
|
|
8812
|
+
.get(name)
|
|
8813
|
+
.cloned()
|
|
8814
|
+
.ok_or_else(|| CompileError::new("agg: unknown group fn", None))?;
|
|
8815
|
+
let mut call_args: Vec<String> = Vec::new();
|
|
8816
|
+
for (i, (_pname, kind)) in sig.params.iter().enumerate() {
|
|
8817
|
+
let a = match args.get(i) {
|
|
8818
|
+
Some(CallArg::Expr(e)) => e,
|
|
8819
|
+
_ => return Err(CompileError::new("agg: call arg shape", None)),
|
|
8820
|
+
};
|
|
8821
|
+
match kind {
|
|
8822
|
+
AggParamKind::Array { is_mut } => {
|
|
8823
|
+
// The arg must be a bare ident naming an array (caller's param or local).
|
|
8824
|
+
let Expr::Ident { name: an, .. } = a else {
|
|
8825
|
+
return Err(CompileError::new("agg: array arg not ident", None));
|
|
8826
|
+
};
|
|
8827
|
+
let r = if *is_mut { "&mut " } else { "&" };
|
|
8828
|
+
call_args.push(format!("{}{}", r, Self::escape_ident(an.as_ref())));
|
|
8829
|
+
}
|
|
8830
|
+
AggParamKind::Scalar(_) => {
|
|
8831
|
+
let (code, _) = self.emit_agg_expr(a, arr, aliases)?;
|
|
8832
|
+
call_args.push(code);
|
|
8833
|
+
}
|
|
8834
|
+
}
|
|
8835
|
+
}
|
|
8836
|
+
// Captured globals are visible as same-named f64 params/locals in the caller too.
|
|
8837
|
+
for g in &sig.captured {
|
|
8838
|
+
call_args.push(Self::escape_ident(g).into_owned());
|
|
8839
|
+
}
|
|
8840
|
+
Ok((
|
|
8841
|
+
format!("{}_agg({})", Self::escape_ident(name), call_args.join(", ")),
|
|
8842
|
+
sig.ret,
|
|
8843
|
+
))
|
|
8844
|
+
}
|
|
8845
|
+
|
|
8846
|
+
/// Try to route a top-level call `name(args)` to its aggregate free fn. `as_value` wraps an
|
|
8847
|
+
/// f64 result in `Value::Number` for the boxed `emit_expr` context. Returns `None` if `name`
|
|
8848
|
+
/// isn't a group fn (caller falls back to the normal path).
|
|
8849
|
+
fn try_emit_toplevel_agg_call(
|
|
8850
|
+
&mut self,
|
|
8851
|
+
callee: &Expr,
|
|
8852
|
+
args: &[CallArg],
|
|
8853
|
+
as_value: bool,
|
|
8854
|
+
) -> Result<Option<(String, RustType)>, CompileError> {
|
|
8855
|
+
let Expr::Ident { name, .. } = callee else {
|
|
8856
|
+
return Ok(None);
|
|
8857
|
+
};
|
|
8858
|
+
if !self.aggregate_fns.contains_key(name.as_ref()) {
|
|
8859
|
+
return Ok(None);
|
|
8860
|
+
}
|
|
8861
|
+
let sig = self.aggregate_fns.get(name.as_ref()).cloned().unwrap();
|
|
8862
|
+
let mut call_args: Vec<String> = Vec::new();
|
|
8863
|
+
for (i, (_pname, kind)) in sig.params.iter().enumerate() {
|
|
8864
|
+
let a = match args.get(i) {
|
|
8865
|
+
Some(CallArg::Expr(e)) => e,
|
|
8866
|
+
_ => return Ok(None),
|
|
8867
|
+
};
|
|
8868
|
+
match kind {
|
|
8869
|
+
AggParamKind::Array { is_mut } => {
|
|
8870
|
+
let Expr::Ident { name: an, .. } = a else {
|
|
8871
|
+
return Ok(None);
|
|
8872
|
+
};
|
|
8873
|
+
let r = if *is_mut { "&mut " } else { "&" };
|
|
8874
|
+
call_args.push(format!("{}{}", r, Self::escape_ident(an.as_ref())));
|
|
8875
|
+
}
|
|
8876
|
+
AggParamKind::Scalar(_) => {
|
|
8877
|
+
let (code, ty) = self.emit_typed_expr(a)?;
|
|
8878
|
+
let f = if ty == RustType::F64 {
|
|
8879
|
+
code
|
|
8880
|
+
} else if ty == RustType::Value {
|
|
8881
|
+
RustType::F64.from_value_expr(&code)
|
|
8882
|
+
} else {
|
|
8883
|
+
code
|
|
8884
|
+
};
|
|
8885
|
+
call_args.push(f);
|
|
8886
|
+
}
|
|
8887
|
+
}
|
|
8888
|
+
}
|
|
8889
|
+
for g in &sig.captured {
|
|
8890
|
+
let (code, ty) = self.emit_typed_expr(&Expr::Ident {
|
|
8891
|
+
name: g.as_str().into(),
|
|
8892
|
+
span: tishlang_ast::Span::default(),
|
|
8893
|
+
})?;
|
|
8894
|
+
let f = if ty == RustType::F64 {
|
|
8895
|
+
code
|
|
8896
|
+
} else if ty == RustType::Value {
|
|
8897
|
+
RustType::F64.from_value_expr(&code)
|
|
8898
|
+
} else {
|
|
8899
|
+
code
|
|
8900
|
+
};
|
|
8901
|
+
call_args.push(f);
|
|
8902
|
+
}
|
|
8903
|
+
let call = format!("{}_agg({})", Self::escape_ident(name.as_ref()), call_args.join(", "));
|
|
8904
|
+
let (code, ty) = match sig.ret {
|
|
8905
|
+
AggRet::F64 => {
|
|
8906
|
+
if as_value {
|
|
8907
|
+
(format!("Value::Number({})", call), RustType::Value)
|
|
8908
|
+
} else {
|
|
8909
|
+
(call, RustType::F64)
|
|
8910
|
+
}
|
|
8911
|
+
}
|
|
8912
|
+
AggRet::ArrayOfStruct => {
|
|
8913
|
+
let alias = self.aggregate_alias.clone().unwrap();
|
|
8914
|
+
(
|
|
8915
|
+
call,
|
|
8916
|
+
RustType::Vec(Box::new(RustType::Named {
|
|
8917
|
+
name: alias.as_str().into(),
|
|
8918
|
+
fields: Vec::new(),
|
|
8919
|
+
})),
|
|
8920
|
+
)
|
|
8921
|
+
}
|
|
8922
|
+
AggRet::Struct => {
|
|
8923
|
+
let alias = self.aggregate_alias.clone().unwrap();
|
|
8924
|
+
(
|
|
8925
|
+
call,
|
|
8926
|
+
RustType::Named {
|
|
8927
|
+
name: alias.as_str().into(),
|
|
8928
|
+
fields: Vec::new(),
|
|
8929
|
+
},
|
|
8930
|
+
)
|
|
8931
|
+
}
|
|
8932
|
+
// void call: `()`; valid only in statement position (ExprStmt).
|
|
8933
|
+
AggRet::Unit => (call, RustType::Unit),
|
|
8934
|
+
};
|
|
8935
|
+
Ok(Some((code, ty)))
|
|
8936
|
+
}
|
|
8937
|
+
|
|
8938
|
+
/// Detect the unboxed struct alias: the unique type-alias name `A` that is (a) used as an
|
|
8939
|
+
/// `A[]` array param of some top-level fn and (b) registered as a struct whose fields are all
|
|
8940
|
+
/// `Copy` (numeric/bool) — so element field reads/writes by index are sound. Returns `None`
|
|
8941
|
+
/// if there is no such alias or more than one (ambiguous → bail to boxed).
|
|
8942
|
+
fn detect_aggregate_alias(&self, program: &Program) -> Option<String> {
|
|
8943
|
+
let mut found: Option<String> = None;
|
|
8944
|
+
for s in &program.statements {
|
|
8945
|
+
if let Statement::FunDecl { params, .. } = s {
|
|
8946
|
+
for p in params {
|
|
8947
|
+
if let FunParam::Simple(tp) = p {
|
|
8948
|
+
if let Some(TypeAnnotation::Array(inner)) = tp.type_ann.as_ref() {
|
|
8949
|
+
if let TypeAnnotation::Simple(a, _) = inner.as_ref() {
|
|
8950
|
+
let a = a.to_string();
|
|
8951
|
+
if !self.alias_is_copy_struct(&a) {
|
|
8952
|
+
continue;
|
|
8953
|
+
}
|
|
8954
|
+
match &found {
|
|
8955
|
+
Some(prev) if prev != &a => return None, // ambiguous
|
|
8956
|
+
_ => found = Some(a),
|
|
8957
|
+
}
|
|
8958
|
+
}
|
|
8959
|
+
}
|
|
8960
|
+
}
|
|
8961
|
+
}
|
|
8962
|
+
}
|
|
8963
|
+
}
|
|
8964
|
+
found
|
|
8965
|
+
}
|
|
8966
|
+
|
|
8967
|
+
/// Is `name` a registered struct alias whose every field is a `Copy` scalar (f64/bool/i32)?
|
|
8968
|
+
fn alias_is_copy_struct(&self, name: &str) -> bool {
|
|
8969
|
+
match self.type_aliases.get(name) {
|
|
8970
|
+
Some(RustType::Named { fields, .. }) | Some(RustType::Object(fields)) => {
|
|
8971
|
+
!fields.is_empty()
|
|
8972
|
+
&& fields.iter().all(|(_, t)| {
|
|
8973
|
+
matches!(t, RustType::F64 | RustType::Bool | RustType::I32)
|
|
8974
|
+
})
|
|
8975
|
+
}
|
|
8976
|
+
_ => false,
|
|
8977
|
+
}
|
|
8978
|
+
}
|
|
8979
|
+
|
|
8980
|
+
/// Is `ann` exactly `Simple(alias)`?
|
|
8981
|
+
fn ann_is_simple(ann: Option<&TypeAnnotation>, alias: &str) -> bool {
|
|
8982
|
+
matches!(ann, Some(TypeAnnotation::Simple(a, _)) if a.as_ref() == alias)
|
|
8983
|
+
}
|
|
8984
|
+
|
|
8985
|
+
/// Is `ann` exactly `Array(Simple(alias))` (i.e. `alias[]`)?
|
|
8986
|
+
fn ann_is_array_of(ann: Option<&TypeAnnotation>, alias: &str) -> bool {
|
|
8987
|
+
matches!(ann, Some(TypeAnnotation::Array(inner))
|
|
8988
|
+
if matches!(inner.as_ref(), TypeAnnotation::Simple(a, _) if a.as_ref() == alias))
|
|
8989
|
+
}
|
|
8990
|
+
|
|
8991
|
+
/// Math methods whose JS semantics match the Rust `f64` method 1:1 (no rounding/sign quirks).
|
|
8992
|
+
fn agg_clean_math_method(m: &str) -> Option<&'static str> {
|
|
8993
|
+
Some(match m {
|
|
8994
|
+
"sqrt" => "sqrt",
|
|
8995
|
+
"sin" => "sin",
|
|
8996
|
+
"cos" => "cos",
|
|
8997
|
+
"tan" => "tan",
|
|
8998
|
+
"exp" => "exp",
|
|
8999
|
+
"log" => "ln",
|
|
9000
|
+
"sinh" => "sinh",
|
|
9001
|
+
"cosh" => "cosh",
|
|
9002
|
+
"tanh" => "tanh",
|
|
9003
|
+
"asin" => "asin",
|
|
9004
|
+
"acos" => "acos",
|
|
9005
|
+
"atan" => "atan",
|
|
9006
|
+
"asinh" => "asinh",
|
|
9007
|
+
"acosh" => "acosh",
|
|
9008
|
+
"atanh" => "atanh",
|
|
9009
|
+
"cbrt" => "cbrt",
|
|
9010
|
+
"log2" => "log2",
|
|
9011
|
+
"log10" => "log10",
|
|
9012
|
+
_ => return None,
|
|
9013
|
+
})
|
|
9014
|
+
}
|
|
9015
|
+
|
|
9016
|
+
/// Does `body` contain a write through array param `p` (element field write / index write)?
|
|
9017
|
+
fn agg_fn_mutates_array(body: &Statement, p: &str) -> bool {
|
|
9018
|
+
let mut aliases: std::collections::HashSet<String> = std::collections::HashSet::new();
|
|
9019
|
+
Self::agg_collect_aliases(body, p, &mut aliases);
|
|
9020
|
+
Self::agg_stmt_writes(body, p, &aliases)
|
|
9021
|
+
}
|
|
9022
|
+
|
|
9023
|
+
fn agg_collect_aliases(s: &Statement, p: &str, out: &mut std::collections::HashSet<String>) {
|
|
9024
|
+
match s {
|
|
9025
|
+
Statement::VarDecl {
|
|
9026
|
+
name,
|
|
9027
|
+
init: Some(Expr::Index { object, index, .. }),
|
|
9028
|
+
..
|
|
9029
|
+
} => {
|
|
9030
|
+
if matches!(object.as_ref(), Expr::Ident { name: o, .. } if o.as_ref() == p)
|
|
9031
|
+
&& matches!(index.as_ref(), Expr::Ident { .. })
|
|
9032
|
+
{
|
|
9033
|
+
out.insert(name.to_string());
|
|
9034
|
+
}
|
|
9035
|
+
}
|
|
9036
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
9037
|
+
statements.iter().for_each(|x| Self::agg_collect_aliases(x, p, out));
|
|
9038
|
+
}
|
|
9039
|
+
Statement::If {
|
|
9040
|
+
then_branch,
|
|
9041
|
+
else_branch,
|
|
9042
|
+
..
|
|
9043
|
+
} => {
|
|
9044
|
+
Self::agg_collect_aliases(then_branch, p, out);
|
|
9045
|
+
if let Some(e) = else_branch {
|
|
9046
|
+
Self::agg_collect_aliases(e, p, out);
|
|
9047
|
+
}
|
|
9048
|
+
}
|
|
9049
|
+
Statement::For { init, body, .. } => {
|
|
9050
|
+
if let Some(i) = init {
|
|
9051
|
+
Self::agg_collect_aliases(i, p, out);
|
|
9052
|
+
}
|
|
9053
|
+
Self::agg_collect_aliases(body, p, out);
|
|
9054
|
+
}
|
|
9055
|
+
Statement::While { body, .. }
|
|
9056
|
+
| Statement::DoWhile { body, .. }
|
|
9057
|
+
| Statement::ForOf { body, .. } => Self::agg_collect_aliases(body, p, out),
|
|
9058
|
+
_ => {}
|
|
9059
|
+
}
|
|
9060
|
+
}
|
|
9061
|
+
|
|
9062
|
+
fn agg_stmt_writes(
|
|
9063
|
+
s: &Statement,
|
|
9064
|
+
p: &str,
|
|
9065
|
+
aliases: &std::collections::HashSet<String>,
|
|
9066
|
+
) -> bool {
|
|
9067
|
+
match s {
|
|
9068
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
9069
|
+
statements.iter().any(|x| Self::agg_stmt_writes(x, p, aliases))
|
|
9070
|
+
}
|
|
9071
|
+
Statement::ExprStmt { expr, .. } => Self::agg_expr_writes(expr, p, aliases),
|
|
9072
|
+
Statement::If {
|
|
9073
|
+
then_branch,
|
|
9074
|
+
else_branch,
|
|
9075
|
+
..
|
|
9076
|
+
} => {
|
|
9077
|
+
Self::agg_stmt_writes(then_branch, p, aliases)
|
|
9078
|
+
|| else_branch
|
|
9079
|
+
.as_ref()
|
|
9080
|
+
.is_some_and(|e| Self::agg_stmt_writes(e, p, aliases))
|
|
9081
|
+
}
|
|
9082
|
+
Statement::For { body, .. }
|
|
9083
|
+
| Statement::While { body, .. }
|
|
9084
|
+
| Statement::DoWhile { body, .. }
|
|
9085
|
+
| Statement::ForOf { body, .. } => Self::agg_stmt_writes(body, p, aliases),
|
|
9086
|
+
_ => false,
|
|
9087
|
+
}
|
|
9088
|
+
}
|
|
9089
|
+
|
|
9090
|
+
fn agg_expr_writes(
|
|
9091
|
+
e: &Expr,
|
|
9092
|
+
p: &str,
|
|
9093
|
+
aliases: &std::collections::HashSet<String>,
|
|
9094
|
+
) -> bool {
|
|
9095
|
+
match e {
|
|
9096
|
+
Expr::MemberAssign { object, .. } => match object.as_ref() {
|
|
9097
|
+
Expr::Ident { name, .. } => aliases.contains(name.as_ref()),
|
|
9098
|
+
Expr::Index { object: io, .. } => {
|
|
9099
|
+
matches!(io.as_ref(), Expr::Ident { name, .. } if name.as_ref() == p)
|
|
9100
|
+
}
|
|
9101
|
+
_ => false,
|
|
9102
|
+
},
|
|
9103
|
+
Expr::IndexAssign { object, .. } => {
|
|
9104
|
+
matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == p)
|
|
9105
|
+
}
|
|
9106
|
+
_ => false,
|
|
9107
|
+
}
|
|
9108
|
+
}
|
|
9109
|
+
|
|
9110
|
+
/// Top-level `let` names whose initializer is a numeric constant — the only globals safe to
|
|
9111
|
+
/// thread into an aggregate fn as a trailing `f64` param. A `let bodies = makeBodies()` or
|
|
9112
|
+
/// `let t0 = Date.now()` is excluded so it can never be mistyped as `f64`.
|
|
9113
|
+
fn collect_toplevel_global_lets(program: &Program) -> std::collections::HashSet<String> {
|
|
9114
|
+
let mut out: std::collections::HashSet<String> = std::collections::HashSet::new();
|
|
9115
|
+
for s in &program.statements {
|
|
9116
|
+
if let Statement::VarDecl {
|
|
9117
|
+
name,
|
|
9118
|
+
init: Some(e),
|
|
9119
|
+
..
|
|
9120
|
+
} = s
|
|
9121
|
+
{
|
|
9122
|
+
if Self::expr_is_numeric_const(e, &out) {
|
|
9123
|
+
out.insert(name.to_string());
|
|
9124
|
+
}
|
|
9125
|
+
}
|
|
9126
|
+
}
|
|
9127
|
+
out
|
|
9128
|
+
}
|
|
9129
|
+
|
|
9130
|
+
/// Conservatively: a numeric literal, an arithmetic combination of such, or a reference to an
|
|
9131
|
+
/// already-proven numeric global (`numeric` carries the names accepted so far, in source order).
|
|
9132
|
+
fn expr_is_numeric_const(e: &Expr, numeric: &std::collections::HashSet<String>) -> bool {
|
|
9133
|
+
match e {
|
|
9134
|
+
Expr::Literal {
|
|
9135
|
+
value: Literal::Number(_),
|
|
9136
|
+
..
|
|
9137
|
+
} => true,
|
|
9138
|
+
Expr::Ident { name, .. } => numeric.contains(name.as_ref()),
|
|
9139
|
+
Expr::Unary {
|
|
9140
|
+
op: UnaryOp::Neg | UnaryOp::Pos,
|
|
9141
|
+
operand,
|
|
9142
|
+
..
|
|
9143
|
+
} => Self::expr_is_numeric_const(operand, numeric),
|
|
9144
|
+
Expr::Binary {
|
|
9145
|
+
left, op, right, ..
|
|
9146
|
+
} => {
|
|
9147
|
+
matches!(
|
|
9148
|
+
op,
|
|
9149
|
+
BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow
|
|
9150
|
+
) && Self::expr_is_numeric_const(left, numeric)
|
|
9151
|
+
&& Self::expr_is_numeric_const(right, numeric)
|
|
9152
|
+
}
|
|
9153
|
+
_ => false,
|
|
9154
|
+
}
|
|
9155
|
+
}
|
|
9156
|
+
|
|
9157
|
+
/// Captured globals a fn body references: free idents that are top-level globals, excluding
|
|
9158
|
+
/// the fn's own params, its locals, the other group-fn names, and the struct alias.
|
|
9159
|
+
fn agg_captured_globals(
|
|
9160
|
+
body: &Statement,
|
|
9161
|
+
params: &[FunParam],
|
|
9162
|
+
globals: &std::collections::HashSet<String>,
|
|
9163
|
+
group_fns: &std::collections::HashMap<String, AggFnSig>,
|
|
9164
|
+
self_name: &str,
|
|
9165
|
+
alias: &str,
|
|
9166
|
+
) -> Vec<String> {
|
|
9167
|
+
let mut idents: std::collections::HashSet<String> = std::collections::HashSet::new();
|
|
9168
|
+
Self::collect_stmt_idents(body, &mut idents);
|
|
9169
|
+
let mut locals: std::collections::HashSet<String> = std::collections::HashSet::new();
|
|
9170
|
+
Self::collect_local_var_names(body, &mut locals);
|
|
9171
|
+
let pnames: std::collections::HashSet<String> = params
|
|
9172
|
+
.iter()
|
|
9173
|
+
.flat_map(|p| p.bound_names())
|
|
9174
|
+
.map(|n| n.to_string())
|
|
9175
|
+
.collect();
|
|
9176
|
+
let mut out: Vec<String> = idents
|
|
9177
|
+
.into_iter()
|
|
9178
|
+
.filter(|id| {
|
|
9179
|
+
globals.contains(id)
|
|
9180
|
+
&& !pnames.contains(id)
|
|
9181
|
+
&& !locals.contains(id)
|
|
9182
|
+
&& !group_fns.contains_key(id)
|
|
9183
|
+
&& id != self_name
|
|
9184
|
+
&& id != alias
|
|
9185
|
+
})
|
|
9186
|
+
.collect();
|
|
9187
|
+
out.sort();
|
|
9188
|
+
out
|
|
9189
|
+
}
|
|
9190
|
+
|
|
9191
|
+
/// Does `s` contain a `return <value>` (vs only bare `return;` / no return)?
|
|
9192
|
+
fn stmt_returns_value(s: &Statement) -> bool {
|
|
9193
|
+
match s {
|
|
9194
|
+
Statement::Return { value, .. } => value.is_some(),
|
|
9195
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
9196
|
+
statements.iter().any(Self::stmt_returns_value)
|
|
9197
|
+
}
|
|
9198
|
+
Statement::If {
|
|
9199
|
+
then_branch,
|
|
9200
|
+
else_branch,
|
|
9201
|
+
..
|
|
9202
|
+
} => {
|
|
9203
|
+
Self::stmt_returns_value(then_branch)
|
|
9204
|
+
|| else_branch.as_ref().is_some_and(|e| Self::stmt_returns_value(e))
|
|
9205
|
+
}
|
|
9206
|
+
Statement::For { body, .. }
|
|
9207
|
+
| Statement::While { body, .. }
|
|
9208
|
+
| Statement::DoWhile { body, .. }
|
|
9209
|
+
| Statement::ForOf { body, .. } => Self::stmt_returns_value(body),
|
|
9210
|
+
_ => false,
|
|
9211
|
+
}
|
|
9212
|
+
}
|
|
9213
|
+
|
|
9214
|
+
/// Lower an expression that JS will coerce to **int32** inside a bitwise/shift computation,
|
|
9215
|
+
/// staying in the integer domain instead of round-tripping every intermediate through `f64`.
|
|
9216
|
+
///
|
|
9217
|
+
/// Returns `Ok(Some(code))` where `code` is an `i32`-typed Rust expression equal to
|
|
9218
|
+
/// `ToInt32(e)`, or `Ok(None)` if a leaf can't be proven `F64` (then the caller keeps the
|
|
9219
|
+
/// existing per-op lowering — purely additive, never a regression).
|
|
9220
|
+
///
|
|
9221
|
+
/// This is behaviour-identical to the nested `to_int32`/`to_uint32` lowering: an intermediate
|
|
9222
|
+
/// `(i32 as f64)` immediately re-narrowed by `to_int32` is exact (every `i32` is representable
|
|
9223
|
+
/// in `f64`, and `to_int32` of a finite value recovers it), so erasing it changes nothing but
|
|
9224
|
+
/// the round-trips. Crucially, only bitwise/shift nodes recurse — an `f64` `*`/`+`/`-` node is
|
|
9225
|
+
/// a *leaf* here, so e.g. `(h * 16777619) >>> 0` keeps its `f64` multiply (the 2^53 rule: the
|
|
9226
|
+
/// product exceeds 2^53 and must round in `f64` *before* `ToUint32`, exactly as V8 does).
|
|
9227
|
+
fn emit_int32_operand(&mut self, e: &Expr) -> Result<Option<String>, CompileError> {
|
|
9228
|
+
if let Expr::Binary {
|
|
9229
|
+
left, op, right, ..
|
|
9230
|
+
} = e
|
|
9231
|
+
{
|
|
9232
|
+
let bitwise = matches!(
|
|
9233
|
+
op,
|
|
9234
|
+
BinOp::BitAnd
|
|
9235
|
+
| BinOp::BitOr
|
|
9236
|
+
| BinOp::BitXor
|
|
9237
|
+
| BinOp::Shl
|
|
9238
|
+
| BinOp::Shr
|
|
9239
|
+
| BinOp::UShr
|
|
9240
|
+
);
|
|
9241
|
+
if bitwise {
|
|
9242
|
+
let li = match self.emit_int32_operand(left)? {
|
|
9243
|
+
Some(c) => c,
|
|
9244
|
+
None => return Ok(None),
|
|
9245
|
+
};
|
|
9246
|
+
let ri = match self.emit_int32_operand(right)? {
|
|
9247
|
+
Some(c) => c,
|
|
9248
|
+
None => return Ok(None),
|
|
9249
|
+
};
|
|
9250
|
+
// Shift counts: `(ri as u32)` shares its low 5 bits with `to_uint32(rhs)`, and
|
|
9251
|
+
// `wrapping_sh*` masks the count mod 32 — exactly JS's `count & 31`.
|
|
9252
|
+
let code = match op {
|
|
9253
|
+
BinOp::BitAnd => format!("({} & {})", li, ri),
|
|
9254
|
+
BinOp::BitOr => format!("({} | {})", li, ri),
|
|
9255
|
+
BinOp::BitXor => format!("({} ^ {})", li, ri),
|
|
9256
|
+
BinOp::Shl => format!("({}).wrapping_shl(({}) as u32)", li, ri),
|
|
9257
|
+
BinOp::Shr => format!("({}).wrapping_shr(({}) as u32)", li, ri),
|
|
9258
|
+
// `>>>` is a logical shift on the uint32 view; reinterpret back to `i32` to
|
|
9259
|
+
// stay in the integer domain (the unsigned value is recovered at the f64 edge).
|
|
9260
|
+
BinOp::UShr => {
|
|
9261
|
+
format!("((({}) as u32).wrapping_shr(({}) as u32) as i32)", li, ri)
|
|
9262
|
+
}
|
|
9263
|
+
_ => unreachable!(),
|
|
9264
|
+
};
|
|
9265
|
+
return Ok(Some(code));
|
|
9266
|
+
}
|
|
9267
|
+
}
|
|
9268
|
+
// Leaf: fold only when it is a plain `f64` (so `to_int32` applies directly). `to_int32`
|
|
9269
|
+
// keeps its `is_finite` guard here — a leaf may legitimately be NaN/±Infinity (→ 0).
|
|
9270
|
+
let (code, ty) = self.emit_typed_expr(e)?;
|
|
9271
|
+
if ty == RustType::F64 {
|
|
9272
|
+
// When the leaf is an ARITHMETIC node PROVABLY finite with `|x| < 2^62` (operands are
|
|
9273
|
+
// i32-register reads and finite literals — e.g. the FNV `h * 16777619` excursion), drop
|
|
9274
|
+
// the `is_finite` guard and Rust's saturating cast and truncate directly. Bit-identical
|
|
9275
|
+
// on this domain (`x as i64` truncates toward zero = JS ToInt32 truncation; `as i32` =
|
|
9276
|
+
// modulo 2^32), a few instructions cheaper per iteration. Emitted inline so the generated
|
|
9277
|
+
// crate needs no new runtime symbol. Any unproven leaf keeps the guarded `to_int32`.
|
|
9278
|
+
if matches!(e, Expr::Binary { .. }) && self.f64_finite_bounded_below_2pow62(e) {
|
|
9279
|
+
Ok(Some(format!(
|
|
9280
|
+
"(unsafe {{ ({}).to_int_unchecked::<i64>() }} as i32)",
|
|
9281
|
+
code
|
|
9282
|
+
)))
|
|
9283
|
+
} else {
|
|
9284
|
+
Ok(Some(format!("tishlang_runtime::to_int32({})", code)))
|
|
9285
|
+
}
|
|
9286
|
+
} else if ty == RustType::I32 {
|
|
9287
|
+
// An `I32` loop-accumulator already holds its JS ToInt32 bit-pattern in an integer
|
|
9288
|
+
// register — feed it straight in, NO `to_int32` round-trip. This is the perf win: the
|
|
9289
|
+
// per-op `f64`→i32 narrowing across the hash loop collapses to a register read.
|
|
9290
|
+
Ok(Some(code))
|
|
9291
|
+
} else {
|
|
9292
|
+
Ok(None)
|
|
9293
|
+
}
|
|
9294
|
+
}
|
|
9295
|
+
|
|
9296
|
+
fn emit_typed_expr(&mut self, expr: &Expr) -> Result<(String, RustType), CompileError> {
|
|
9297
|
+
match expr {
|
|
9298
|
+
// ── literals ─────────────────────────────────────────────────────────
|
|
9299
|
+
Expr::Literal { value, .. } => match value {
|
|
9300
|
+
Literal::Number(n) => Ok((Self::f64_lit(*n), RustType::F64)),
|
|
9301
|
+
Literal::String(s) => {
|
|
9302
|
+
Ok((format!("{:?}.to_string()", s.as_ref()), RustType::String))
|
|
9303
|
+
}
|
|
9304
|
+
Literal::Bool(b) => Ok((format!("{}", b), RustType::Bool)),
|
|
9305
|
+
Literal::Null => Ok(("Value::Null".to_string(), RustType::Value)),
|
|
9306
|
+
},
|
|
9307
|
+
|
|
9308
|
+
// ── identifiers ──────────────────────────────────────────────────────
|
|
9309
|
+
Expr::Ident { name, .. } => {
|
|
9310
|
+
let escaped = Self::escape_ident(name.as_ref());
|
|
9311
|
+
if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
9312
|
+
let var_type = self.type_context.get_type(name.as_ref());
|
|
9313
|
+
if var_type.is_native() {
|
|
9314
|
+
Ok((format!("(*{}.borrow()).clone()", escaped), var_type))
|
|
9315
|
+
} else {
|
|
9316
|
+
Ok((format!("(*{}.borrow()).clone()", escaped), RustType::Value))
|
|
9317
|
+
}
|
|
9318
|
+
} else {
|
|
9319
|
+
let var_type = self.type_context.get_type(name.as_ref());
|
|
9320
|
+
if var_type.is_native() {
|
|
9321
|
+
Ok((escaped.into_owned(), var_type))
|
|
9322
|
+
} else {
|
|
9323
|
+
Ok((escaped.into_owned(), RustType::Value))
|
|
9324
|
+
}
|
|
9325
|
+
}
|
|
9326
|
+
}
|
|
9327
|
+
|
|
9328
|
+
// ── binary expressions ───────────────────────────────────────────────
|
|
7887
9329
|
Expr::Binary {
|
|
7888
9330
|
left,
|
|
7889
9331
|
op,
|
|
@@ -8138,6 +9580,13 @@ impl Codegen {
|
|
|
8138
9580
|
// skipping the boxed value_call per element. Only methods whose Rust f64 op
|
|
8139
9581
|
// matches JS semantics (round half-up & sign(0) differ → left to the runtime).
|
|
8140
9582
|
Expr::Call { callee, args, .. } => {
|
|
9583
|
+
// #177: a de-virtualized aggregate fn used in native arithmetic (e.g. `energy(bodies)`
|
|
9584
|
+
// feeding an f64 expression) → call `name_agg(..)` returning the native type.
|
|
9585
|
+
if !self.aggregate_fns.is_empty() {
|
|
9586
|
+
if let Some((code, ty)) = self.try_emit_toplevel_agg_call(callee, args, false)? {
|
|
9587
|
+
return Ok((code, ty));
|
|
9588
|
+
}
|
|
9589
|
+
}
|
|
8141
9590
|
// M5: direct call to an eligible native fn -> `name_native(<native args>)`.
|
|
8142
9591
|
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
8143
9592
|
if self.native_fns.contains(fname.as_ref()) {
|
|
@@ -8237,6 +9686,19 @@ impl Codegen {
|
|
|
8237
9686
|
} => {
|
|
8238
9687
|
if let Expr::Ident { name: var_name, .. } = object.as_ref() {
|
|
8239
9688
|
let var_type = self.type_context.get_type(var_name.as_ref());
|
|
9689
|
+
// #173: `vec.length` on a native `Vec<_>` → `(vec.len() as f64)`, so the length
|
|
9690
|
+
// (and arithmetic derived from it) stays native instead of a boxed `get_prop`.
|
|
9691
|
+
if let RustType::Vec(_) = &var_type {
|
|
9692
|
+
if prop_name.as_ref() == "length" {
|
|
9693
|
+
let var_esc = Self::escape_ident(var_name.as_ref()).into_owned();
|
|
9694
|
+
let code = if self.refcell_wrapped_vars.contains(var_name.as_ref()) {
|
|
9695
|
+
format!("({}.borrow().len() as f64)", var_esc)
|
|
9696
|
+
} else {
|
|
9697
|
+
format!("({}.len() as f64)", var_esc)
|
|
9698
|
+
};
|
|
9699
|
+
return Ok((code, RustType::F64));
|
|
9700
|
+
}
|
|
9701
|
+
}
|
|
8240
9702
|
if let RustType::Named { fields, .. } = &var_type {
|
|
8241
9703
|
if let Some((_, field_ty)) =
|
|
8242
9704
|
fields.iter().find(|(k, _)| k.as_ref() == prop_name.as_ref())
|