@tishlang/tish-format 3.3.0 → 3.6.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-format +0 -0
- package/crates/tish/tests/scope_merge.rs +90 -0
- package/crates/tish_compile/src/check.rs +178 -0
- package/crates/tish_compile/src/codegen.rs +1139 -130
- package/crates/tish_compile/src/lib.rs +33 -1
- package/crates/tish_compile/src/resolve.rs +76 -7
- package/crates/tish_compile/src/types.rs +35 -1
- package/crates/tish_compile/tests/call_arity_641.rs +104 -0
- package/crates/tish_compile/tests/regr_gba_lowered_call_sites.rs +405 -1
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
|
@@ -705,6 +705,28 @@ pub fn compile_with_native_modules(
|
|
|
705
705
|
/// as warnings; `TISH_CHECK=error` also fails the build. Unset/`0` → no-op (default builds are
|
|
706
706
|
/// unaffected). The checker is gradual (see `check.rs`): it never flags code it can't prove wrong.
|
|
707
707
|
fn run_type_check(program: &Program) -> Result<(), CompileError> {
|
|
708
|
+
// #641: call arity is checked UNCONDITIONALLY, ahead of the opt-in gradual checker. It cannot
|
|
709
|
+
// sit behind `TISH_CHECK` — the case this exists for built clean and shipped, and a diagnostic
|
|
710
|
+
// nobody enables would not have caught it. It is also not "gradual": an omitted argument bound
|
|
711
|
+
// to a typed parameter arrives as `Value::Null` and cannot satisfy the typed prologue, so it is
|
|
712
|
+
// provably wrong on every backend rather than an unproven annotation.
|
|
713
|
+
//
|
|
714
|
+
// `TISH_ARITY=0` is the escape hatch, for bisecting a build against pre-#641 behaviour.
|
|
715
|
+
if std::env::var("TISH_ARITY").as_deref() != Ok("0") {
|
|
716
|
+
let arity = crate::check::check_call_arity(program);
|
|
717
|
+
if let Some(first) = arity.first() {
|
|
718
|
+
for d in &arity {
|
|
719
|
+
eprintln!(
|
|
720
|
+
"tish error: {}:{}: {}",
|
|
721
|
+
d.span.start.0, d.span.start.1, d.message
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
return Err(CompileError::new(
|
|
725
|
+
format!("call arity: {} error(s)", arity.len()),
|
|
726
|
+
Some(first.span),
|
|
727
|
+
));
|
|
728
|
+
}
|
|
729
|
+
}
|
|
708
730
|
let mode = std::env::var("TISH_CHECK").unwrap_or_default();
|
|
709
731
|
if mode.is_empty() || mode == "0" {
|
|
710
732
|
return Ok(());
|
|
@@ -1067,7 +1089,11 @@ pub(crate) struct Codegen {
|
|
|
1067
1089
|
native_ns_locals: std::collections::HashMap<String, String>,
|
|
1068
1090
|
/// Typed native imports (`declare fun` + a native import): tish name -> its typed signature.
|
|
1069
1091
|
/// A matching call emits a DIRECT `<crate>::name_typed(..)` instead of a boxed `value_call`.
|
|
1070
|
-
|
|
1092
|
+
/// #615: MULTIPLE signatures per name, dispatched on EXACT arity. An extern with optional
|
|
1093
|
+
/// trailing args (`ui_text` 4-10, `ui_rect` 5 or 6) could previously be typed for one arity
|
|
1094
|
+
/// only; every other call site stayed boxed. Additive by construction: an arity with no
|
|
1095
|
+
/// declaration finds no match and keeps the boxed path bit-identically.
|
|
1096
|
+
extern_fns: std::collections::HashMap<String, Vec<ExternSig>>,
|
|
1071
1097
|
/// User tish functions whose declared return type is a native STRUCT (`function f(): Iface`).
|
|
1072
1098
|
/// A call in a typed context coerces the boxed result to the native struct (a `Value::Struct`
|
|
1073
1099
|
/// downcast on GBA), so `f().field` reads are native instead of `get_prop`. Name -> `RustType::Named`.
|
|
@@ -1102,6 +1128,21 @@ pub(crate) struct Codegen {
|
|
|
1102
1128
|
/// Top-level `let name = [f64 literals…]` never reassigned — emitted as `const G_name: [f64; N]`
|
|
1103
1129
|
/// for direct indexing from native fns (fasta `codes`/`probs`).
|
|
1104
1130
|
module_const_f64_arrays: std::collections::HashMap<String, Vec<f64>>,
|
|
1131
|
+
/// #597: names passed DIRECTLY as a call argument (`fill(reqs)`). A typed array in this set must
|
|
1132
|
+
/// not lower to a native `Vec` — see the note at the `VarDecl` typing site.
|
|
1133
|
+
arrays_passed_to_calls: std::collections::HashSet<String>,
|
|
1134
|
+
/// The STATIC names from `module_const_f64_arrays` whose every element is an exact integer in
|
|
1135
|
+
/// `i32` range, so the hoist emits `[i32; N]` instead of `[f64; N]`.
|
|
1136
|
+
///
|
|
1137
|
+
/// ARM7TDMI HAS NO FPU. These passes were written for the host, where `f64` is tish's number and
|
|
1138
|
+
/// the representation is free; on GBA every `f64` touch is a soft-float call and every element
|
|
1139
|
+
/// costs 8 bytes instead of 4. Worse, the source usually declares the element type — hoisting
|
|
1140
|
+
/// `let A: i32[] = [1,2,3]` to `[f64; 3]` discards a type the program already stated, then pays
|
|
1141
|
+
/// an `i32 -> f64 -> usize` round trip on each index to get back to where it started.
|
|
1142
|
+
///
|
|
1143
|
+
/// Keyed by static name, not source name, so every read site (including the `_cum` aliases) can
|
|
1144
|
+
/// ask the question with what it already has in hand.
|
|
1145
|
+
module_const_int_statics: std::collections::HashSet<String>,
|
|
1105
1146
|
/// Precomputed cumulative arrays for `module_const_f64_arrays` used as `cumulative(p)` input
|
|
1106
1147
|
/// (`probs` → `G_probs_cum`).
|
|
1107
1148
|
module_const_f64_cum: std::collections::HashMap<String, String>,
|
|
@@ -1219,7 +1260,22 @@ pub(crate) struct Codegen {
|
|
|
1219
1260
|
/// once in `emit_program` (after type aliases + `native_fns`), consulted at `VarDecl` to force
|
|
1220
1261
|
/// `RustType::Value`. This is the rust-AOT analogue of the VM array-JIT bailing to the
|
|
1221
1262
|
/// interpreter on a non-numeric element. See `collect_demoted_numeric_locals`.
|
|
1222
|
-
|
|
1263
|
+
///
|
|
1264
|
+
/// #614: keyed by `(declaring scope, name)`, NOT by bare name. The scope is the top-level fn
|
|
1265
|
+
/// whose body declares the local, or `""` for a module-level binding. Flat keying meant one
|
|
1266
|
+
/// boxed `i` anywhere in the bundle demoted every `i` in every module — with 44 of tish-gba's
|
|
1267
|
+
/// 70 packages untyped, a legacy package silently de-optimized a fully-modernized one that
|
|
1268
|
+
/// merely shared a loop-variable name (~40-60 cycles per boxed use against 2-4 native).
|
|
1269
|
+
///
|
|
1270
|
+
/// Module-level bindings stay program-wide (scope `""`) ON PURPOSE: a module `let` can be
|
|
1271
|
+
/// reassigned from inside any fn, so its demotion is genuinely a whole-program property.
|
|
1272
|
+
/// Only FUNCTION-LOCAL names are scoped, and for those every reassignment necessarily lives
|
|
1273
|
+
/// in the declaring fn (or a closure nested in it) — exactly the region analyzed.
|
|
1274
|
+
demoted_numeric_locals: std::collections::HashSet<(String, String)>,
|
|
1275
|
+
/// #614: the top-level fn currently being emitted (`""` at module level), used to key lookups
|
|
1276
|
+
/// into `demoted_numeric_locals`. A stack because fn emission nests; the FIRST entry is the
|
|
1277
|
+
/// top-level fn, which is the granularity the collector analyzes.
|
|
1278
|
+
demote_scope_stack: Vec<String>,
|
|
1223
1279
|
/// Top-level fns whose EVERY return is provably numeric → the native type a call to one yields.
|
|
1224
1280
|
///
|
|
1225
1281
|
/// Without it a call is opaque, so a local assigned from one demotes to a boxed `Value` and
|
|
@@ -1383,6 +1439,8 @@ impl Codegen {
|
|
|
1383
1439
|
refcell_wrapped_vars: std::collections::HashSet::new(),
|
|
1384
1440
|
native_numeric_globals: std::collections::HashMap::new(),
|
|
1385
1441
|
module_const_f64_arrays: std::collections::HashMap::new(),
|
|
1442
|
+
module_const_int_statics: std::collections::HashSet::new(),
|
|
1443
|
+
arrays_passed_to_calls: std::collections::HashSet::new(),
|
|
1386
1444
|
module_const_f64_cum: std::collections::HashMap::new(),
|
|
1387
1445
|
module_const_f64_aliases: std::collections::HashMap::new(),
|
|
1388
1446
|
map_instance_locals: std::collections::HashSet::new(),
|
|
@@ -1417,6 +1475,7 @@ impl Codegen {
|
|
|
1417
1475
|
aggregate_array_locals: std::collections::HashSet::new(),
|
|
1418
1476
|
agg_cur_ret: None,
|
|
1419
1477
|
demoted_numeric_locals: std::collections::HashSet::new(),
|
|
1478
|
+
demote_scope_stack: Vec::new(),
|
|
1420
1479
|
user_fn_ret: std::collections::HashMap::new(),
|
|
1421
1480
|
int_range_locals: std::collections::HashMap::new(),
|
|
1422
1481
|
diag_coord_indices: std::collections::HashSet::new(),
|
|
@@ -2429,6 +2488,20 @@ impl Codegen {
|
|
|
2429
2488
|
"box", "do", "final", "macro", "override", "priv", "try", "typeof", "unsized",
|
|
2430
2489
|
"virtual", "yield",
|
|
2431
2490
|
];
|
|
2491
|
+
// #549: FOUR KEYWORDS CANNOT BE RAW IDENTIFIERS AT ALL. `r#self`, `r#Self`, `r#super` and
|
|
2492
|
+
// `r#crate` are rejected by rustc outright ("`self` cannot be a raw identifier"), so the
|
|
2493
|
+
// blanket `r#` below emitted a program that could not compile. These are ordinary names in
|
|
2494
|
+
// tish and the interpreter accepts them, which made this a backend divergence that REJECTED
|
|
2495
|
+
// VALID SOURCE rather than merely lowering it differently.
|
|
2496
|
+
//
|
|
2497
|
+
// They are renamed instead. The suffix is deliberately not a bare `_`: `self_` is a name a
|
|
2498
|
+
// program could plausibly use itself, and colliding with it would trade a compile error for
|
|
2499
|
+
// a silent capture of the wrong binding. `__tish_kw_` cannot be written in tish source,
|
|
2500
|
+
// because a leading double underscore is reserved by the compiler for generated temporaries.
|
|
2501
|
+
const NON_RAW_KEYWORDS: &[&str] = &["self", "Self", "super", "crate"];
|
|
2502
|
+
if NON_RAW_KEYWORDS.contains(&name) {
|
|
2503
|
+
return Cow::Owned(format!("__tish_kw_{}", name));
|
|
2504
|
+
}
|
|
2432
2505
|
if RUST_KEYWORDS.contains(&name) {
|
|
2433
2506
|
Cow::Owned(format!("r#{}", name))
|
|
2434
2507
|
} else {
|
|
@@ -2958,13 +3031,35 @@ impl Codegen {
|
|
|
2958
3031
|
.as_ref()
|
|
2959
3032
|
.map(RustType::from_annotation)
|
|
2960
3033
|
.unwrap_or(RustType::Unit);
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
);
|
|
3034
|
+
// #615: one name may carry several arities. A REDECLARATION of an arity already seen
|
|
3035
|
+
// replaces it (last wins), matching the previous single-map behaviour for the
|
|
3036
|
+
// single-arity case exactly.
|
|
3037
|
+
let sig = ExternSig { crate_name, symbol: export_name.clone(), params: ptys, ret };
|
|
3038
|
+
let slot = self.extern_fns.entry(name.to_string()).or_default();
|
|
3039
|
+
if let Some(existing) = slot.iter_mut().find(|e| e.params.len() == sig.params.len()) {
|
|
3040
|
+
*existing = sig;
|
|
3041
|
+
} else {
|
|
3042
|
+
slot.push(sig);
|
|
3043
|
+
}
|
|
2965
3044
|
}
|
|
2966
3045
|
}
|
|
2967
3046
|
|
|
3047
|
+
/// #615: the declared signature for `name` at EXACTLY `argc` arguments, plus the Rust symbol
|
|
3048
|
+
/// suffix to call it by. A name with a single declaration keeps the historical `<symbol>_typed`
|
|
3049
|
+
/// spelling, so every existing extension crate links unchanged; only a name that declares more
|
|
3050
|
+
/// than one arity uses `<symbol>_typed_<arity>`, which is the new surface a crate opts into by
|
|
3051
|
+
/// declaring the extra arities in the first place.
|
|
3052
|
+
fn extern_sig_for_arity(&self, name: &str, argc: usize) -> Option<(ExternSig, String)> {
|
|
3053
|
+
let sigs = self.extern_fns.get(name)?;
|
|
3054
|
+
let sig = sigs.iter().find(|s| s.params.len() == argc)?.clone();
|
|
3055
|
+
let suffix = if sigs.len() > 1 {
|
|
3056
|
+
format!("_typed_{}", argc)
|
|
3057
|
+
} else {
|
|
3058
|
+
"_typed".to_string()
|
|
3059
|
+
};
|
|
3060
|
+
Some((sig, suffix))
|
|
3061
|
+
}
|
|
3062
|
+
|
|
2968
3063
|
/// Record every top-level `function f(...): Iface` whose return annotation resolves — via the type
|
|
2969
3064
|
/// aliases — to a native STRUCT (`RustType::Named`, or a `ShapeUnion`). A later call to `f` in a
|
|
2970
3065
|
/// typed position then coerces the boxed result to the native struct (a `Value::Struct` downcast on
|
|
@@ -3181,6 +3276,37 @@ impl Codegen {
|
|
|
3181
3276
|
&program.statements,
|
|
3182
3277
|
&self.module_const_f64_cum,
|
|
3183
3278
|
);
|
|
3279
|
+
// #597: which annotated arrays escape into a call, and so must stay boxed to preserve
|
|
3280
|
+
// aliasing. Collected once over the whole program — an array declared here can be
|
|
3281
|
+
// passed to a fn declared anywhere.
|
|
3282
|
+
//
|
|
3283
|
+
// GATED ON GBA, and not only because the RESULT is GBA-only: this scan runs
|
|
3284
|
+
// `scan_param_use` over the whole program once per annotated array, so it is
|
|
3285
|
+
// O(arrays x program). Off-target that is pure waste — the set is never read — and it
|
|
3286
|
+
// would be a silent compile-time tax on every host build.
|
|
3287
|
+
if self.emit_mode == crate::NativeEmitMode::Gba {
|
|
3288
|
+
let mut array_names: Vec<String> = Vec::new();
|
|
3289
|
+
Self::collect_annotated_array_names(&program.statements, &mut array_names);
|
|
3290
|
+
self.arrays_passed_to_calls =
|
|
3291
|
+
Self::collect_escaping_array_names(&program.statements, &array_names);
|
|
3292
|
+
}
|
|
3293
|
+
// Pick the integer representation where the values allow it. GBA only: off-target the
|
|
3294
|
+
// `f64` layout is free and eligibility must stay byte-identical to before.
|
|
3295
|
+
//
|
|
3296
|
+
// A `_cum` array is deliberately NOT considered — the fasta ladder that consumes one
|
|
3297
|
+
// compares it against an `r_f64` and is specialised around f64 arms, so leaving cum
|
|
3298
|
+
// alone keeps that path untouched.
|
|
3299
|
+
if self.emit_mode == crate::NativeEmitMode::Gba {
|
|
3300
|
+
for (name, vals) in &self.module_const_f64_arrays {
|
|
3301
|
+
if vals
|
|
3302
|
+
.iter()
|
|
3303
|
+
.all(|v| v.fract() == 0.0 && *v >= i32::MIN as f64 && *v <= i32::MAX as f64)
|
|
3304
|
+
{
|
|
3305
|
+
self.module_const_int_statics
|
|
3306
|
+
.insert(Self::module_const_static(name));
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3184
3310
|
if !self.module_const_f64_arrays.is_empty() {
|
|
3185
3311
|
self.emit_module_const_f64_arrays()?;
|
|
3186
3312
|
self.writeln("");
|
|
@@ -3675,7 +3801,25 @@ impl Codegen {
|
|
|
3675
3801
|
if self.is_async {
|
|
3676
3802
|
self.async_context_stack.push(true); // run() body is async Rust context
|
|
3677
3803
|
}
|
|
3678
|
-
|
|
3804
|
+
// #629: A FUNCTION MAY REFERENCE A MODULE BINDING DECLARED BELOW IT. Module bindings are
|
|
3805
|
+
// emitted into `run()` in source order, but a function VALUE is emitted where it appears and
|
|
3806
|
+
// captures by name — so `function readA() { return A[i] }` above `let A = [...]` emitted a
|
|
3807
|
+
// capture of `A` five lines before `let A`, and rustc rejected the program:
|
|
3808
|
+
//
|
|
3809
|
+
// let rangDone = { Value::native(move |args| { … vm_read(&rangLive) … }) }; // use
|
|
3810
|
+
// let rangLive = VmRef::new(vec![(0_f64) as i32]); // decl
|
|
3811
|
+
//
|
|
3812
|
+
// That is legal tish — the function is not CALLED before the binding is initialised — and
|
|
3813
|
+
// only the ORDER differs; moving the `let` above the functions compiles. It took hyrule's
|
|
3814
|
+
// whole ROM down (`rangDone` at weapons.tish:28, `rangLive` at :46).
|
|
3815
|
+
//
|
|
3816
|
+
// Reordered here rather than at the capture site because the capture is not wrong; its
|
|
3817
|
+
// position is. Restricted to LITERAL initialisers on purpose: hoisting `let x = f()` would
|
|
3818
|
+
// move a call, changing when its side effects run and possibly reading state the statements
|
|
3819
|
+
// above it establish. A literal cannot observe or disturb anything, so moving it is not
|
|
3820
|
+
// visible to the program.
|
|
3821
|
+
let reordered = Self::hoist_forward_referenced_module_bindings(&program.statements);
|
|
3822
|
+
self.emit_statements_with_folds(reordered.as_deref().unwrap_or(&program.statements))?;
|
|
3679
3823
|
if self.is_async {
|
|
3680
3824
|
self.async_context_stack.pop();
|
|
3681
3825
|
}
|
|
@@ -4101,9 +4245,26 @@ impl Codegen {
|
|
|
4101
4245
|
// `emit_int32_operand` (a `>>> 0` result is u32 reinterpreted to i32; signed
|
|
4102
4246
|
// bitwise ops yield i32 directly) — so store the i32 with NO `f64` round-trip.
|
|
4103
4247
|
if rust_type == RustType::I32 {
|
|
4248
|
+
// #599: A CAPTURED i32 IS A CELL, AND THE WRITE HAS TO SAY SO. Reads of a
|
|
4249
|
+
// wrapped scalar already go through `vm_read(&x)`; every store below used to
|
|
4250
|
+
// emit a bare `x = <i32>`, so the two halves disagreed and the crate failed with
|
|
4251
|
+
// `expected VmRef<i32>, found i32`. Net effect was that a mutable module-level
|
|
4252
|
+
// typed scalar could not be used at all on GBA — every counter had to move into
|
|
4253
|
+
// a function body or hide in a one-element `i32[]`.
|
|
4254
|
+
//
|
|
4255
|
+
// The narrow-int branch below has always done this; the i32 branch is the one
|
|
4256
|
+
// that was missed, across all three of its store shapes.
|
|
4257
|
+
let is_refcell = self.refcell_wrapped_vars.contains(name.as_ref());
|
|
4258
|
+
let store = |escaped: &str, val: String| {
|
|
4259
|
+
if is_refcell {
|
|
4260
|
+
format!("{{ let _t = {}; *{}.borrow_mut() = _t; }}", val, escaped)
|
|
4261
|
+
} else {
|
|
4262
|
+
format!("{} = {}", escaped, val)
|
|
4263
|
+
}
|
|
4264
|
+
};
|
|
4104
4265
|
if let Some(int_code) = self.emit_int32_operand(value)? {
|
|
4105
4266
|
let escaped = Self::escape_ident(name.as_ref());
|
|
4106
|
-
return Ok(format!("
|
|
4267
|
+
return Ok(store(&escaped, format!("({}) as i32", int_code)));
|
|
4107
4268
|
}
|
|
4108
4269
|
let (val_code, val_ty) = self.emit_typed_expr(value)?;
|
|
4109
4270
|
// An RHS that is ALREADY an integer stores straight into the register. `d = d + 1`
|
|
@@ -4112,7 +4273,7 @@ impl Codegen {
|
|
|
4112
4273
|
// out of it was the whole cost of incrementing an annotated counter.
|
|
4113
4274
|
let escaped_int = Self::escape_ident(name.as_ref());
|
|
4114
4275
|
if val_ty.is_integer_scalar() {
|
|
4115
|
-
return Ok(format!("
|
|
4276
|
+
return Ok(store(&escaped_int, format!("({}) as i32", val_code)));
|
|
4116
4277
|
}
|
|
4117
4278
|
// Defensive: gate guarantees `Some`, but if a future RHS shape slips through,
|
|
4118
4279
|
// fall back to a sound f64-narrowed store rather than miscompiling.
|
|
@@ -4122,11 +4283,7 @@ impl Codegen {
|
|
|
4122
4283
|
val_code
|
|
4123
4284
|
};
|
|
4124
4285
|
let escaped = Self::escape_ident(name.as_ref());
|
|
4125
|
-
return Ok(
|
|
4126
|
-
"{} = {}",
|
|
4127
|
-
escaped,
|
|
4128
|
-
RustType::I32.from_value_expr(&v)
|
|
4129
|
-
));
|
|
4286
|
+
return Ok(store(&escaped, RustType::I32.from_value_expr(&v)));
|
|
4130
4287
|
}
|
|
4131
4288
|
// String self-append `s = s + rhs` -> in-place push_str (amortized O(1)). The
|
|
4132
4289
|
// general path boxes via `ops::add(Value::String(s.clone()), ...)` which clones
|
|
@@ -5777,6 +5934,34 @@ impl Codegen {
|
|
|
5777
5934
|
})
|
|
5778
5935
|
.unwrap_or(RustType::Value);
|
|
5779
5936
|
|
|
5937
|
+
// #597: AN ARRAY THAT ESCAPES INTO A BOXED CALL STAYS BOXED. A native `Vec<T>`
|
|
5938
|
+
// handed to a `value_call` is marshalled by BUILDING A FRESH `Value::Array` from
|
|
5939
|
+
// its elements — a copy by construction — so the callee's `push`es land in the copy
|
|
5940
|
+
// and the caller's array silently keeps its old contents. Nothing reports it: not
|
|
5941
|
+
// the compiler, not the runtime. An UNTYPED array passes the same `VmRef` and
|
|
5942
|
+
// aliases correctly, which means adding an annotation changed program BEHAVIOUR
|
|
5943
|
+
// rather than just representation.
|
|
5944
|
+
//
|
|
5945
|
+
// The call site cannot fix this — boxing a `Vec<i32>` into a `Value::Array` is a
|
|
5946
|
+
// copy however it is spelled. So the decision has to be made here, at the
|
|
5947
|
+
// declaration: if the array is ever passed as a call argument, keep it boxed.
|
|
5948
|
+
//
|
|
5949
|
+
// Same rule the read-only param analysis already applies from the other side
|
|
5950
|
+
// (`arr_param_readonly_fns` refuses an owned copy when a param is mutated, escapes
|
|
5951
|
+
// or is forwarded). Conservative on purpose: an array that never escapes is
|
|
5952
|
+
// untouched and keeps its native lowering.
|
|
5953
|
+
// GBA ONLY. The reporter checked the other backends: `tish run` and a native
|
|
5954
|
+
// `tish build` both alias correctly and print the expected result, so this is a
|
|
5955
|
+
// TARGET divergence rather than a language-wide one. Narrowing off-target would
|
|
5956
|
+
// deopt host code to fix a bug host code does not have, and one host test
|
|
5957
|
+
// (`escaping_array_keeps_oob_safe_lowering`) pins the native store it would remove.
|
|
5958
|
+
if self.emit_mode == crate::NativeEmitMode::Gba
|
|
5959
|
+
&& matches!(rust_type, RustType::Vec(_))
|
|
5960
|
+
&& self.arrays_passed_to_calls.contains(name.as_ref())
|
|
5961
|
+
{
|
|
5962
|
+
rust_type = RustType::Value;
|
|
5963
|
+
}
|
|
5964
|
+
|
|
5780
5965
|
// M5 native-fn body: `let r = genRandom(1)` without a `: number` annotation.
|
|
5781
5966
|
if self.native_fn_body_emit && rust_type == RustType::Value {
|
|
5782
5967
|
if let Some(Expr::Call { callee, .. }) = init.as_ref() {
|
|
@@ -5816,9 +6001,26 @@ impl Codegen {
|
|
|
5816
6001
|
// `s = s + arr[i]`, JS string concat) must stay a boxed `Value` — a native-f64
|
|
5817
6002
|
// store would panic at the `from_value_expr(F64)` coercion. See
|
|
5818
6003
|
// `demoted_numeric_locals`.
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
6004
|
+
// #614: look up (declaring scope, name). A module-level binding was decided
|
|
6005
|
+
// program-wide under scope `""`; a function-local under its top-level fn. Both are
|
|
6006
|
+
// checked because this same emission path serves module code and fn bodies, and a
|
|
6007
|
+
// fn-local name may legitimately shadow a module one.
|
|
6008
|
+
// #614: look up (declaring scope, name). A fn-local is keyed under its top-level
|
|
6009
|
+
// fn; a module binding under `""`. Both are consulted because this same emission
|
|
6010
|
+
// path serves module code and fn bodies.
|
|
6011
|
+
// #614: key by the scope this declaration belongs to. No module fallback — the
|
|
6012
|
+
// statement being emitted IS a declaration, so the variable is by definition owned
|
|
6013
|
+
// by the current scope: inside a fn it is that fn's local (`scope` = the top-level
|
|
6014
|
+
// fn), at module level `scope` is `""`, which is the module key itself. Consulting
|
|
6015
|
+
// the module verdict for a fn-local is exactly the false positive this fixes.
|
|
6016
|
+
if rust_type == RustType::F64 {
|
|
6017
|
+
let scope = self.demote_scope_stack.first().cloned().unwrap_or_default();
|
|
6018
|
+
if self
|
|
6019
|
+
.demoted_numeric_locals
|
|
6020
|
+
.contains(&(scope, name.to_string()))
|
|
6021
|
+
{
|
|
6022
|
+
rust_type = RustType::Value;
|
|
6023
|
+
}
|
|
5822
6024
|
}
|
|
5823
6025
|
|
|
5824
6026
|
// Native-vec call initializer: `let cum = cumulative(probs)` → `Vec<f64>`.
|
|
@@ -5904,12 +6106,22 @@ impl Codegen {
|
|
|
5904
6106
|
return Ok(());
|
|
5905
6107
|
}
|
|
5906
6108
|
|
|
5907
|
-
// Module-level const
|
|
6109
|
+
// Module-level const numeric arrays — no local slot in `run()`. The element type
|
|
6110
|
+
// must match the emitted representation, or a consumer reading the type context
|
|
6111
|
+
// coerces against a layout that is not there.
|
|
5908
6112
|
if self.module_const_f64_arrays.contains_key(name.as_ref())
|
|
5909
6113
|
&& self.outer_vars_stack.len() == 1
|
|
5910
6114
|
{
|
|
6115
|
+
let elem = if self
|
|
6116
|
+
.module_const_int_statics
|
|
6117
|
+
.contains(&Self::module_const_static(name.as_ref()))
|
|
6118
|
+
{
|
|
6119
|
+
RustType::I32
|
|
6120
|
+
} else {
|
|
6121
|
+
RustType::F64
|
|
6122
|
+
};
|
|
5911
6123
|
self.type_context
|
|
5912
|
-
.define(name.as_ref(), RustType::Vec(Box::new(
|
|
6124
|
+
.define(name.as_ref(), RustType::Vec(Box::new(elem)));
|
|
5913
6125
|
return Ok(());
|
|
5914
6126
|
}
|
|
5915
6127
|
|
|
@@ -6969,6 +7181,45 @@ impl Codegen {
|
|
|
6969
7181
|
sibling_escaped, sibling_escaped
|
|
6970
7182
|
));
|
|
6971
7183
|
}
|
|
7184
|
+
// #563: a fn from an OUTER function scope is not a sibling, so the `_ref`/`_cell`
|
|
7185
|
+
// path above never fires for it — but the enclosing frame already materialized it
|
|
7186
|
+
// as a plain `Value` local (module fns and imports arrive that way, via the
|
|
7187
|
+
// `let f = (*f_ref.borrow()).clone();` prelude). A `move` closure therefore MOVES
|
|
7188
|
+
// it, and the SECOND nested fn that names it fails:
|
|
7189
|
+
//
|
|
7190
|
+
// error[E0382]: use of moved value: `uiRowText`
|
|
7191
|
+
//
|
|
7192
|
+
// One nested fn compiles fine, which is why this reads as "nested functions are
|
|
7193
|
+
// broken" rather than "the second one is". Clone it in, exactly as the builtin list
|
|
7194
|
+
// below does for `console`/`Math` — same hazard, same remedy; that list is just a
|
|
7195
|
+
// hardcoded enumeration of the names that happened to hit it first.
|
|
7196
|
+
//
|
|
7197
|
+
// Scope-stack order matters: `.last()` is the current (sibling) scope and is
|
|
7198
|
+
// already handled, and scope 0 is module level. Anything in between is an
|
|
7199
|
+
// enclosing function's scope — those are the locals at risk.
|
|
7200
|
+
let outer_scope_fns: Vec<String> = {
|
|
7201
|
+
let depth = self.function_scope_stack.len();
|
|
7202
|
+
let mut seen = std::collections::HashSet::new();
|
|
7203
|
+
let mut out = Vec::new();
|
|
7204
|
+
for scope in self.function_scope_stack.iter().take(depth.saturating_sub(1)) {
|
|
7205
|
+
for f in scope {
|
|
7206
|
+
if f.as_str() != name_raw
|
|
7207
|
+
&& referenced.contains(f.as_str())
|
|
7208
|
+
&& !sibling_fns.iter().any(|s| s == f)
|
|
7209
|
+
&& !outer_vars.iter().any(|v| v == f)
|
|
7210
|
+
&& !outer_params.iter().any(|p| p == f)
|
|
7211
|
+
&& seen.insert(f.clone())
|
|
7212
|
+
{
|
|
7213
|
+
out.push(f.clone());
|
|
7214
|
+
}
|
|
7215
|
+
}
|
|
7216
|
+
}
|
|
7217
|
+
out
|
|
7218
|
+
};
|
|
7219
|
+
for f in &outer_scope_fns {
|
|
7220
|
+
let escaped = Self::escape_ident(f);
|
|
7221
|
+
self.writeln(&format!("let {} = {}.clone();", escaped, escaped));
|
|
7222
|
+
}
|
|
6972
7223
|
// Clone outer parameters so they can be captured by the move closure
|
|
6973
7224
|
for outer_param in &outer_params {
|
|
6974
7225
|
let param_escaped = Self::escape_ident(outer_param);
|
|
@@ -7323,9 +7574,12 @@ impl Codegen {
|
|
|
7323
7574
|
for v in &body_cell_vars {
|
|
7324
7575
|
self.refcell_wrapped_vars.insert(v.clone());
|
|
7325
7576
|
}
|
|
7577
|
+
// #614: this body's locals were analyzed under THIS fn's scope.
|
|
7578
|
+
self.demote_scope_stack.push(name_raw.to_string());
|
|
7326
7579
|
for s in statements {
|
|
7327
7580
|
self.emit_statement(s)?;
|
|
7328
7581
|
}
|
|
7582
|
+
self.demote_scope_stack.pop();
|
|
7329
7583
|
for v in &body_cell_vars {
|
|
7330
7584
|
self.refcell_wrapped_vars.remove(v);
|
|
7331
7585
|
}
|
|
@@ -7337,7 +7591,9 @@ impl Codegen {
|
|
|
7337
7591
|
self.outer_vars_stack.push(Vec::new());
|
|
7338
7592
|
self.rc_cell_storage_scopes
|
|
7339
7593
|
.push(std::collections::HashSet::new());
|
|
7594
|
+
self.demote_scope_stack.push(name_raw.to_string()); // #614
|
|
7340
7595
|
self.emit_statement(body)?;
|
|
7596
|
+
self.demote_scope_stack.pop();
|
|
7341
7597
|
self.function_scope_stack.pop();
|
|
7342
7598
|
self.outer_vars_stack.pop();
|
|
7343
7599
|
self.rc_cell_storage_scopes.pop();
|
|
@@ -7954,7 +8210,7 @@ impl Codegen {
|
|
|
7954
8210
|
));
|
|
7955
8211
|
}
|
|
7956
8212
|
if let Some(static_name) = self.module_const_f64_array_rust_ref(name.as_ref()) {
|
|
7957
|
-
let v =
|
|
8213
|
+
let v = self.module_const_f64_array_as_value(&static_name);
|
|
7958
8214
|
return Ok(if self.value_fn_depth > 0 || !self.loop_stack.is_empty() {
|
|
7959
8215
|
format!("({}).clone()", v)
|
|
7960
8216
|
} else {
|
|
@@ -9098,10 +9354,10 @@ impl Codegen {
|
|
|
9098
9354
|
// `value_call` dispatch through the namespace object. Falls through to the boxed path
|
|
9099
9355
|
// when the arg count/kind doesn't match (a dynamic/spread call stays correct).
|
|
9100
9356
|
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
9101
|
-
if let Some(sig
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
{
|
|
9357
|
+
if let Some((sig, suffix)) =
|
|
9358
|
+
self.extern_sig_for_arity(fname.as_ref(), args.len())
|
|
9359
|
+
{
|
|
9360
|
+
if args.iter().all(|a| matches!(a, CallArg::Expr(_))) {
|
|
9105
9361
|
let mut argc: Vec<String> = Vec::with_capacity(args.len());
|
|
9106
9362
|
for (a, ty) in args.iter().zip(sig.params.iter()) {
|
|
9107
9363
|
if let CallArg::Expr(e) = a {
|
|
@@ -9109,9 +9365,10 @@ impl Codegen {
|
|
|
9109
9365
|
}
|
|
9110
9366
|
}
|
|
9111
9367
|
let call = format!(
|
|
9112
|
-
"{}::{}
|
|
9368
|
+
"{}::{}{}({})",
|
|
9113
9369
|
sig.crate_name,
|
|
9114
9370
|
sig.symbol,
|
|
9371
|
+
suffix,
|
|
9115
9372
|
argc.join(", ")
|
|
9116
9373
|
);
|
|
9117
9374
|
// This is a Value-producing context: a `void` extern yields `Null`; a
|
|
@@ -12444,9 +12701,16 @@ impl Codegen {
|
|
|
12444
12701
|
/// function that mixes the two. A function whose whole signature is `i32` has one convention
|
|
12445
12702
|
/// and keeps the coercion at the boundary, where the caller does it once on an argument that is
|
|
12446
12703
|
/// usually already an integer.
|
|
12704
|
+
///
|
|
12705
|
+
/// #647: ZERO PARAMETERS QUALIFY. The rule above is about keeping ONE coercion convention
|
|
12706
|
+
/// across a signature — and a signature with no parameters has nothing to be inconsistent
|
|
12707
|
+
/// about, so the reason the rule exists cannot apply to it. Excluding it cost every niladic
|
|
12708
|
+
/// accessor (`rCols()`, `rPocket()`, `atkLines()`) a boxed `value_call`: measured at 55 ticks
|
|
12709
|
+
/// against ~0 for the identical body taking one argument. The body proof still runs, so a
|
|
12710
|
+
/// zero-arg fn that touches something unlowerable is rejected on its body, as before — this
|
|
12711
|
+
/// only stops rejecting it on its (empty) parameter list.
|
|
12447
12712
|
fn fn_sig_all_i32(params: &[FunParam], return_type: &Option<TypeAnnotation>) -> bool {
|
|
12448
12713
|
return_type.as_ref().is_some_and(Self::ann_is_i32)
|
|
12449
|
-
&& !params.is_empty()
|
|
12450
12714
|
&& params.iter().all(|p| {
|
|
12451
12715
|
matches!(p, FunParam::Simple(tp)
|
|
12452
12716
|
if tp.default.is_none()
|
|
@@ -12711,23 +12975,105 @@ impl Codegen {
|
|
|
12711
12975
|
}
|
|
12712
12976
|
}
|
|
12713
12977
|
|
|
12714
|
-
fn collect_demoted_numeric_locals(&self, stmts: &[Statement]) -> HashSet<String> {
|
|
12715
|
-
//
|
|
12978
|
+
fn collect_demoted_numeric_locals(&self, stmts: &[Statement]) -> HashSet<(String, String)> {
|
|
12979
|
+
// #614: the analysis below is UNCHANGED — same env, same reassignment set, same fixpoint.
|
|
12980
|
+
// What changed is the KEY. Previously a bare name, so one boxed `i` anywhere in the bundle
|
|
12981
|
+
// demoted every `i` in every module (~40-60 cycles per boxed use against 2-4 native); with
|
|
12982
|
+
// 44 of tish-gba's 70 packages untyped, a legacy package silently de-optimized a fully
|
|
12983
|
+
// modernized one that merely shared a loop-variable name.
|
|
12984
|
+
//
|
|
12985
|
+
// Scope granularity is the TOP-LEVEL FN, or `""` for module level. Each fn is analyzed from
|
|
12986
|
+
// its own `FunDecl` statement — not just its body — so params (including a REST param, whose
|
|
12987
|
+
// `Vec<f64>` element type is what keeps `total` native in `typed_assign_conversion`) are
|
|
12988
|
+
// collected exactly as the flat pass collected them. Nested fns fold into their enclosing
|
|
12989
|
+
// top-level fn: still conservative, but bounded to one function instead of the whole bundle.
|
|
12990
|
+
let mut out: HashSet<(String, String)> = HashSet::new();
|
|
12991
|
+
|
|
12992
|
+
// Module-level names keep the program-wide verdict: a module `let` is reassignable from
|
|
12993
|
+
// inside any fn, so whether it can go non-numeric is genuinely a whole-program property.
|
|
12994
|
+
let module_names: HashSet<String> = stmts
|
|
12995
|
+
.iter()
|
|
12996
|
+
.filter_map(|s| match s {
|
|
12997
|
+
Statement::VarDecl { name, .. } => Some(name.to_string()),
|
|
12998
|
+
_ => None,
|
|
12999
|
+
})
|
|
13000
|
+
.collect();
|
|
13001
|
+
|
|
12716
13002
|
let mut env: HashMap<String, RustType> = HashMap::new();
|
|
12717
13003
|
Self::collect_annotated_types(stmts, &self.type_aliases, &mut env);
|
|
12718
|
-
// 2. Every reassignment `(name, rhs)` anywhere in the program (incl. nested exprs/closures).
|
|
12719
13004
|
let mut reassigns: Vec<(String, &Expr)> = Vec::new();
|
|
12720
13005
|
Self::collect_reassignments_stmts(stmts, &mut reassigns);
|
|
12721
|
-
|
|
13006
|
+
for name in self.demote_fixpoint(&mut env, &reassigns, &module_names) {
|
|
13007
|
+
out.insert((String::new(), name));
|
|
13008
|
+
}
|
|
13009
|
+
|
|
13010
|
+
// A fn's env must START from what MODULE scope makes visible to it. Without that, a body
|
|
13011
|
+
// reading a module binding — `sum = (sum + codes[j]) % M`, where `codes` is a hoisted const
|
|
13012
|
+
// f64 array — cannot type the RHS, wrongly demotes `sum` to `Value`, and the emitted code
|
|
13013
|
+
// then adds an `f64` static to a `Value`: `error[E0369]: cannot add f64 to Value`. The flat
|
|
13014
|
+
// pass had this information for free; scoping the KEY must not scope away the VISIBILITY.
|
|
13015
|
+
//
|
|
13016
|
+
// Module-only, and carrying the module verdicts just settled above: a demoted module binding
|
|
13017
|
+
// reads as `Value` inside a fn too, which is exactly what its declaration will emit.
|
|
13018
|
+
let mut module_env: HashMap<String, RustType> = HashMap::new();
|
|
13019
|
+
for s in stmts {
|
|
13020
|
+
if !matches!(s, Statement::FunDecl { .. }) {
|
|
13021
|
+
Self::collect_annotated_types(std::slice::from_ref(s), &self.type_aliases, &mut module_env);
|
|
13022
|
+
}
|
|
13023
|
+
}
|
|
13024
|
+
for (scope, n) in &out {
|
|
13025
|
+
if scope.is_empty() {
|
|
13026
|
+
module_env.insert(n.clone(), RustType::Value);
|
|
13027
|
+
}
|
|
13028
|
+
}
|
|
13029
|
+
|
|
13030
|
+
for s in stmts {
|
|
13031
|
+
let Statement::FunDecl { name, .. } = s else {
|
|
13032
|
+
continue;
|
|
13033
|
+
};
|
|
13034
|
+
let scope = name.to_string();
|
|
13035
|
+
let one = std::slice::from_ref(s);
|
|
13036
|
+
// Module bindings first, then this fn's own declarations OVERLAY them — so a local that
|
|
13037
|
+
// shadows a module name takes its own type, not the module one.
|
|
13038
|
+
// What this fn itself declares — computed separately from the visibility env, because
|
|
13039
|
+
// only these names may take a verdict under this scope. INCLUDES a name that shadows a
|
|
13040
|
+
// module binding: a shadowing local is a different variable and gets its own verdict;
|
|
13041
|
+
// leaving it out drops it back onto the module's verdict, which is the bug being fixed.
|
|
13042
|
+
let mut own: HashMap<String, RustType> = HashMap::new();
|
|
13043
|
+
Self::collect_annotated_types(one, &self.type_aliases, &mut own);
|
|
13044
|
+
let local_names: HashSet<String> = own.keys().cloned().collect();
|
|
13045
|
+
let mut fenv: HashMap<String, RustType> = module_env.clone();
|
|
13046
|
+
fenv.extend(own);
|
|
13047
|
+
if local_names.is_empty() {
|
|
13048
|
+
continue;
|
|
13049
|
+
}
|
|
13050
|
+
let mut freassigns: Vec<(String, &Expr)> = Vec::new();
|
|
13051
|
+
Self::collect_reassignments_stmts(one, &mut freassigns);
|
|
13052
|
+
for n in self.demote_fixpoint(&mut fenv, &freassigns, &local_names) {
|
|
13053
|
+
out.insert((scope.clone(), n));
|
|
13054
|
+
}
|
|
13055
|
+
}
|
|
13056
|
+
out
|
|
13057
|
+
}
|
|
13058
|
+
|
|
13059
|
+
/// #614: the demotion fixpoint, factored out so the module scope and every function scope run
|
|
13060
|
+
/// the IDENTICAL rule. Only names in `considered` may demote; `env` is updated in place so a
|
|
13061
|
+
/// demoted name's later reads see `Value` — that feedback is what makes it a fixpoint.
|
|
13062
|
+
fn demote_fixpoint(
|
|
13063
|
+
&self,
|
|
13064
|
+
env: &mut HashMap<String, RustType>,
|
|
13065
|
+
reassigns: &[(String, &Expr)],
|
|
13066
|
+
considered: &HashSet<String>,
|
|
13067
|
+
) -> HashSet<String> {
|
|
12722
13068
|
let mut demoted: HashSet<String> = HashSet::new();
|
|
12723
13069
|
loop {
|
|
12724
13070
|
let mut changed = false;
|
|
12725
|
-
for (name, rhs) in
|
|
12726
|
-
if demoted.contains(name) {
|
|
13071
|
+
for (name, rhs) in reassigns {
|
|
13072
|
+
if demoted.contains(name) || !considered.contains(name) {
|
|
12727
13073
|
continue;
|
|
12728
13074
|
}
|
|
12729
13075
|
if env.get(name) == Some(&RustType::F64)
|
|
12730
|
-
&& self.expr_native_type(rhs,
|
|
13076
|
+
&& self.expr_native_type(rhs, env) != RustType::F64
|
|
12731
13077
|
{
|
|
12732
13078
|
demoted.insert(name.clone());
|
|
12733
13079
|
env.insert(name.clone(), RustType::Value);
|
|
@@ -12939,8 +13285,28 @@ impl Codegen {
|
|
|
12939
13285
|
}
|
|
12940
13286
|
|
|
12941
13287
|
fn index_in_bounds(&self, index: &Expr, arr: &str) -> bool {
|
|
12942
|
-
|
|
12943
|
-
|
|
13288
|
+
// #645: a PROMOTED module-const array knows its own length unconditionally — it is a
|
|
13289
|
+
// `const G_X: [i32; N]`, fixed at emit time. `vec_fixed_len` is per-scope inference and is
|
|
13290
|
+
// NOT populated for it in every body, so the SAME expression proved in bounds inside a
|
|
13291
|
+
// function's native twin and failed inside its boxed closure — the twin got a direct
|
|
13292
|
+
// integer read while the closure, which is the copy a boxed call actually runs, kept the
|
|
13293
|
+
// bounds-checked `f64`/NaN form. Falling back to the static's own length makes the proof
|
|
13294
|
+
// scope-independent, which is what a compile-time constant deserves.
|
|
13295
|
+
let owned_len;
|
|
13296
|
+
let len = match self.vec_fixed_len.get(arr) {
|
|
13297
|
+
Some(l) => l,
|
|
13298
|
+
None => match self
|
|
13299
|
+
.module_const_f64_arrays
|
|
13300
|
+
.get(arr)
|
|
13301
|
+
.map(|v| v.len())
|
|
13302
|
+
.filter(|n| *n > 0)
|
|
13303
|
+
{
|
|
13304
|
+
Some(n) => {
|
|
13305
|
+
owned_len = BoundKey::Const(n as i64);
|
|
13306
|
+
&owned_len
|
|
13307
|
+
}
|
|
13308
|
+
None => return false,
|
|
13309
|
+
},
|
|
12944
13310
|
};
|
|
12945
13311
|
let Expr::Ident { name: idx, .. } = index else {
|
|
12946
13312
|
if let Expr::Literal {
|
|
@@ -12956,6 +13322,32 @@ impl Codegen {
|
|
|
12956
13322
|
return true;
|
|
12957
13323
|
}
|
|
12958
13324
|
}
|
|
13325
|
+
// #645: a MASK bounds its result exactly. For any i32 `x` and a non-negative literal
|
|
13326
|
+
// mask `M`, `x & M` lies in `[0, M]` — the AND clears every bit above M's highest,
|
|
13327
|
+
// including the sign bit, so a negative `x` cannot escape the range either. That makes
|
|
13328
|
+
// `A[k & 63]` provably in bounds for a 64-element `A` without knowing anything about
|
|
13329
|
+
// `k`.
|
|
13330
|
+
//
|
|
13331
|
+
// Worth the special case because it is the idiomatic way to index a power-of-two table,
|
|
13332
|
+
// and without it such a read misses the proven-in-bounds path and falls to the
|
|
13333
|
+
// bounds-checked form — which returns `f64` (it must be able to answer NaN), dragging
|
|
13334
|
+
// an otherwise all-integer expression through an f64 round trip on EVERY element.
|
|
13335
|
+
if let Expr::Binary { op: BinOp::BitAnd, left, right, .. } = index {
|
|
13336
|
+
let mask_of = |e: &Expr| -> Option<i64> {
|
|
13337
|
+
if let Expr::Literal { value: Literal::Number(n), .. } = e {
|
|
13338
|
+
Self::int_literal_value(*n).filter(|v| *v >= 0)
|
|
13339
|
+
} else {
|
|
13340
|
+
None
|
|
13341
|
+
}
|
|
13342
|
+
};
|
|
13343
|
+
if let Some(m) = mask_of(right).or_else(|| mask_of(left)) {
|
|
13344
|
+
if let BoundKey::Const(len) = len {
|
|
13345
|
+
if m < *len {
|
|
13346
|
+
return true;
|
|
13347
|
+
}
|
|
13348
|
+
}
|
|
13349
|
+
}
|
|
13350
|
+
}
|
|
12959
13351
|
return self.sub_index_in_bounds(index, arr);
|
|
12960
13352
|
};
|
|
12961
13353
|
let idx = idx.as_ref();
|
|
@@ -14053,13 +14445,22 @@ impl Codegen {
|
|
|
14053
14445
|
// so a RHS that reads the same cell (`a[i] += x`, `a[i] = a[j]`) releases its shared borrow
|
|
14054
14446
|
// first (no overlapping lock → no deadlock), then store into the real element through the cell.
|
|
14055
14447
|
if wrapped {
|
|
14056
|
-
|
|
14057
|
-
|
|
14448
|
+
// #611: I32 GROWS TOO. A boxed array absorbs `a[i] = v` past the end by growing; a
|
|
14449
|
+
// typed one used to fall through to a bare element store, so the same source line
|
|
14450
|
+
// behaved differently depending only on whether the annotation was present — the write
|
|
14451
|
+
// was lost with no error at compile time or run time. That silent divergence between
|
|
14452
|
+
// the typed and boxed forms is the worst of the available options, so the typed form
|
|
14453
|
+
// now matches the boxed one. Pad is `0`, which is what the READ path already answers
|
|
14454
|
+
// for an out-of-range `i32` element (`.get(i).copied().unwrap_or(0)`).
|
|
14455
|
+
let needs_resize = matches!(
|
|
14456
|
+
elem_type.as_ref(),
|
|
14457
|
+
RustType::F64 | RustType::Bool | RustType::I32
|
|
14458
|
+
) && !in_bounds;
|
|
14058
14459
|
let store = if needs_resize {
|
|
14059
|
-
let pad =
|
|
14060
|
-
"f64::NAN"
|
|
14061
|
-
|
|
14062
|
-
"false"
|
|
14460
|
+
let pad = match elem_type.as_ref() {
|
|
14461
|
+
RustType::F64 => "f64::NAN",
|
|
14462
|
+
RustType::I32 => "0",
|
|
14463
|
+
_ => "false",
|
|
14063
14464
|
};
|
|
14064
14465
|
format!(
|
|
14065
14466
|
"let __i = {idx}; let __v = {val}; let mut __g = {esc}.borrow_mut(); if __i >= __g.len() {{ __g.resize(__i + 1, {pad}); }} __g[__i] = __v;",
|
|
@@ -14085,16 +14486,22 @@ impl Codegen {
|
|
|
14085
14486
|
format!("{{ {}[{}] = {}; Value::Null }}", esc_obj, idx_usize, native_val)
|
|
14086
14487
|
}
|
|
14087
14488
|
}
|
|
14088
|
-
|
|
14089
|
-
|
|
14090
|
-
|
|
14091
|
-
|
|
14092
|
-
"
|
|
14489
|
+
// #611: same grow-to-index rule on the unwrapped path — see the note above.
|
|
14490
|
+
RustType::F64 | RustType::Bool | RustType::I32 => {
|
|
14491
|
+
let pad = match elem_type.as_ref() {
|
|
14492
|
+
RustType::F64 => "f64::NAN",
|
|
14493
|
+
RustType::I32 => "0",
|
|
14494
|
+
_ => "false",
|
|
14093
14495
|
};
|
|
14094
|
-
format!(
|
|
14095
|
-
"{{ let _idx = {}; if _idx >= {}.len() {{ {}.resize(_idx + 1, {}); }} {}[_idx] = {}
|
|
14496
|
+
let store = format!(
|
|
14497
|
+
"{{ let _idx = {}; if _idx >= {}.len() {{ {}.resize(_idx + 1, {}); }} {}[_idx] = {}",
|
|
14096
14498
|
idx_usize, esc_obj, esc_obj, pad, esc_obj, native_val
|
|
14097
|
-
)
|
|
14499
|
+
);
|
|
14500
|
+
if side_effect_only {
|
|
14501
|
+
format!("{} }}", store)
|
|
14502
|
+
} else {
|
|
14503
|
+
format!("{}; Value::Null }}", store)
|
|
14504
|
+
}
|
|
14098
14505
|
}
|
|
14099
14506
|
_ if side_effect_only => {
|
|
14100
14507
|
format!("{}[{}] = {}", esc_obj, idx_usize, native_val)
|
|
@@ -15411,7 +15818,7 @@ impl Codegen {
|
|
|
15411
15818
|
/// statements (blocks, if, loops, switch, try, return/throw). `f` is responsible for recursing
|
|
15412
15819
|
/// into each expression's own subtree. Does NOT descend into nested fn-decl bodies (a different
|
|
15413
15820
|
/// lexical scope; a captured loop var would be RefCell-bailed before reaching here).
|
|
15414
|
-
fn for_each_stmt_expr(s: &Statement, f: &mut dyn FnMut(&Expr)) {
|
|
15821
|
+
pub(crate) fn for_each_stmt_expr(s: &Statement, f: &mut dyn FnMut(&Expr)) {
|
|
15415
15822
|
match s {
|
|
15416
15823
|
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
15417
15824
|
for st in statements {
|
|
@@ -16798,7 +17205,15 @@ impl Codegen {
|
|
|
16798
17205
|
{
|
|
16799
17206
|
let sum_esc = Self::escape_ident(sum_name.as_ref());
|
|
16800
17207
|
let arms: Vec<String> = (0..n)
|
|
16801
|
-
|
|
17208
|
+
// The fasta ladder is specialised around f64 arms, so an integer-represented
|
|
17209
|
+
// codes array widens here rather than perturbing that path.
|
|
17210
|
+
.map(|i| {
|
|
17211
|
+
if self.module_const_int_statics.contains(codes_static.as_str()) {
|
|
17212
|
+
format!("{} => {}[{}] as f64", i, codes_static, i)
|
|
17213
|
+
} else {
|
|
17214
|
+
format!("{} => {}[{}]", i, codes_static, i)
|
|
17215
|
+
}
|
|
17216
|
+
})
|
|
16802
17217
|
.collect();
|
|
16803
17218
|
self.writeln(&format!(
|
|
16804
17219
|
"{} = (({} + match ({} as usize) {{ {}, _ => 0.0 }}) % 2147483647_f64);",
|
|
@@ -16956,6 +17371,105 @@ impl Codegen {
|
|
|
16956
17371
|
self.native_fn_emit_name.is_some() || self.native_vec_ret.is_some()
|
|
16957
17372
|
}
|
|
16958
17373
|
|
|
17374
|
+
/// #629: move module-level `let`s with LITERAL initialisers ahead of any function declared
|
|
17375
|
+
/// above them that references the name. Returns `None` when nothing needs moving, so the common
|
|
17376
|
+
/// program keeps its exact statement slice and this costs one scan.
|
|
17377
|
+
///
|
|
17378
|
+
/// Only bindings that are actually forward-referenced move, and only past the point where they
|
|
17379
|
+
/// are first needed — the relative order of everything else is preserved.
|
|
17380
|
+
fn hoist_forward_referenced_module_bindings(stmts: &[Statement]) -> Option<Vec<Statement>> {
|
|
17381
|
+
// Names bound at module level by a literal `let`, with the index of their declaration.
|
|
17382
|
+
let mut literal_decls: Vec<(usize, String)> = Vec::new();
|
|
17383
|
+
for (i, s) in stmts.iter().enumerate() {
|
|
17384
|
+
if let Statement::VarDecl {
|
|
17385
|
+
name,
|
|
17386
|
+
init: Some(e),
|
|
17387
|
+
..
|
|
17388
|
+
} = s
|
|
17389
|
+
{
|
|
17390
|
+
if Self::is_literal_initialiser(e) {
|
|
17391
|
+
literal_decls.push((i, name.to_string()));
|
|
17392
|
+
}
|
|
17393
|
+
}
|
|
17394
|
+
}
|
|
17395
|
+
if literal_decls.is_empty() {
|
|
17396
|
+
return None;
|
|
17397
|
+
}
|
|
17398
|
+
// A NAME DECLARED MORE THAN ONCE AT MODULE LEVEL IS NEVER MOVED. An earlier mention of such
|
|
17399
|
+
// a name refers to the EARLIER binding, not forward to this one, so hoisting this
|
|
17400
|
+
// declaration over it silently changes which value the earlier code sees. That is a wrong
|
|
17401
|
+
// ANSWER, not a build break: tests/core/js_compat_gaps.tish declares `text` twice — a string
|
|
17402
|
+
// at the top and a different string further down — and moving the second above the first
|
|
17403
|
+
// made an intervening `slice` read the wrong source, printing `hello 123 world 45` where
|
|
17404
|
+
// `.mdx` was expected.
|
|
17405
|
+
let mut decl_counts: std::collections::HashMap<&str, usize> =
|
|
17406
|
+
std::collections::HashMap::new();
|
|
17407
|
+
for s in stmts {
|
|
17408
|
+
if let Statement::VarDecl { name, .. } = s {
|
|
17409
|
+
*decl_counts.entry(name.as_ref()).or_insert(0) += 1;
|
|
17410
|
+
}
|
|
17411
|
+
}
|
|
17412
|
+
// A binding needs moving when a FUNCTION BODY above its declaration mentions it. Deferred
|
|
17413
|
+
// execution is what makes a forward reference legal — the body does not run until called.
|
|
17414
|
+
//
|
|
17415
|
+
// Straight-line code above the declaration is deliberately NOT counted: there the mention
|
|
17416
|
+
// either refers to a previous binding of the name or is a genuine use-before-init, and in
|
|
17417
|
+
// neither case may the declaration be moved over it.
|
|
17418
|
+
let mut needed: Vec<(usize, usize)> = Vec::new(); // (decl index, first referencing index)
|
|
17419
|
+
for (di, name) in &literal_decls {
|
|
17420
|
+
if decl_counts.get(name.as_str()).copied().unwrap_or(0) > 1 {
|
|
17421
|
+
continue;
|
|
17422
|
+
}
|
|
17423
|
+
let mut first = None;
|
|
17424
|
+
for (i, s) in stmts.iter().enumerate().take(*di) {
|
|
17425
|
+
if matches!(s, Statement::FunDecl { .. }) && Self::stmt_mentions_ident(s, name) {
|
|
17426
|
+
first = Some(i);
|
|
17427
|
+
break;
|
|
17428
|
+
}
|
|
17429
|
+
}
|
|
17430
|
+
if let Some(f) = first {
|
|
17431
|
+
needed.push((*di, f));
|
|
17432
|
+
}
|
|
17433
|
+
}
|
|
17434
|
+
if needed.is_empty() {
|
|
17435
|
+
return None;
|
|
17436
|
+
}
|
|
17437
|
+
// Rebuild: each moved declaration is inserted immediately before the first statement that
|
|
17438
|
+
// references it, keeping every other statement in place.
|
|
17439
|
+
let mut out: Vec<Statement> = Vec::with_capacity(stmts.len());
|
|
17440
|
+
let moved: std::collections::HashSet<usize> = needed.iter().map(|(d, _)| *d).collect();
|
|
17441
|
+
for (i, s) in stmts.iter().enumerate() {
|
|
17442
|
+
for (di, fi) in &needed {
|
|
17443
|
+
if *fi == i {
|
|
17444
|
+
out.push(stmts[*di].clone());
|
|
17445
|
+
}
|
|
17446
|
+
}
|
|
17447
|
+
if !moved.contains(&i) {
|
|
17448
|
+
out.push(s.clone());
|
|
17449
|
+
}
|
|
17450
|
+
}
|
|
17451
|
+
Some(out)
|
|
17452
|
+
}
|
|
17453
|
+
|
|
17454
|
+
/// An initialiser that cannot observe or disturb program state, so moving it is invisible.
|
|
17455
|
+
/// Deliberately narrow — array/object literals are included only when every element is itself
|
|
17456
|
+
/// literal, so no call or property read comes along for the ride.
|
|
17457
|
+
fn is_literal_initialiser(e: &Expr) -> bool {
|
|
17458
|
+
match e {
|
|
17459
|
+
Expr::Literal { .. } => true,
|
|
17460
|
+
Expr::Unary { operand, .. } => Self::is_literal_initialiser(operand),
|
|
17461
|
+
Expr::Array { elements, .. } => elements.iter().all(|el| match el {
|
|
17462
|
+
ArrayElement::Expr(e) => Self::is_literal_initialiser(e),
|
|
17463
|
+
_ => false,
|
|
17464
|
+
}),
|
|
17465
|
+
Expr::Object { props, .. } => props.iter().all(|p| match p {
|
|
17466
|
+
ObjectProp::KeyValue(_, v, _) => Self::is_literal_initialiser(v),
|
|
17467
|
+
_ => false,
|
|
17468
|
+
}),
|
|
17469
|
+
_ => false,
|
|
17470
|
+
}
|
|
17471
|
+
}
|
|
17472
|
+
|
|
16959
17473
|
fn emit_statements_with_folds(&mut self, statements: &[Statement]) -> Result<(), CompileError> {
|
|
16960
17474
|
let mut i = 0;
|
|
16961
17475
|
while i < statements.len() {
|
|
@@ -17155,41 +17669,82 @@ impl Codegen {
|
|
|
17155
17669
|
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
17156
17670
|
Self::walk_stmts_for_module_const_disqualifiers(statements, g, bad);
|
|
17157
17671
|
}
|
|
17672
|
+
// EVERY child expression has to be visited, not just the sub-statements. These arms used
|
|
17673
|
+
// to walk only the bodies, so a use in a loop or branch CONDITION was invisible — which
|
|
17674
|
+
// is how `while (i < SB_FORCE.length)` (card-gba qb-engine.tish) kept an array eligible
|
|
17675
|
+
// for hoisting even though `.length` is a disqualifying member use. Same defect class as
|
|
17676
|
+
// the missing closure arm below: the walker's silence meant "safe", not "absent".
|
|
17158
17677
|
Statement::If {
|
|
17678
|
+
cond,
|
|
17159
17679
|
then_branch,
|
|
17160
17680
|
else_branch,
|
|
17161
17681
|
..
|
|
17162
17682
|
} => {
|
|
17683
|
+
Self::walk_expr_for_module_const_disqualifiers(cond, g, bad);
|
|
17163
17684
|
Self::walk_stmt_for_module_const_disqualifiers(then_branch, g, bad);
|
|
17164
17685
|
if let Some(eb) = else_branch {
|
|
17165
17686
|
Self::walk_stmt_for_module_const_disqualifiers(eb, g, bad);
|
|
17166
17687
|
}
|
|
17167
17688
|
}
|
|
17168
|
-
Statement::While { body, .. } | Statement::DoWhile { body, .. } => {
|
|
17689
|
+
Statement::While { cond, body, .. } | Statement::DoWhile { body, cond, .. } => {
|
|
17690
|
+
Self::walk_expr_for_module_const_disqualifiers(cond, g, bad);
|
|
17169
17691
|
Self::walk_stmt_for_module_const_disqualifiers(body, g, bad);
|
|
17170
17692
|
}
|
|
17171
|
-
Statement::For {
|
|
17693
|
+
Statement::For {
|
|
17694
|
+
init,
|
|
17695
|
+
cond,
|
|
17696
|
+
update,
|
|
17697
|
+
body,
|
|
17698
|
+
..
|
|
17699
|
+
} => {
|
|
17172
17700
|
if let Some(i) = init {
|
|
17173
17701
|
Self::walk_stmt_for_module_const_disqualifiers(i, g, bad);
|
|
17174
17702
|
}
|
|
17703
|
+
if let Some(c) = cond {
|
|
17704
|
+
Self::walk_expr_for_module_const_disqualifiers(c, g, bad);
|
|
17705
|
+
}
|
|
17706
|
+
if let Some(u) = update {
|
|
17707
|
+
Self::walk_expr_for_module_const_disqualifiers(u, g, bad);
|
|
17708
|
+
}
|
|
17175
17709
|
Self::walk_stmt_for_module_const_disqualifiers(body, g, bad);
|
|
17176
17710
|
}
|
|
17177
|
-
Statement::ForOf { body, .. }
|
|
17178
|
-
| Statement::ForIn {
|
|
17711
|
+
Statement::ForOf { iterable, body, .. }
|
|
17712
|
+
| Statement::ForIn {
|
|
17713
|
+
object: iterable,
|
|
17714
|
+
body,
|
|
17715
|
+
..
|
|
17716
|
+
} => {
|
|
17717
|
+
// ITERATING THE ARRAY IS A SUPPORTED USE, not a disqualifying one: `for (x of X)`
|
|
17718
|
+
// lowers to a native index loop over the hoisted static (`for _fof_i0 in
|
|
17719
|
+
// 0..G_X.len()`), so the name never survives into the output. Walking the iterable
|
|
17720
|
+
// unconditionally would see a bare `Ident` and refuse the hoist, which loses the
|
|
17721
|
+
// very optimisation the pass exists for (tests/core/typed_array_forof.tish).
|
|
17722
|
+
// Anything else in iterable position is walked normally.
|
|
17723
|
+
if !matches!(iterable, Expr::Ident { name, .. } if name.as_ref() == g) {
|
|
17724
|
+
Self::walk_expr_for_module_const_disqualifiers(iterable, g, bad);
|
|
17725
|
+
}
|
|
17179
17726
|
Self::walk_stmt_for_module_const_disqualifiers(body, g, bad);
|
|
17180
17727
|
}
|
|
17181
17728
|
Statement::Switch {
|
|
17729
|
+
expr,
|
|
17182
17730
|
cases,
|
|
17183
17731
|
default_body,
|
|
17184
17732
|
..
|
|
17185
17733
|
} => {
|
|
17186
|
-
|
|
17734
|
+
Self::walk_expr_for_module_const_disqualifiers(expr, g, bad);
|
|
17735
|
+
for (case_expr, stmts) in cases {
|
|
17736
|
+
if let Some(e) = case_expr {
|
|
17737
|
+
Self::walk_expr_for_module_const_disqualifiers(e, g, bad);
|
|
17738
|
+
}
|
|
17187
17739
|
Self::walk_stmts_for_module_const_disqualifiers(stmts, g, bad);
|
|
17188
17740
|
}
|
|
17189
17741
|
if let Some(stmts) = default_body {
|
|
17190
17742
|
Self::walk_stmts_for_module_const_disqualifiers(stmts, g, bad);
|
|
17191
17743
|
}
|
|
17192
17744
|
}
|
|
17745
|
+
Statement::Throw { value, .. } => {
|
|
17746
|
+
Self::walk_expr_for_module_const_disqualifiers(value, g, bad);
|
|
17747
|
+
}
|
|
17193
17748
|
Statement::Try {
|
|
17194
17749
|
body,
|
|
17195
17750
|
catch_body,
|
|
@@ -17220,10 +17775,32 @@ impl Codegen {
|
|
|
17220
17775
|
Self::walk_expr_for_module_const_disqualifiers(e, g, bad);
|
|
17221
17776
|
}
|
|
17222
17777
|
}
|
|
17223
|
-
|
|
17778
|
+
// CONSERVATIVE DEFAULT. This walker decides whether it is safe to move an array into a
|
|
17779
|
+
// `const`, so "I do not model this statement" must mean "refuse", never "permit" — the
|
|
17780
|
+
// old `_ => {}` meant the latter, and a wrong permit is a program that does not compile.
|
|
17781
|
+
// Over-rejecting only costs the hoist; under-rejecting cost four ROMs their build.
|
|
17782
|
+
s => {
|
|
17783
|
+
if Self::stmt_mentions_ident(s, g) {
|
|
17784
|
+
*bad = true;
|
|
17785
|
+
}
|
|
17786
|
+
}
|
|
17224
17787
|
}
|
|
17225
17788
|
}
|
|
17226
17789
|
|
|
17790
|
+
/// Complete-traversal membership test, used as the safe fallback by both walkers above.
|
|
17791
|
+
fn stmt_mentions_ident(s: &Statement, g: &str) -> bool {
|
|
17792
|
+
let mut idents = HashSet::new();
|
|
17793
|
+
Self::collect_stmt_idents(s, &mut idents);
|
|
17794
|
+
idents.contains(g)
|
|
17795
|
+
}
|
|
17796
|
+
|
|
17797
|
+
/// As `stmt_mentions_ident`, for an expression subtree.
|
|
17798
|
+
fn expr_mentions_ident(e: &Expr, g: &str) -> bool {
|
|
17799
|
+
let mut idents = HashSet::new();
|
|
17800
|
+
Self::collect_expr_idents(e, &mut idents);
|
|
17801
|
+
idents.contains(g)
|
|
17802
|
+
}
|
|
17803
|
+
|
|
17227
17804
|
fn walk_expr_for_module_const_disqualifiers(e: &Expr, g: &str, bad: &mut bool) {
|
|
17228
17805
|
if *bad {
|
|
17229
17806
|
return;
|
|
@@ -17281,9 +17858,51 @@ impl Codegen {
|
|
|
17281
17858
|
callee.as_ref(),
|
|
17282
17859
|
Expr::Ident { name, .. } if name.as_ref() == "cumulative"
|
|
17283
17860
|
);
|
|
17861
|
+
// A METHOD CALL ON THE ARRAY IS SUPPORTED — but only the ones that go through the
|
|
17862
|
+
// `Value` bridge. `X.join(", ")` emits the object as
|
|
17863
|
+
// `NumArrayBacking::Packed(G_X…)`, so the local name does not survive and the hoist
|
|
17864
|
+
// is fine (tests/core/template_literals.tish).
|
|
17865
|
+
//
|
|
17866
|
+
// THE FUSEABLE HOFs ARE NOT. `X.map(...)`, `X.reduce(...)` and their siblings lower
|
|
17867
|
+
// to a NATIVE iterator chain — `xs.iter().copied().map(...)` — which names the local
|
|
17868
|
+
// directly and is NOT rewritten to the static. Exempting them hoisted an array whose
|
|
17869
|
+
// every use then referenced a binding that no longer existed:
|
|
17870
|
+
//
|
|
17871
|
+
// let mut sum: f64 = { … for b in xs.iter().copied() { … } … };
|
|
17872
|
+
// error[E0425]: cannot find value `xs` in this scope
|
|
17873
|
+
//
|
|
17874
|
+
// Caught by the perf gauntlet's `typed_array_hof` fixture on the NATIVE target —
|
|
17875
|
+
// this walker is not GBA-gated, so a too-permissive exemption breaks the host.
|
|
17876
|
+
//
|
|
17877
|
+
// Nor does the exemption extend to a member READ such as `X.length`, which lowers to
|
|
17878
|
+
// a bare `X.len()` — one of the two shapes that broke the ROMs.
|
|
17879
|
+
// ALLOWLIST, NOT DENYLIST. This started as "any method call except the fuseable
|
|
17880
|
+
// HOFs", which is allow-by-default — and every MUTATING method walked straight
|
|
17881
|
+
// through it. `arr2.unshift(1, 2)` hoisted its receiver into a `const` and then
|
|
17882
|
+
// tried to mutate it, so the local vanished and rustc reported
|
|
17883
|
+
// `cannot find value arr2` (tests/core/array_methods.tish, under
|
|
17884
|
+
// TISH_PACKED_ARRAYS=1). `push`/`pop`/`splice`/`sort`/`reverse`/`fill` are the same
|
|
17885
|
+
// shape.
|
|
17886
|
+
//
|
|
17887
|
+
// So only methods PROVEN to route the receiver through the `Value` bridge are
|
|
17888
|
+
// exempt. `join` is the one this pass actually needs
|
|
17889
|
+
// (tests/core/template_literals.tish emits
|
|
17890
|
+
// `array_join(&Value::NumberArray(…Packed(G_NUMS…)))`, where the local name does not
|
|
17891
|
+
// survive). Anything else — mutators, the HOFs that lower to a native iterator chain
|
|
17892
|
+
// over the bare local, and any method added later — disqualifies the hoist, which is
|
|
17893
|
+
// the safe direction: it costs an optimisation, not a build.
|
|
17894
|
+
let member_call_on_g = matches!(
|
|
17895
|
+
callee.as_ref(),
|
|
17896
|
+
Expr::Member { object, prop, .. }
|
|
17897
|
+
if matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == g)
|
|
17898
|
+
&& matches!(
|
|
17899
|
+
prop,
|
|
17900
|
+
MemberProp::Name { name: m, .. } if m.as_ref() == "join"
|
|
17901
|
+
)
|
|
17902
|
+
);
|
|
17284
17903
|
if matches!(callee.as_ref(), Expr::Ident { name, .. } if name.as_ref() == g) {
|
|
17285
17904
|
*bad = true;
|
|
17286
|
-
} else {
|
|
17905
|
+
} else if !member_call_on_g {
|
|
17287
17906
|
Self::walk_expr_for_module_const_disqualifiers(callee, g, bad);
|
|
17288
17907
|
}
|
|
17289
17908
|
for (i, a) in args.iter().enumerate() {
|
|
@@ -17334,7 +17953,75 @@ impl Codegen {
|
|
|
17334
17953
|
}
|
|
17335
17954
|
}
|
|
17336
17955
|
}
|
|
17337
|
-
|
|
17956
|
+
// Everything below was missing, and the omission is what made this pass unsafe: an
|
|
17957
|
+
// admission check whose walker reports "no disqualifying use" for a shape it does not
|
|
17958
|
+
// recognise is silently permissive. `ArrowFunction` was the expensive one — every use
|
|
17959
|
+
// inside a CLOSURE landed in `_ => {}`, so an array a closure writes got hoisted to a
|
|
17960
|
+
// `const` anyway. Reads then renamed to `G_X` while the write kept the boxed local, and
|
|
17961
|
+
// that local is declared after the closure: rustc reports `cannot find value` on a name
|
|
17962
|
+
// the game never misspelled (card-gba queensblood/triad/gwent; tish-gba hyrule's
|
|
17963
|
+
// `rangLive`). Renaming the write would not have been a fix either — a `const` cannot be
|
|
17964
|
+
// assigned through, so a written array must not be hoisted at all. The sibling scalar
|
|
17965
|
+
// walker has always had these arms; this one is a copy that lost them.
|
|
17966
|
+
//
|
|
17967
|
+
// Indexing stays deliberately absent from the disqualifier list: `X[i]` is the whole
|
|
17968
|
+
// point of the hoist, so descending here must not become "any closure mention kills it",
|
|
17969
|
+
// which would give back the #594 reachability this pass exists to provide.
|
|
17970
|
+
Expr::ArrowFunction { body, .. } => match body {
|
|
17971
|
+
ArrowBody::Expr(e) => Self::walk_expr_for_module_const_disqualifiers(e, g, bad),
|
|
17972
|
+
ArrowBody::Block(s) => Self::walk_stmt_for_module_const_disqualifiers(s, g, bad),
|
|
17973
|
+
},
|
|
17974
|
+
// A write to a member of the const, by the same rule the `Member` arm above uses.
|
|
17975
|
+
Expr::MemberAssign { object, value, .. } => {
|
|
17976
|
+
if matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == g) {
|
|
17977
|
+
*bad = true;
|
|
17978
|
+
} else {
|
|
17979
|
+
Self::walk_expr_for_module_const_disqualifiers(object, g, bad);
|
|
17980
|
+
}
|
|
17981
|
+
Self::walk_expr_for_module_const_disqualifiers(value, g, bad);
|
|
17982
|
+
}
|
|
17983
|
+
Expr::Delete { target, .. }
|
|
17984
|
+
if matches!(target.as_ref(), Expr::Ident { name, .. } if name.as_ref() == g) =>
|
|
17985
|
+
{
|
|
17986
|
+
*bad = true;
|
|
17987
|
+
}
|
|
17988
|
+
// The remaining shapes need no special rule: a bare mention of the name is already
|
|
17989
|
+
// disqualifying via the `Ident` arm, so plain descent is sufficient.
|
|
17990
|
+
Expr::New { callee, args, .. } => {
|
|
17991
|
+
Self::walk_expr_for_module_const_disqualifiers(callee, g, bad);
|
|
17992
|
+
for a in args {
|
|
17993
|
+
if let CallArg::Expr(e) | CallArg::Spread(e) = a {
|
|
17994
|
+
Self::walk_expr_for_module_const_disqualifiers(e, g, bad);
|
|
17995
|
+
}
|
|
17996
|
+
}
|
|
17997
|
+
}
|
|
17998
|
+
Expr::NullishCoalesce { left, right, .. } => {
|
|
17999
|
+
Self::walk_expr_for_module_const_disqualifiers(left, g, bad);
|
|
18000
|
+
Self::walk_expr_for_module_const_disqualifiers(right, g, bad);
|
|
18001
|
+
}
|
|
18002
|
+
Expr::TypeOf { operand, .. } => {
|
|
18003
|
+
Self::walk_expr_for_module_const_disqualifiers(operand, g, bad);
|
|
18004
|
+
}
|
|
18005
|
+
Expr::TemplateLiteral { exprs, .. } => {
|
|
18006
|
+
for e in exprs {
|
|
18007
|
+
Self::walk_expr_for_module_const_disqualifiers(e, g, bad);
|
|
18008
|
+
}
|
|
18009
|
+
}
|
|
18010
|
+
// Leaves, listed explicitly so they do NOT reach the fallback below. A literal cannot
|
|
18011
|
+
// mention a binding, and there is one in nearly every expression in a real program —
|
|
18012
|
+
// routing those through a whole-subtree ident scan turned this walk superlinear and hung
|
|
18013
|
+
// the compiler outright (a `tish build` of `scii` spun at 100% CPU for 40+ minutes in the
|
|
18014
|
+
// frontend, no rustc involved). The fallback is for shapes that might CONTAIN a
|
|
18015
|
+
// reference, not for every constant in the file.
|
|
18016
|
+
Expr::Literal { .. } => {}
|
|
18017
|
+
// CONSERVATIVE DEFAULT — see the statement walker. An expression shape this match does
|
|
18018
|
+
// not model (today: `Await`, `JsxElement`, and anything the AST gains later) disqualifies
|
|
18019
|
+
// if the name appears anywhere beneath it, rather than being waved through.
|
|
18020
|
+
other => {
|
|
18021
|
+
if Self::expr_mentions_ident(other, g) {
|
|
18022
|
+
*bad = true;
|
|
18023
|
+
}
|
|
18024
|
+
}
|
|
17338
18025
|
}
|
|
17339
18026
|
}
|
|
17340
18027
|
|
|
@@ -17518,15 +18205,41 @@ impl Codegen {
|
|
|
17518
18205
|
)
|
|
17519
18206
|
}
|
|
17520
18207
|
|
|
17521
|
-
|
|
18208
|
+
/// The boxed bridge. `NumArrayBacking::Packed` is a `Vec<f64>`, so an `[i32; N]` hoist widens on
|
|
18209
|
+
/// the way out — this is the escape hatch where the array is handed to code that wants a
|
|
18210
|
+
/// `Value`, and it is a whole-array copy either way.
|
|
18211
|
+
fn module_const_f64_array_as_value(&self, static_name: &str) -> String {
|
|
18212
|
+
let elems = if self.module_const_int_statics.contains(static_name) {
|
|
18213
|
+
format!("{}.iter().map(|v| *v as f64)", static_name)
|
|
18214
|
+
} else {
|
|
18215
|
+
format!("{}.iter().copied()", static_name)
|
|
18216
|
+
};
|
|
17522
18217
|
format!(
|
|
17523
|
-
"Value::NumberArray(VmRef::new(tishlang_runtime::NumArrayBacking::Packed({}.
|
|
17524
|
-
|
|
18218
|
+
"Value::NumberArray(VmRef::new(tishlang_runtime::NumArrayBacking::Packed({}.collect::<Vec<f64>>())))",
|
|
18219
|
+
elems
|
|
17525
18220
|
)
|
|
17526
18221
|
}
|
|
17527
18222
|
|
|
17528
18223
|
fn emit_module_const_f64_arrays(&mut self) -> Result<(), CompileError> {
|
|
17529
18224
|
for (name, vals) in self.module_const_f64_arrays.clone() {
|
|
18225
|
+
let static_name = Self::module_const_static(name.as_str());
|
|
18226
|
+
// `[i32; N]` where the values permit it — half the ROM, and reads that stay in the
|
|
18227
|
+
// integer domain instead of round-tripping through soft float. See the field comment on
|
|
18228
|
+
// `module_const_int_statics`.
|
|
18229
|
+
if self.module_const_int_statics.contains(&static_name) {
|
|
18230
|
+
let lit = vals
|
|
18231
|
+
.iter()
|
|
18232
|
+
.map(|v| format!("{}", *v as i32))
|
|
18233
|
+
.collect::<Vec<_>>()
|
|
18234
|
+
.join(", ");
|
|
18235
|
+
self.writeln(&format!(
|
|
18236
|
+
"const {}: [i32; {}] = [{}];",
|
|
18237
|
+
static_name,
|
|
18238
|
+
vals.len(),
|
|
18239
|
+
lit
|
|
18240
|
+
));
|
|
18241
|
+
continue;
|
|
18242
|
+
}
|
|
17530
18243
|
let lit = vals
|
|
17531
18244
|
.iter()
|
|
17532
18245
|
.map(|v| Self::f64_lit(*v))
|
|
@@ -17534,7 +18247,7 @@ impl Codegen {
|
|
|
17534
18247
|
.join(", ");
|
|
17535
18248
|
self.writeln(&format!(
|
|
17536
18249
|
"const {}: [f64; {}] = [{}];",
|
|
17537
|
-
|
|
18250
|
+
static_name,
|
|
17538
18251
|
vals.len(),
|
|
17539
18252
|
lit
|
|
17540
18253
|
));
|
|
@@ -17938,35 +18651,74 @@ impl Codegen {
|
|
|
17938
18651
|
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
17939
18652
|
Self::walk_stmts_for_global_disqualifiers(statements, g, bad);
|
|
17940
18653
|
}
|
|
18654
|
+
// Conditions are visited, not just bodies. This walker had the same blind spot the
|
|
18655
|
+
// module-const one did: `while (i < X.length)` put a disqualifying use somewhere the
|
|
18656
|
+
// walk never reached, so the global was hoisted anyway and the emitted program
|
|
18657
|
+
// referenced a name that no longer existed. Latent here only because a scalar is rarely
|
|
18658
|
+
// spelled that way — the defect is identical.
|
|
17941
18659
|
Statement::If {
|
|
18660
|
+
cond,
|
|
17942
18661
|
then_branch,
|
|
17943
18662
|
else_branch,
|
|
17944
18663
|
..
|
|
17945
18664
|
} => {
|
|
18665
|
+
Self::walk_expr_for_global_disqualifiers(cond, g, bad);
|
|
17946
18666
|
Self::walk_stmt_for_global_disqualifiers(then_branch, g, bad);
|
|
17947
18667
|
if let Some(eb) = else_branch {
|
|
17948
18668
|
Self::walk_stmt_for_global_disqualifiers(eb, g, bad);
|
|
17949
18669
|
}
|
|
17950
18670
|
}
|
|
17951
|
-
Statement::While { body, .. }
|
|
17952
|
-
|
|
17953
|
-
|
|
17954
|
-
|
|
17955
|
-
|
|
18671
|
+
Statement::While { cond, body, .. } | Statement::DoWhile { body, cond, .. } => {
|
|
18672
|
+
Self::walk_expr_for_global_disqualifiers(cond, g, bad);
|
|
18673
|
+
Self::walk_stmt_for_global_disqualifiers(body, g, bad);
|
|
18674
|
+
}
|
|
18675
|
+
Statement::For {
|
|
18676
|
+
init,
|
|
18677
|
+
cond,
|
|
18678
|
+
update,
|
|
18679
|
+
body,
|
|
18680
|
+
..
|
|
18681
|
+
} => {
|
|
18682
|
+
if let Some(i) = init {
|
|
18683
|
+
Self::walk_stmt_for_global_disqualifiers(i, g, bad);
|
|
18684
|
+
}
|
|
18685
|
+
if let Some(c) = cond {
|
|
18686
|
+
Self::walk_expr_for_global_disqualifiers(c, g, bad);
|
|
18687
|
+
}
|
|
18688
|
+
if let Some(u) = update {
|
|
18689
|
+
Self::walk_expr_for_global_disqualifiers(u, g, bad);
|
|
18690
|
+
}
|
|
18691
|
+
Self::walk_stmt_for_global_disqualifiers(body, g, bad);
|
|
18692
|
+
}
|
|
18693
|
+
Statement::ForOf { iterable, body, .. }
|
|
18694
|
+
| Statement::ForIn {
|
|
18695
|
+
object: iterable,
|
|
18696
|
+
body,
|
|
18697
|
+
..
|
|
18698
|
+
} => {
|
|
18699
|
+
Self::walk_expr_for_global_disqualifiers(iterable, g, bad);
|
|
17956
18700
|
Self::walk_stmt_for_global_disqualifiers(body, g, bad);
|
|
17957
18701
|
}
|
|
17958
18702
|
Statement::Switch {
|
|
18703
|
+
expr,
|
|
17959
18704
|
cases,
|
|
17960
18705
|
default_body,
|
|
17961
18706
|
..
|
|
17962
18707
|
} => {
|
|
17963
|
-
|
|
18708
|
+
Self::walk_expr_for_global_disqualifiers(expr, g, bad);
|
|
18709
|
+
for (case_expr, stmts) in cases {
|
|
18710
|
+
if let Some(e) = case_expr {
|
|
18711
|
+
Self::walk_expr_for_global_disqualifiers(e, g, bad);
|
|
18712
|
+
}
|
|
17964
18713
|
Self::walk_stmts_for_global_disqualifiers(stmts, g, bad);
|
|
17965
18714
|
}
|
|
17966
18715
|
if let Some(d) = default_body {
|
|
17967
18716
|
Self::walk_stmts_for_global_disqualifiers(d, g, bad);
|
|
17968
18717
|
}
|
|
17969
18718
|
}
|
|
18719
|
+
Statement::Throw { value, .. } => {
|
|
18720
|
+
Self::walk_expr_for_global_disqualifiers(value, g, bad);
|
|
18721
|
+
}
|
|
17970
18722
|
Statement::Try {
|
|
17971
18723
|
body,
|
|
17972
18724
|
catch_body,
|
|
@@ -18165,6 +18917,151 @@ impl Codegen {
|
|
|
18165
18917
|
ok
|
|
18166
18918
|
}
|
|
18167
18919
|
|
|
18920
|
+
/// #597: every `let x: T[]` name in the program, at any nesting depth. These are the candidates
|
|
18921
|
+
/// whose escape has to be checked before they may lower to a native `Vec`.
|
|
18922
|
+
fn collect_annotated_array_names(stmts: &[Statement], out: &mut Vec<String>) {
|
|
18923
|
+
for s in stmts {
|
|
18924
|
+
Self::collect_annotated_array_names_stmt(s, out);
|
|
18925
|
+
}
|
|
18926
|
+
}
|
|
18927
|
+
|
|
18928
|
+
fn collect_annotated_array_names_stmt(s: &Statement, out: &mut Vec<String>) {
|
|
18929
|
+
match s {
|
|
18930
|
+
Statement::VarDecl {
|
|
18931
|
+
name,
|
|
18932
|
+
type_ann: Some(t),
|
|
18933
|
+
..
|
|
18934
|
+
} => {
|
|
18935
|
+
if matches!(crate::types::RustType::from_annotation(t), RustType::Vec(_)) {
|
|
18936
|
+
out.push(name.to_string());
|
|
18937
|
+
}
|
|
18938
|
+
}
|
|
18939
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
18940
|
+
for s in statements {
|
|
18941
|
+
Self::collect_annotated_array_names_stmt(s, out);
|
|
18942
|
+
}
|
|
18943
|
+
}
|
|
18944
|
+
Statement::If {
|
|
18945
|
+
then_branch,
|
|
18946
|
+
else_branch,
|
|
18947
|
+
..
|
|
18948
|
+
} => {
|
|
18949
|
+
Self::collect_annotated_array_names_stmt(then_branch, out);
|
|
18950
|
+
if let Some(e) = else_branch {
|
|
18951
|
+
Self::collect_annotated_array_names_stmt(e, out);
|
|
18952
|
+
}
|
|
18953
|
+
}
|
|
18954
|
+
Statement::While { body, .. }
|
|
18955
|
+
| Statement::DoWhile { body, .. }
|
|
18956
|
+
| Statement::ForOf { body, .. }
|
|
18957
|
+
| Statement::ForIn { body, .. }
|
|
18958
|
+
| Statement::FunDecl { body, .. } => {
|
|
18959
|
+
Self::collect_annotated_array_names_stmt(body, out);
|
|
18960
|
+
}
|
|
18961
|
+
Statement::For { init, body, .. } => {
|
|
18962
|
+
if let Some(i) = init {
|
|
18963
|
+
Self::collect_annotated_array_names_stmt(i, out);
|
|
18964
|
+
}
|
|
18965
|
+
Self::collect_annotated_array_names_stmt(body, out);
|
|
18966
|
+
}
|
|
18967
|
+
Statement::Switch {
|
|
18968
|
+
cases,
|
|
18969
|
+
default_body,
|
|
18970
|
+
..
|
|
18971
|
+
} => {
|
|
18972
|
+
for (_, stmts) in cases {
|
|
18973
|
+
for s in stmts {
|
|
18974
|
+
Self::collect_annotated_array_names_stmt(s, out);
|
|
18975
|
+
}
|
|
18976
|
+
}
|
|
18977
|
+
if let Some(stmts) = default_body {
|
|
18978
|
+
for s in stmts {
|
|
18979
|
+
Self::collect_annotated_array_names_stmt(s, out);
|
|
18980
|
+
}
|
|
18981
|
+
}
|
|
18982
|
+
}
|
|
18983
|
+
Statement::Try {
|
|
18984
|
+
body,
|
|
18985
|
+
catch_body,
|
|
18986
|
+
finally_body,
|
|
18987
|
+
..
|
|
18988
|
+
} => {
|
|
18989
|
+
Self::collect_annotated_array_names_stmt(body, out);
|
|
18990
|
+
if let Some(c) = catch_body {
|
|
18991
|
+
Self::collect_annotated_array_names_stmt(c, out);
|
|
18992
|
+
}
|
|
18993
|
+
if let Some(f) = finally_body {
|
|
18994
|
+
Self::collect_annotated_array_names_stmt(f, out);
|
|
18995
|
+
}
|
|
18996
|
+
}
|
|
18997
|
+
_ => {}
|
|
18998
|
+
}
|
|
18999
|
+
}
|
|
19000
|
+
|
|
19001
|
+
/// #597: array names that ESCAPE — handed to a call as a bare argument, or otherwise used as a
|
|
19002
|
+
/// whole value rather than indexed.
|
|
19003
|
+
///
|
|
19004
|
+
/// Reuses `scan_param_use`, the same analysis the read-only param path already trusts for this
|
|
19005
|
+
/// exact hazard: its `forwarded` flag is documented as "the callee could mutate the array, and a
|
|
19006
|
+
/// write to the caller's array would be lost on the owned copy". That is #597 from the other
|
|
19007
|
+
/// side, so the answer should come from one analysis rather than a second, subtly-different one.
|
|
19008
|
+
///
|
|
19009
|
+
/// Over-approximating only costs an array its native lowering. Under-approximating brings back a
|
|
19010
|
+
/// silently wrong result, so the bias is deliberate.
|
|
19011
|
+
fn collect_escaping_array_names(stmts: &[Statement], names: &[String]) -> std::collections::HashSet<String> {
|
|
19012
|
+
// `for_each_stmt_expr` deliberately does NOT descend into `FunDecl` bodies, so the forwarding
|
|
19013
|
+
// call is normally invisible to it — the array is declared inside a function and passed
|
|
19014
|
+
// inside that same function, which is exactly the reported shape. Gather nested bodies as
|
|
19015
|
+
// additional roots first.
|
|
19016
|
+
fn add_roots<'a>(s: &'a Statement, roots: &mut Vec<&'a Statement>) {
|
|
19017
|
+
roots.push(s);
|
|
19018
|
+
match s {
|
|
19019
|
+
Statement::FunDecl { body, .. }
|
|
19020
|
+
| Statement::While { body, .. }
|
|
19021
|
+
| Statement::DoWhile { body, .. }
|
|
19022
|
+
| Statement::ForOf { body, .. }
|
|
19023
|
+
| Statement::ForIn { body, .. }
|
|
19024
|
+
| Statement::For { body, .. } => add_roots(body, roots),
|
|
19025
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
19026
|
+
for s in statements {
|
|
19027
|
+
add_roots(s, roots);
|
|
19028
|
+
}
|
|
19029
|
+
}
|
|
19030
|
+
Statement::If {
|
|
19031
|
+
then_branch,
|
|
19032
|
+
else_branch,
|
|
19033
|
+
..
|
|
19034
|
+
} => {
|
|
19035
|
+
add_roots(then_branch, roots);
|
|
19036
|
+
if let Some(e) = else_branch {
|
|
19037
|
+
add_roots(e, roots);
|
|
19038
|
+
}
|
|
19039
|
+
}
|
|
19040
|
+
_ => {}
|
|
19041
|
+
}
|
|
19042
|
+
}
|
|
19043
|
+
let mut roots: Vec<&Statement> = Vec::new();
|
|
19044
|
+
for s in stmts {
|
|
19045
|
+
add_roots(s, &mut roots);
|
|
19046
|
+
}
|
|
19047
|
+
let mut out = std::collections::HashSet::new();
|
|
19048
|
+
for n in names {
|
|
19049
|
+
let mut f = ParamUse::default();
|
|
19050
|
+
for s in &roots {
|
|
19051
|
+
Self::for_each_stmt_expr(s, &mut |e| Self::scan_param_use(e, n, false, &mut f));
|
|
19052
|
+
}
|
|
19053
|
+
// `forwarded` ONLY, not `escaped`. `escaped` covers any whole-value use — including
|
|
19054
|
+
// `return arr`, which hands ownership out and does not create the aliasing hazard this
|
|
19055
|
+
// is about. Using it deopted arrays that are legitimately native
|
|
19056
|
+
// (`numeric_returning_fn_keeps_pushed_array_native`). #597 is specifically "passed as a
|
|
19057
|
+
// call argument", which is what `forwarded` means.
|
|
19058
|
+
if f.forwarded {
|
|
19059
|
+
out.insert(n.clone());
|
|
19060
|
+
}
|
|
19061
|
+
}
|
|
19062
|
+
out
|
|
19063
|
+
}
|
|
19064
|
+
|
|
18168
19065
|
fn for_each_expr_root(stmts: &[Statement], f: &mut dyn FnMut(&Expr)) {
|
|
18169
19066
|
for s in stmts {
|
|
18170
19067
|
Self::for_each_expr_root_stmt(s, f);
|
|
@@ -19542,16 +20439,27 @@ impl Codegen {
|
|
|
19542
20439
|
matches!(op, UnaryOp::Neg | UnaryOp::Pos)
|
|
19543
20440
|
&& Self::inline_body_numeric(operand, params, self_name)
|
|
19544
20441
|
}
|
|
19545
|
-
|
|
19546
|
-
|
|
19547
|
-
|
|
19548
|
-
|
|
19549
|
-
|
|
19550
|
-
|
|
19551
|
-
|
|
19552
|
-
|
|
19553
|
-
|
|
19554
|
-
|
|
20442
|
+
// #637: A TERNARY IS NEVER INLINED. The substitution that rewrites a parameter to its
|
|
20443
|
+
// `_inl<N>_<param>` temporary reaches the condition but NOT the branches, so an inlined
|
|
20444
|
+
// ternary emits branches naming parameters that do not exist in the generated scope:
|
|
20445
|
+
//
|
|
20446
|
+
// if Value::Bool((_inl0_a > _inl0_b)).is_truthy() { a } else { b }
|
|
20447
|
+
// ^^^^^^^ substituted ^ ^ not substituted
|
|
20448
|
+
// error[E0425]: cannot find value `a` in this scope
|
|
20449
|
+
//
|
|
20450
|
+
// The set this arm used to admit was exactly the broken one. `inline_body_numeric`
|
|
20451
|
+
// accepts a bare param, so `flag ? 10 : 20` was inlined — and since the inliner binds
|
|
20452
|
+
// every param as `f64`, the emitter then asked an `f64` for `.is_truthy()`
|
|
20453
|
+
// (`error[E0614]: type f64 cannot be dereferenced`). A comparison cond such as
|
|
20454
|
+
// `a > b ? a : b` was never admitted here, because `Gt` is not in the arithmetic op list
|
|
20455
|
+
// above — which is why that shape compiled fine and only truthiness tests broke.
|
|
20456
|
+
//
|
|
20457
|
+
// So refusing outright REMOVES no working inline: every ternary this arm ever accepted
|
|
20458
|
+
// produced code that did not build from the second call site onward. Widening it instead
|
|
20459
|
+
// (admitting comparison conds) is what surfaced the branch-substitution gap, and fixing
|
|
20460
|
+
// that belongs with the substitution machinery, not here. Ternaries fall back to the
|
|
20461
|
+
// ordinary call: correct, and merely un-inlined.
|
|
20462
|
+
Expr::Conditional { .. } => false,
|
|
19555
20463
|
Expr::Call { callee, args, .. } => {
|
|
19556
20464
|
// Only 1-arg `Math.<intrinsic>` — never another fn (incl. self), to keep it a pure
|
|
19557
20465
|
// expression with no dispatch and no recursion.
|
|
@@ -20084,6 +20992,14 @@ impl Codegen {
|
|
|
20084
20992
|
}
|
|
20085
20993
|
}
|
|
20086
20994
|
}
|
|
20995
|
+
// #630: A NAME REMOVED HERE IS REJECTED FOR GOOD. `collect_bad_vec_callsites` is a
|
|
20996
|
+
// SOUNDNESS verdict — the fn is called somewhere in a way the native-vec ABI cannot
|
|
20997
|
+
// represent — and that fact is a property of the program, which does not change during this
|
|
20998
|
+
// analysis. Re-admitting it later can only produce the same wrong answer.
|
|
20999
|
+
//
|
|
21000
|
+
// Recording it is also what makes the forwarding fixpoint below terminate; see the note
|
|
21001
|
+
// there.
|
|
21002
|
+
let mut rejected: std::collections::HashSet<String> = std::collections::HashSet::new();
|
|
20087
21003
|
loop {
|
|
20088
21004
|
let mut remove: Vec<String> = Vec::new();
|
|
20089
21005
|
for s in &program.statements {
|
|
@@ -20095,6 +21011,7 @@ impl Codegen {
|
|
|
20095
21011
|
break;
|
|
20096
21012
|
}
|
|
20097
21013
|
for n in remove {
|
|
21014
|
+
rejected.insert(n.clone());
|
|
20098
21015
|
sigs.remove(&n);
|
|
20099
21016
|
}
|
|
20100
21017
|
if sigs.is_empty() {
|
|
@@ -20130,6 +21047,22 @@ impl Codegen {
|
|
|
20130
21047
|
if Self::ann_is_simple(return_type.as_ref(), "fixed") {
|
|
20131
21048
|
continue;
|
|
20132
21049
|
}
|
|
21050
|
+
// #630: AND IT MUST NOT RE-ADD A NAME THE REMOVAL PASS ALREADY REJECTED. This is
|
|
21051
|
+
// what makes the analysis terminate. Without it the two fixpoints fight: the
|
|
21052
|
+
// forwarding pass admits `F`, the removal pass below proves `F` unsound and
|
|
21053
|
+
// drops it, `added` is true so the outer loop runs again, and `F` — re-evaluated
|
|
21054
|
+
// against a program that has not changed — is admitted again. Forever.
|
|
21055
|
+
//
|
|
21056
|
+
// The only exits were `!added` and `sigs.is_empty()`, and an oscillating pair
|
|
21057
|
+
// satisfies neither: measured on the reporter's project it sat at a steady
|
|
21058
|
+
// `sigs=3` for 200+ iterations at 100% CPU, with no cargo child and nothing
|
|
21059
|
+
// printed, which is why it read as a dead build rather than a slow one.
|
|
21060
|
+
//
|
|
21061
|
+
// With this guard every outer iteration must admit a name never admitted before,
|
|
21062
|
+
// so the loop is bounded by the number of top-level fns.
|
|
21063
|
+
if rejected.contains(name.as_ref()) {
|
|
21064
|
+
continue;
|
|
21065
|
+
}
|
|
20133
21066
|
if let Some(sig) = Self::infer_vec_fn_sig_forwarding(params, body, &sigs) {
|
|
20134
21067
|
let has_array_param = sig
|
|
20135
21068
|
.params
|
|
@@ -20156,6 +21089,7 @@ impl Codegen {
|
|
|
20156
21089
|
break;
|
|
20157
21090
|
}
|
|
20158
21091
|
for n in remove {
|
|
21092
|
+
rejected.insert(n.clone());
|
|
20159
21093
|
sigs.remove(&n);
|
|
20160
21094
|
}
|
|
20161
21095
|
if sigs.is_empty() {
|
|
@@ -20704,7 +21638,23 @@ impl Codegen {
|
|
|
20704
21638
|
//
|
|
20705
21639
|
// This does not close #594's other half — a lowered fn that can actually SEE module state,
|
|
20706
21640
|
// which needs those bindings emitted as statics on GBA too. It stops the miscompile.
|
|
20707
|
-
|
|
21641
|
+
// #633: THE SAME HOLE EXISTS OFF-GBA. The scoping above reasoned that off-target "the
|
|
21642
|
+
// statics DO exist" — true only for what those passes actually cover: numeric arrays and
|
|
21643
|
+
// numeric globals. A module binding of any OTHER shape (an array of ARRAYS, a string, an
|
|
21644
|
+
// object) gets no top-level home on ANY target, stays a `let` in `run()`, and is just as
|
|
21645
|
+
// unreachable from a free `fn name_nv` on host as on GBA:
|
|
21646
|
+
//
|
|
21647
|
+
// let COLS = [[1,2,3],[4,5]] // array of arrays → no static
|
|
21648
|
+
// function widths(){ let o=[]; …COLS[i].length…; return o }
|
|
21649
|
+
// widths(); widths() // ≥2 call sites → not inlined
|
|
21650
|
+
//
|
|
21651
|
+
// compiles to `error[E0425]: cannot find value COLS in this scope`. One call site hides it
|
|
21652
|
+
// (the fn is inlined and never lowered), which is why this surfaced as a cross-module
|
|
21653
|
+
// ordering bug in a large program rather than as the two-line case it actually is.
|
|
21654
|
+
//
|
|
21655
|
+
// The carve-out is what differs per target, not the reachability rule — so run the rejection
|
|
21656
|
+
// everywhere and let the per-target `remove` list below decide what still has a home.
|
|
21657
|
+
{
|
|
20708
21658
|
let mut module_bindings = Self::collect_module_level_binding_names(&program.statements);
|
|
20709
21659
|
// …EXCEPT the ones that now have a top-level home. Since #594 a module const numeric
|
|
20710
21660
|
// array is a `const` and a mutable numeric global is a `SingleCore` static, both
|
|
@@ -24680,6 +25630,21 @@ impl Codegen {
|
|
|
24680
25630
|
// in (-2^53, 2^53) and `c` a positive integer literal → `(x as i64) % c` instead
|
|
24681
25631
|
// of `fmod`. Bit-identical (x is exactly an integer in f64; Rust `%` and `fmod`
|
|
24682
25632
|
// both truncate toward zero), and far faster — the fmod in LCG/hash recurrences.
|
|
25633
|
+
// #606: a mixed `fixed`/`f64` pair types as `Fixed` (or `Bool` for a
|
|
25634
|
+
// comparison), so the f64 side must be LIFTED before the operator — agb's
|
|
25635
|
+
// `Num<i32,8>` has no `f64` overload. `coerce_native_arg` owns the scaling, so
|
|
25636
|
+
// a value crossing here reads identically to one crossing a Value boundary or
|
|
25637
|
+
// the literal fold.
|
|
25638
|
+
let (l, r) = if lt == RustType::Fixed && rt == RustType::F64 {
|
|
25639
|
+
let rc = self.coerce_native_arg(&r, RustType::F64, RustType::Fixed);
|
|
25640
|
+
(l, rc)
|
|
25641
|
+
} else if lt == RustType::F64 && rt == RustType::Fixed {
|
|
25642
|
+
let lc = self.coerce_native_arg(&l, RustType::F64, RustType::Fixed);
|
|
25643
|
+
(lc, r)
|
|
25644
|
+
} else {
|
|
25645
|
+
(l, r)
|
|
25646
|
+
};
|
|
25647
|
+
|
|
24683
25648
|
if matches!(op, BinOp::Mod) && result_ty == RustType::F64 {
|
|
24684
25649
|
if let Some(c) = Self::int_literal_value_of(right).filter(|&c| c > 0) {
|
|
24685
25650
|
if self.int_range(left, &self.int_range_locals).is_some() {
|
|
@@ -24848,47 +25813,56 @@ impl Codegen {
|
|
|
24848
25813
|
name.as_ref(),
|
|
24849
25814
|
) {
|
|
24850
25815
|
let in_bounds = self.index_in_bounds(index, name.as_ref());
|
|
24851
|
-
|
|
24852
|
-
|
|
24853
|
-
|
|
25816
|
+
// #645: use the SHARED index lowering. This site used to accept only
|
|
25817
|
+
// `F64` natively and push everything else — including a perfectly good
|
|
25818
|
+
// integer expression — through `Value::Number(..)` and back, or through
|
|
25819
|
+
// an `i32 -> f64 -> usize` cast. On a target with no FPU that cast is a
|
|
25820
|
+
// soft-float call PER ELEMENT, which is most of what made a promoted
|
|
25821
|
+
// array's read expensive. `emit_index_usize` already has the int-domain
|
|
25822
|
+
// shortcut (`(int).max(0) as usize`, where `.max(0)` reproduces f64's
|
|
25823
|
+
// saturate-to-zero for a negative index).
|
|
25824
|
+
let idx_usize = self.emit_index_usize(index)?;
|
|
25825
|
+
let is_int = self.module_const_int_statics.contains(&static_name);
|
|
25826
|
+
// The proven-in-bounds read on an integer array is the whole point: a
|
|
25827
|
+
// plain word load, reported as I32 so the consumer stays in the integer
|
|
25828
|
+
// domain instead of converting back and forth through f64.
|
|
25829
|
+
if in_bounds {
|
|
25830
|
+
let access = format!("{}[{}]", static_name, idx_usize);
|
|
25831
|
+
return Ok((
|
|
25832
|
+
access,
|
|
25833
|
+
if is_int { RustType::I32 } else { RustType::F64 },
|
|
25834
|
+
));
|
|
25835
|
+
}
|
|
25836
|
+
let const_len = self
|
|
25837
|
+
.cum_static_and_len(name.as_ref())
|
|
25838
|
+
.map(|(_, n)| n)
|
|
25839
|
+
.or_else(|| {
|
|
25840
|
+
self.module_const_f64_arrays
|
|
25841
|
+
.get(name.as_ref())
|
|
25842
|
+
.map(|v| v.len())
|
|
25843
|
+
})
|
|
25844
|
+
.unwrap_or(0);
|
|
25845
|
+
// OUT-OF-RANGE STAYS f64. An unproven index still has to be able to
|
|
25846
|
+
// answer `NaN`, and there is no `i32` that means "absent" — picking a
|
|
25847
|
+
// sentinel here would silently turn a missing element into a real value.
|
|
25848
|
+
// So the bounds-checked form keeps today's exact semantics and only the
|
|
25849
|
+
// STORAGE narrows; the integer win is claimed above, where the index is
|
|
25850
|
+
// proven and no sentinel is reachable.
|
|
25851
|
+
let elem = if is_int {
|
|
25852
|
+
format!("{}[_i] as f64", static_name)
|
|
24854
25853
|
} else {
|
|
24855
|
-
|
|
24856
|
-
|
|
24857
|
-
|
|
24858
|
-
idx_code
|
|
24859
|
-
};
|
|
25854
|
+
format!("{}[_i]", static_name)
|
|
25855
|
+
};
|
|
25856
|
+
let access = if const_len > 0 {
|
|
24860
25857
|
format!(
|
|
24861
|
-
"{{ let _i =
|
|
24862
|
-
|
|
25858
|
+
"{{ let _i = {}; if _i < {} {{ {} }} else {{ f64::NAN }} }}",
|
|
25859
|
+
idx_usize, const_len, elem
|
|
24863
25860
|
)
|
|
24864
|
-
};
|
|
24865
|
-
let access = if in_bounds {
|
|
24866
|
-
format!("{}[{}]", static_name, idx_usize)
|
|
24867
25861
|
} else {
|
|
24868
|
-
|
|
24869
|
-
.
|
|
24870
|
-
|
|
24871
|
-
|
|
24872
|
-
self.module_const_f64_arrays
|
|
24873
|
-
.get(name.as_ref())
|
|
24874
|
-
.map(|v| v.len())
|
|
24875
|
-
})
|
|
24876
|
-
.unwrap_or(0);
|
|
24877
|
-
if const_len > 0 {
|
|
24878
|
-
format!(
|
|
24879
|
-
"{{ let _i = {}; if _i < {} {{ {}[_i] }} else {{ f64::NAN }} }}",
|
|
24880
|
-
idx_usize,
|
|
24881
|
-
const_len,
|
|
24882
|
-
static_name
|
|
24883
|
-
)
|
|
24884
|
-
} else {
|
|
24885
|
-
format!(
|
|
24886
|
-
"{{ let _i = {}; if _i < {}.len() {{ {}[_i] }} else {{ f64::NAN }} }}",
|
|
24887
|
-
idx_usize,
|
|
24888
|
-
static_name,
|
|
24889
|
-
static_name
|
|
24890
|
-
)
|
|
24891
|
-
}
|
|
25862
|
+
format!(
|
|
25863
|
+
"{{ let _i = {}; if _i < {}.len() {{ {} }} else {{ f64::NAN }} }}",
|
|
25864
|
+
idx_usize, static_name, elem
|
|
25865
|
+
)
|
|
24892
25866
|
};
|
|
24893
25867
|
return Ok((access, RustType::F64));
|
|
24894
25868
|
}
|
|
@@ -24960,6 +25934,19 @@ impl Codegen {
|
|
|
24960
25934
|
// `borrow()` guard a non-Copy element must be cloned OUT (it can't be
|
|
24961
25935
|
// moved from the borrow); a plain local keeps the direct place.
|
|
24962
25936
|
_ if idx_wrapped => format!("{}[{}].clone()", esc_obj, idx_ref),
|
|
25937
|
+
// #600: a STRING element is cloned even unwrapped. `let s: string =
|
|
25938
|
+
// NAMES[i]` binds the element, and binding a non-Copy value out of a
|
|
25939
|
+
// `Vec` is a move — `error[E0507]: cannot move out of index`. The
|
|
25940
|
+
// obvious spelling was the one that did not compile, with indexing
|
|
25941
|
+
// straight into a call as the only working form.
|
|
25942
|
+
//
|
|
25943
|
+
// Scoped to String deliberately. The struct case (#571) looks
|
|
25944
|
+
// identical but must NOT clone here: `states[id].timer` projects a
|
|
25945
|
+
// field out of the place, and cloning would copy the whole struct on
|
|
25946
|
+
// every field read — the O(n)-per-read footgun #558 exists about.
|
|
25947
|
+
// A string is already copied at every boxed boundary, so the clone
|
|
25948
|
+
// costs nothing that was not already being paid.
|
|
25949
|
+
RustType::String => format!("{}[{}].clone()", esc_obj, idx_ref),
|
|
24963
25950
|
_ => format!("{}[{}]", esc_obj, idx_ref),
|
|
24964
25951
|
};
|
|
24965
25952
|
// #567: wrap the wrapped-Vec read in a block that pre-binds the index
|
|
@@ -25097,9 +26084,10 @@ impl Codegen {
|
|
|
25097
26084
|
// dropping to `ops::*`. Only a native-returning extern is taken here; a `void`/`Value`
|
|
25098
26085
|
// one falls through to the boxed path (this is a value-position expression).
|
|
25099
26086
|
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
25100
|
-
if let Some(sig
|
|
26087
|
+
if let Some((sig, suffix)) =
|
|
26088
|
+
self.extern_sig_for_arity(fname.as_ref(), args.len())
|
|
26089
|
+
{
|
|
25101
26090
|
if sig.ret.is_native()
|
|
25102
|
-
&& args.len() == sig.params.len()
|
|
25103
26091
|
&& args.iter().all(|a| matches!(a, CallArg::Expr(_)))
|
|
25104
26092
|
{
|
|
25105
26093
|
let mut argc: Vec<String> = Vec::with_capacity(args.len());
|
|
@@ -25109,9 +26097,10 @@ impl Codegen {
|
|
|
25109
26097
|
}
|
|
25110
26098
|
}
|
|
25111
26099
|
let call = format!(
|
|
25112
|
-
"{}::{}
|
|
26100
|
+
"{}::{}{}({})",
|
|
25113
26101
|
sig.crate_name,
|
|
25114
26102
|
sig.symbol,
|
|
26103
|
+
suffix,
|
|
25115
26104
|
argc.join(", ")
|
|
25116
26105
|
);
|
|
25117
26106
|
return Ok((call, sig.ret));
|
|
@@ -26048,7 +27037,12 @@ impl Codegen {
|
|
|
26048
27037
|
let RustType::Vec(inner) = self.type_context.get_type(recv_name.as_ref()) else {
|
|
26049
27038
|
return Ok(None);
|
|
26050
27039
|
};
|
|
26051
|
-
|
|
27040
|
+
// #609: the element-type gate exists in TWO places — here and at the top of
|
|
27041
|
+
// `try_native_vec_hof`. Widening only the inner one changes nothing, because this one
|
|
27042
|
+
// returns first: the array is boxed whole into a `Value::Array` and handed to
|
|
27043
|
+
// `tishlang_runtime::array_map`, which is the per-element `value_call` path this issue is
|
|
27044
|
+
// about. Both must agree.
|
|
27045
|
+
if *inner != RustType::F64 && *inner != RustType::I32 {
|
|
26052
27046
|
return Ok(None);
|
|
26053
27047
|
}
|
|
26054
27048
|
let recv_code = Self::escape_ident(recv_name.as_ref()).into_owned();
|
|
@@ -26077,8 +27071,14 @@ impl Codegen {
|
|
|
26077
27071
|
method: &str,
|
|
26078
27072
|
args: &[CallArg],
|
|
26079
27073
|
) -> Result<Option<(String, RustType)>, CompileError> {
|
|
26080
|
-
//
|
|
26081
|
-
|
|
27074
|
+
// `.copied()` needs a `Copy` element, which both of these are.
|
|
27075
|
+
//
|
|
27076
|
+
// #609: `i32` joins `f64` here. On GBA every idiomatic array is `i32[]`, so restricting the
|
|
27077
|
+
// fast path to `Vec<f64>` meant `.map/.filter/.some/.every` fell back to one boxed
|
|
27078
|
+
// `value_call` PER ELEMENT on exactly the arrays the target guidance tells you to write.
|
|
27079
|
+
//
|
|
27080
|
+
// `reduce` is deliberately NOT extended — see its arm below.
|
|
27081
|
+
if *elem != RustType::F64 && *elem != RustType::I32 {
|
|
26082
27082
|
return Ok(None);
|
|
26083
27083
|
}
|
|
26084
27084
|
let Some(CallArg::Expr(Expr::ArrowFunction { params, body, .. })) = args.first() else {
|
|
@@ -26110,7 +27110,12 @@ impl Codegen {
|
|
|
26110
27110
|
res
|
|
26111
27111
|
};
|
|
26112
27112
|
match method {
|
|
26113
|
-
|
|
27113
|
+
// #609 stops short here on purpose. The accumulator is typed `f64` (or `i64` for the
|
|
27114
|
+
// integer-range form); with an `i32` element the body would mix an `i32` binding into
|
|
27115
|
+
// that accumulator — `acc = acc + x` either fails to compile or silently changes
|
|
27116
|
+
// overflow behaviour from f64's exact-integer range to i32 wrapping. That is a results
|
|
27117
|
+
// question, not a speed one, so it wants its own change with its own tests.
|
|
27118
|
+
"reduce" if *elem == RustType::F64 => {
|
|
26114
27119
|
if args.len() != 2 || params.len() != 2 {
|
|
26115
27120
|
return Ok(None);
|
|
26116
27121
|
}
|
|
@@ -26204,7 +27209,7 @@ impl Codegen {
|
|
|
26204
27209
|
let Some(x) = simple_name(¶ms[0]) else {
|
|
26205
27210
|
return Ok(None);
|
|
26206
27211
|
};
|
|
26207
|
-
let (body_code, body_ty) = emit_with(self, &[(&x,
|
|
27212
|
+
let (body_code, body_ty) = emit_with(self, &[(&x, elem.clone())])?;
|
|
26208
27213
|
if !body_ty.is_native() {
|
|
26209
27214
|
return Ok(None);
|
|
26210
27215
|
}
|
|
@@ -26224,17 +27229,21 @@ impl Codegen {
|
|
|
26224
27229
|
let Some(x) = simple_name(¶ms[0]) else {
|
|
26225
27230
|
return Ok(None);
|
|
26226
27231
|
};
|
|
26227
|
-
let (body_code, body_ty) = emit_with(self, &[(&x,
|
|
27232
|
+
let (body_code, body_ty) = emit_with(self, &[(&x, elem.clone())])?;
|
|
26228
27233
|
if body_ty != RustType::Bool {
|
|
26229
27234
|
return Ok(None);
|
|
26230
27235
|
}
|
|
26231
27236
|
let x_esc = Self::escape_ident(x.as_ref()).into_owned();
|
|
27237
|
+
// A filter PRESERVES the element type — collecting to `Vec<f64>` from an `i32`
|
|
27238
|
+
// receiver would not compile, and silently widening would change what the caller
|
|
27239
|
+
// gets back.
|
|
26232
27240
|
Ok(Some((
|
|
26233
27241
|
format!(
|
|
26234
|
-
"{recv}.iter().copied().filter(|&{x}| {body}).collect::<Vec<
|
|
26235
|
-
recv = recv, x = x_esc, body = body_code
|
|
27242
|
+
"{recv}.iter().copied().filter(|&{x}| {body}).collect::<Vec<{ety}>>()",
|
|
27243
|
+
recv = recv, x = x_esc, body = body_code,
|
|
27244
|
+
ety = elem.to_rust_type_str()
|
|
26236
27245
|
),
|
|
26237
|
-
RustType::Vec(Box::new(
|
|
27246
|
+
RustType::Vec(Box::new(elem.clone())),
|
|
26238
27247
|
)))
|
|
26239
27248
|
}
|
|
26240
27249
|
"some" | "every" => {
|
|
@@ -26244,7 +27253,7 @@ impl Codegen {
|
|
|
26244
27253
|
let Some(x) = simple_name(¶ms[0]) else {
|
|
26245
27254
|
return Ok(None);
|
|
26246
27255
|
};
|
|
26247
|
-
let (body_code, body_ty) = emit_with(self, &[(&x,
|
|
27256
|
+
let (body_code, body_ty) = emit_with(self, &[(&x, elem.clone())])?;
|
|
26248
27257
|
if body_ty != RustType::Bool {
|
|
26249
27258
|
return Ok(None);
|
|
26250
27259
|
}
|