@tishlang/tish 2.8.0 → 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/tish/Cargo.toml +2 -1
- package/crates/tish/src/cli_help.rs +3 -0
- package/crates/tish/src/main.rs +70 -1
- package/crates/tish/tests/integration_test.rs +65 -0
- package/crates/tish_compile/src/codegen.rs +1208 -1
- package/crates/tish_compile/src/infer.rs +1946 -52
- package/crates/tish_compile_js/Cargo.toml +3 -0
- package/crates/tish_compile_js/src/codegen.rs +310 -5
- package/crates/tish_compile_js/src/lib.rs +2 -1
- package/crates/tish_compile_js/src/tests_jsx.rs +185 -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);",
|
|
@@ -2135,7 +2200,11 @@ impl Codegen {
|
|
|
2135
2200
|
// Track the variable type
|
|
2136
2201
|
self.type_context.define(name.as_ref(), rust_type.clone());
|
|
2137
2202
|
|
|
2138
|
-
|
|
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" };
|
|
2139
2208
|
let escaped_name = Self::escape_ident(name.as_ref());
|
|
2140
2209
|
|
|
2141
2210
|
if rust_type.is_native() {
|
|
@@ -2605,6 +2674,15 @@ impl Codegen {
|
|
|
2605
2674
|
span,
|
|
2606
2675
|
..
|
|
2607
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
|
+
}
|
|
2608
2686
|
// Use Rc<RefCell<>> pattern to allow recursive function calls
|
|
2609
2687
|
// The function can reference itself through the cell
|
|
2610
2688
|
let name_raw = name.as_ref();
|
|
@@ -3321,6 +3399,14 @@ impl Codegen {
|
|
|
3321
3399
|
}
|
|
3322
3400
|
}
|
|
3323
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
|
+
}
|
|
3324
3410
|
// Typed-struct shortcut for `JSON.stringify(typedValue)`.
|
|
3325
3411
|
// When the single arg has a known native type that owns a
|
|
3326
3412
|
// hand-rolled `_tish_write_json` (struct or `Vec<struct>`),
|
|
@@ -5897,6 +5983,16 @@ impl Codegen {
|
|
|
5897
5983
|
expr: &Expr,
|
|
5898
5984
|
target_type: &RustType,
|
|
5899
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
|
+
|
|
5900
5996
|
// Try to emit literals directly as native types
|
|
5901
5997
|
if let Expr::Literal { value, .. } = expr {
|
|
5902
5998
|
match (target_type, value) {
|
|
@@ -7769,6 +7865,1110 @@ impl Codegen {
|
|
|
7769
7865
|
Ok(())
|
|
7770
7866
|
}
|
|
7771
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
|
+
..
|
|
8334
|
+
} => {
|
|
8335
|
+
let (o, _) = self.emit_agg_expr(operand, arr, aliases)?;
|
|
8336
|
+
Ok((format!("(-({}))", o), RustType::F64))
|
|
8337
|
+
}
|
|
8338
|
+
Expr::Unary {
|
|
8339
|
+
op: UnaryOp::Pos,
|
|
8340
|
+
operand,
|
|
8341
|
+
..
|
|
8342
|
+
} => {
|
|
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))
|
|
8364
|
+
}
|
|
8365
|
+
};
|
|
8366
|
+
Ok((code, ty))
|
|
8367
|
+
}
|
|
8368
|
+
Expr::Member {
|
|
8369
|
+
object,
|
|
8370
|
+
prop: MemberProp::Name { name: m, .. },
|
|
8371
|
+
optional: false,
|
|
8372
|
+
..
|
|
8373
|
+
} => {
|
|
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())
|
|
8401
|
+
{
|
|
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
|
+
));
|
|
8410
|
+
}
|
|
8411
|
+
}
|
|
8412
|
+
}
|
|
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))
|
|
8431
|
+
}
|
|
8432
|
+
Expr::Call { callee, args, .. } => {
|
|
8433
|
+
// Nested call to another group fn (e.g. `body(...)` from `makeBodies`).
|
|
8434
|
+
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
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));
|
|
8455
|
+
}
|
|
8456
|
+
}
|
|
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")
|
|
8465
|
+
{
|
|
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
|
+
}
|
|
8471
|
+
}
|
|
8472
|
+
}
|
|
8473
|
+
}
|
|
8474
|
+
Err(CompileError::new("agg: unsupported call", None))
|
|
8475
|
+
}
|
|
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)),
|
|
8488
|
+
}
|
|
8489
|
+
}
|
|
8490
|
+
|
|
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))
|
|
8516
|
+
}
|
|
8517
|
+
}
|
|
8518
|
+
}
|
|
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());
|
|
8545
|
+
}
|
|
8546
|
+
_ => {
|
|
8547
|
+
return Err(CompileError::new(
|
|
8548
|
+
"agg: array element not a struct ident",
|
|
8549
|
+
None,
|
|
8550
|
+
))
|
|
8551
|
+
}
|
|
8552
|
+
}
|
|
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
|
+
}
|
|
8592
|
+
}
|
|
8593
|
+
}
|
|
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
|
+
))
|
|
8602
|
+
}
|
|
8603
|
+
|
|
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
|
+
}
|
|
8645
|
+
}
|
|
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
|
+
}
|
|
8669
|
+
}
|
|
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
|
+
})
|
|
8733
|
+
}
|
|
8734
|
+
_ => false,
|
|
8735
|
+
}
|
|
8736
|
+
}
|
|
8737
|
+
|
|
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
|
+
}
|
|
8793
|
+
}
|
|
8794
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
8795
|
+
statements.iter().for_each(|x| Self::agg_collect_aliases(x, p, out));
|
|
8796
|
+
}
|
|
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);
|
|
8805
|
+
}
|
|
8806
|
+
}
|
|
8807
|
+
Statement::For { init, body, .. } => {
|
|
8808
|
+
if let Some(i) = init {
|
|
8809
|
+
Self::agg_collect_aliases(i, p, out);
|
|
8810
|
+
}
|
|
8811
|
+
Self::agg_collect_aliases(body, p, out);
|
|
8812
|
+
}
|
|
8813
|
+
Statement::While { body, .. }
|
|
8814
|
+
| Statement::DoWhile { body, .. }
|
|
8815
|
+
| Statement::ForOf { body, .. } => Self::agg_collect_aliases(body, p, out),
|
|
8816
|
+
_ => {}
|
|
8817
|
+
}
|
|
8818
|
+
}
|
|
8819
|
+
|
|
8820
|
+
fn agg_stmt_writes(
|
|
8821
|
+
s: &Statement,
|
|
8822
|
+
p: &str,
|
|
8823
|
+
aliases: &std::collections::HashSet<String>,
|
|
8824
|
+
) -> bool {
|
|
8825
|
+
match s {
|
|
8826
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
8827
|
+
statements.iter().any(|x| Self::agg_stmt_writes(x, p, aliases))
|
|
8828
|
+
}
|
|
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))
|
|
8839
|
+
}
|
|
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,
|
|
8845
|
+
}
|
|
8846
|
+
}
|
|
8847
|
+
|
|
8848
|
+
fn agg_expr_writes(
|
|
8849
|
+
e: &Expr,
|
|
8850
|
+
p: &str,
|
|
8851
|
+
aliases: &std::collections::HashSet<String>,
|
|
8852
|
+
) -> bool {
|
|
8853
|
+
match e {
|
|
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)
|
|
8858
|
+
}
|
|
8859
|
+
_ => false,
|
|
8860
|
+
},
|
|
8861
|
+
Expr::IndexAssign { object, .. } => {
|
|
8862
|
+
matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == p)
|
|
8863
|
+
}
|
|
8864
|
+
_ => false,
|
|
8865
|
+
}
|
|
8866
|
+
}
|
|
8867
|
+
|
|
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());
|
|
8882
|
+
}
|
|
8883
|
+
}
|
|
8884
|
+
}
|
|
8885
|
+
out
|
|
8886
|
+
}
|
|
8887
|
+
|
|
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(),
|
|
8953
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
8954
|
+
statements.iter().any(Self::stmt_returns_value)
|
|
8955
|
+
}
|
|
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))
|
|
8963
|
+
}
|
|
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
|
+
|
|
7772
8972
|
/// Lower an expression that JS will coerce to **int32** inside a bitwise/shift computation,
|
|
7773
8973
|
/// staying in the integer domain instead of round-tripping every intermediate through `f64`.
|
|
7774
8974
|
///
|
|
@@ -8138,6 +9338,13 @@ impl Codegen {
|
|
|
8138
9338
|
// skipping the boxed value_call per element. Only methods whose Rust f64 op
|
|
8139
9339
|
// matches JS semantics (round half-up & sign(0) differ → left to the runtime).
|
|
8140
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
|
+
}
|
|
8141
9348
|
// M5: direct call to an eligible native fn -> `name_native(<native args>)`.
|
|
8142
9349
|
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
8143
9350
|
if self.native_fns.contains(fname.as_ref()) {
|