@tishlang/tish-lint 3.3.0 → 3.3.3
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-lint +0 -0
- package/crates/tish_compile/src/codegen.rs +759 -70
- package/crates/tish_compile/tests/regr_gba_lowered_call_sites.rs +405 -1
- package/crates/tish_lint/Cargo.toml +1 -1
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-lint +0 -0
- package/platform/darwin-x64/tish-lint +0 -0
- package/platform/linux-arm64/tish-lint +0 -0
- package/platform/linux-x64/tish-lint +0 -0
- package/platform/win32-x64/tish-lint.exe +0 -0
|
@@ -1102,6 +1102,21 @@ pub(crate) struct Codegen {
|
|
|
1102
1102
|
/// Top-level `let name = [f64 literals…]` never reassigned — emitted as `const G_name: [f64; N]`
|
|
1103
1103
|
/// for direct indexing from native fns (fasta `codes`/`probs`).
|
|
1104
1104
|
module_const_f64_arrays: std::collections::HashMap<String, Vec<f64>>,
|
|
1105
|
+
/// #597: names passed DIRECTLY as a call argument (`fill(reqs)`). A typed array in this set must
|
|
1106
|
+
/// not lower to a native `Vec` — see the note at the `VarDecl` typing site.
|
|
1107
|
+
arrays_passed_to_calls: std::collections::HashSet<String>,
|
|
1108
|
+
/// The STATIC names from `module_const_f64_arrays` whose every element is an exact integer in
|
|
1109
|
+
/// `i32` range, so the hoist emits `[i32; N]` instead of `[f64; N]`.
|
|
1110
|
+
///
|
|
1111
|
+
/// ARM7TDMI HAS NO FPU. These passes were written for the host, where `f64` is tish's number and
|
|
1112
|
+
/// the representation is free; on GBA every `f64` touch is a soft-float call and every element
|
|
1113
|
+
/// costs 8 bytes instead of 4. Worse, the source usually declares the element type — hoisting
|
|
1114
|
+
/// `let A: i32[] = [1,2,3]` to `[f64; 3]` discards a type the program already stated, then pays
|
|
1115
|
+
/// an `i32 -> f64 -> usize` round trip on each index to get back to where it started.
|
|
1116
|
+
///
|
|
1117
|
+
/// Keyed by static name, not source name, so every read site (including the `_cum` aliases) can
|
|
1118
|
+
/// ask the question with what it already has in hand.
|
|
1119
|
+
module_const_int_statics: std::collections::HashSet<String>,
|
|
1105
1120
|
/// Precomputed cumulative arrays for `module_const_f64_arrays` used as `cumulative(p)` input
|
|
1106
1121
|
/// (`probs` → `G_probs_cum`).
|
|
1107
1122
|
module_const_f64_cum: std::collections::HashMap<String, String>,
|
|
@@ -1383,6 +1398,8 @@ impl Codegen {
|
|
|
1383
1398
|
refcell_wrapped_vars: std::collections::HashSet::new(),
|
|
1384
1399
|
native_numeric_globals: std::collections::HashMap::new(),
|
|
1385
1400
|
module_const_f64_arrays: std::collections::HashMap::new(),
|
|
1401
|
+
module_const_int_statics: std::collections::HashSet::new(),
|
|
1402
|
+
arrays_passed_to_calls: std::collections::HashSet::new(),
|
|
1386
1403
|
module_const_f64_cum: std::collections::HashMap::new(),
|
|
1387
1404
|
module_const_f64_aliases: std::collections::HashMap::new(),
|
|
1388
1405
|
map_instance_locals: std::collections::HashSet::new(),
|
|
@@ -2429,6 +2446,20 @@ impl Codegen {
|
|
|
2429
2446
|
"box", "do", "final", "macro", "override", "priv", "try", "typeof", "unsized",
|
|
2430
2447
|
"virtual", "yield",
|
|
2431
2448
|
];
|
|
2449
|
+
// #549: FOUR KEYWORDS CANNOT BE RAW IDENTIFIERS AT ALL. `r#self`, `r#Self`, `r#super` and
|
|
2450
|
+
// `r#crate` are rejected by rustc outright ("`self` cannot be a raw identifier"), so the
|
|
2451
|
+
// blanket `r#` below emitted a program that could not compile. These are ordinary names in
|
|
2452
|
+
// tish and the interpreter accepts them, which made this a backend divergence that REJECTED
|
|
2453
|
+
// VALID SOURCE rather than merely lowering it differently.
|
|
2454
|
+
//
|
|
2455
|
+
// They are renamed instead. The suffix is deliberately not a bare `_`: `self_` is a name a
|
|
2456
|
+
// program could plausibly use itself, and colliding with it would trade a compile error for
|
|
2457
|
+
// a silent capture of the wrong binding. `__tish_kw_` cannot be written in tish source,
|
|
2458
|
+
// because a leading double underscore is reserved by the compiler for generated temporaries.
|
|
2459
|
+
const NON_RAW_KEYWORDS: &[&str] = &["self", "Self", "super", "crate"];
|
|
2460
|
+
if NON_RAW_KEYWORDS.contains(&name) {
|
|
2461
|
+
return Cow::Owned(format!("__tish_kw_{}", name));
|
|
2462
|
+
}
|
|
2432
2463
|
if RUST_KEYWORDS.contains(&name) {
|
|
2433
2464
|
Cow::Owned(format!("r#{}", name))
|
|
2434
2465
|
} else {
|
|
@@ -3181,6 +3212,37 @@ impl Codegen {
|
|
|
3181
3212
|
&program.statements,
|
|
3182
3213
|
&self.module_const_f64_cum,
|
|
3183
3214
|
);
|
|
3215
|
+
// #597: which annotated arrays escape into a call, and so must stay boxed to preserve
|
|
3216
|
+
// aliasing. Collected once over the whole program — an array declared here can be
|
|
3217
|
+
// passed to a fn declared anywhere.
|
|
3218
|
+
//
|
|
3219
|
+
// GATED ON GBA, and not only because the RESULT is GBA-only: this scan runs
|
|
3220
|
+
// `scan_param_use` over the whole program once per annotated array, so it is
|
|
3221
|
+
// O(arrays x program). Off-target that is pure waste — the set is never read — and it
|
|
3222
|
+
// would be a silent compile-time tax on every host build.
|
|
3223
|
+
if self.emit_mode == crate::NativeEmitMode::Gba {
|
|
3224
|
+
let mut array_names: Vec<String> = Vec::new();
|
|
3225
|
+
Self::collect_annotated_array_names(&program.statements, &mut array_names);
|
|
3226
|
+
self.arrays_passed_to_calls =
|
|
3227
|
+
Self::collect_escaping_array_names(&program.statements, &array_names);
|
|
3228
|
+
}
|
|
3229
|
+
// Pick the integer representation where the values allow it. GBA only: off-target the
|
|
3230
|
+
// `f64` layout is free and eligibility must stay byte-identical to before.
|
|
3231
|
+
//
|
|
3232
|
+
// A `_cum` array is deliberately NOT considered — the fasta ladder that consumes one
|
|
3233
|
+
// compares it against an `r_f64` and is specialised around f64 arms, so leaving cum
|
|
3234
|
+
// alone keeps that path untouched.
|
|
3235
|
+
if self.emit_mode == crate::NativeEmitMode::Gba {
|
|
3236
|
+
for (name, vals) in &self.module_const_f64_arrays {
|
|
3237
|
+
if vals
|
|
3238
|
+
.iter()
|
|
3239
|
+
.all(|v| v.fract() == 0.0 && *v >= i32::MIN as f64 && *v <= i32::MAX as f64)
|
|
3240
|
+
{
|
|
3241
|
+
self.module_const_int_statics
|
|
3242
|
+
.insert(Self::module_const_static(name));
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3184
3246
|
if !self.module_const_f64_arrays.is_empty() {
|
|
3185
3247
|
self.emit_module_const_f64_arrays()?;
|
|
3186
3248
|
self.writeln("");
|
|
@@ -3675,7 +3737,25 @@ impl Codegen {
|
|
|
3675
3737
|
if self.is_async {
|
|
3676
3738
|
self.async_context_stack.push(true); // run() body is async Rust context
|
|
3677
3739
|
}
|
|
3678
|
-
|
|
3740
|
+
// #629: A FUNCTION MAY REFERENCE A MODULE BINDING DECLARED BELOW IT. Module bindings are
|
|
3741
|
+
// emitted into `run()` in source order, but a function VALUE is emitted where it appears and
|
|
3742
|
+
// captures by name — so `function readA() { return A[i] }` above `let A = [...]` emitted a
|
|
3743
|
+
// capture of `A` five lines before `let A`, and rustc rejected the program:
|
|
3744
|
+
//
|
|
3745
|
+
// let rangDone = { Value::native(move |args| { … vm_read(&rangLive) … }) }; // use
|
|
3746
|
+
// let rangLive = VmRef::new(vec![(0_f64) as i32]); // decl
|
|
3747
|
+
//
|
|
3748
|
+
// That is legal tish — the function is not CALLED before the binding is initialised — and
|
|
3749
|
+
// only the ORDER differs; moving the `let` above the functions compiles. It took hyrule's
|
|
3750
|
+
// whole ROM down (`rangDone` at weapons.tish:28, `rangLive` at :46).
|
|
3751
|
+
//
|
|
3752
|
+
// Reordered here rather than at the capture site because the capture is not wrong; its
|
|
3753
|
+
// position is. Restricted to LITERAL initialisers on purpose: hoisting `let x = f()` would
|
|
3754
|
+
// move a call, changing when its side effects run and possibly reading state the statements
|
|
3755
|
+
// above it establish. A literal cannot observe or disturb anything, so moving it is not
|
|
3756
|
+
// visible to the program.
|
|
3757
|
+
let reordered = Self::hoist_forward_referenced_module_bindings(&program.statements);
|
|
3758
|
+
self.emit_statements_with_folds(reordered.as_deref().unwrap_or(&program.statements))?;
|
|
3679
3759
|
if self.is_async {
|
|
3680
3760
|
self.async_context_stack.pop();
|
|
3681
3761
|
}
|
|
@@ -4101,9 +4181,26 @@ impl Codegen {
|
|
|
4101
4181
|
// `emit_int32_operand` (a `>>> 0` result is u32 reinterpreted to i32; signed
|
|
4102
4182
|
// bitwise ops yield i32 directly) — so store the i32 with NO `f64` round-trip.
|
|
4103
4183
|
if rust_type == RustType::I32 {
|
|
4184
|
+
// #599: A CAPTURED i32 IS A CELL, AND THE WRITE HAS TO SAY SO. Reads of a
|
|
4185
|
+
// wrapped scalar already go through `vm_read(&x)`; every store below used to
|
|
4186
|
+
// emit a bare `x = <i32>`, so the two halves disagreed and the crate failed with
|
|
4187
|
+
// `expected VmRef<i32>, found i32`. Net effect was that a mutable module-level
|
|
4188
|
+
// typed scalar could not be used at all on GBA — every counter had to move into
|
|
4189
|
+
// a function body or hide in a one-element `i32[]`.
|
|
4190
|
+
//
|
|
4191
|
+
// The narrow-int branch below has always done this; the i32 branch is the one
|
|
4192
|
+
// that was missed, across all three of its store shapes.
|
|
4193
|
+
let is_refcell = self.refcell_wrapped_vars.contains(name.as_ref());
|
|
4194
|
+
let store = |escaped: &str, val: String| {
|
|
4195
|
+
if is_refcell {
|
|
4196
|
+
format!("{{ let _t = {}; *{}.borrow_mut() = _t; }}", val, escaped)
|
|
4197
|
+
} else {
|
|
4198
|
+
format!("{} = {}", escaped, val)
|
|
4199
|
+
}
|
|
4200
|
+
};
|
|
4104
4201
|
if let Some(int_code) = self.emit_int32_operand(value)? {
|
|
4105
4202
|
let escaped = Self::escape_ident(name.as_ref());
|
|
4106
|
-
return Ok(format!("
|
|
4203
|
+
return Ok(store(&escaped, format!("({}) as i32", int_code)));
|
|
4107
4204
|
}
|
|
4108
4205
|
let (val_code, val_ty) = self.emit_typed_expr(value)?;
|
|
4109
4206
|
// An RHS that is ALREADY an integer stores straight into the register. `d = d + 1`
|
|
@@ -4112,7 +4209,7 @@ impl Codegen {
|
|
|
4112
4209
|
// out of it was the whole cost of incrementing an annotated counter.
|
|
4113
4210
|
let escaped_int = Self::escape_ident(name.as_ref());
|
|
4114
4211
|
if val_ty.is_integer_scalar() {
|
|
4115
|
-
return Ok(format!("
|
|
4212
|
+
return Ok(store(&escaped_int, format!("({}) as i32", val_code)));
|
|
4116
4213
|
}
|
|
4117
4214
|
// Defensive: gate guarantees `Some`, but if a future RHS shape slips through,
|
|
4118
4215
|
// fall back to a sound f64-narrowed store rather than miscompiling.
|
|
@@ -4122,11 +4219,7 @@ impl Codegen {
|
|
|
4122
4219
|
val_code
|
|
4123
4220
|
};
|
|
4124
4221
|
let escaped = Self::escape_ident(name.as_ref());
|
|
4125
|
-
return Ok(
|
|
4126
|
-
"{} = {}",
|
|
4127
|
-
escaped,
|
|
4128
|
-
RustType::I32.from_value_expr(&v)
|
|
4129
|
-
));
|
|
4222
|
+
return Ok(store(&escaped, RustType::I32.from_value_expr(&v)));
|
|
4130
4223
|
}
|
|
4131
4224
|
// String self-append `s = s + rhs` -> in-place push_str (amortized O(1)). The
|
|
4132
4225
|
// general path boxes via `ops::add(Value::String(s.clone()), ...)` which clones
|
|
@@ -5777,6 +5870,34 @@ impl Codegen {
|
|
|
5777
5870
|
})
|
|
5778
5871
|
.unwrap_or(RustType::Value);
|
|
5779
5872
|
|
|
5873
|
+
// #597: AN ARRAY THAT ESCAPES INTO A BOXED CALL STAYS BOXED. A native `Vec<T>`
|
|
5874
|
+
// handed to a `value_call` is marshalled by BUILDING A FRESH `Value::Array` from
|
|
5875
|
+
// its elements — a copy by construction — so the callee's `push`es land in the copy
|
|
5876
|
+
// and the caller's array silently keeps its old contents. Nothing reports it: not
|
|
5877
|
+
// the compiler, not the runtime. An UNTYPED array passes the same `VmRef` and
|
|
5878
|
+
// aliases correctly, which means adding an annotation changed program BEHAVIOUR
|
|
5879
|
+
// rather than just representation.
|
|
5880
|
+
//
|
|
5881
|
+
// The call site cannot fix this — boxing a `Vec<i32>` into a `Value::Array` is a
|
|
5882
|
+
// copy however it is spelled. So the decision has to be made here, at the
|
|
5883
|
+
// declaration: if the array is ever passed as a call argument, keep it boxed.
|
|
5884
|
+
//
|
|
5885
|
+
// Same rule the read-only param analysis already applies from the other side
|
|
5886
|
+
// (`arr_param_readonly_fns` refuses an owned copy when a param is mutated, escapes
|
|
5887
|
+
// or is forwarded). Conservative on purpose: an array that never escapes is
|
|
5888
|
+
// untouched and keeps its native lowering.
|
|
5889
|
+
// GBA ONLY. The reporter checked the other backends: `tish run` and a native
|
|
5890
|
+
// `tish build` both alias correctly and print the expected result, so this is a
|
|
5891
|
+
// TARGET divergence rather than a language-wide one. Narrowing off-target would
|
|
5892
|
+
// deopt host code to fix a bug host code does not have, and one host test
|
|
5893
|
+
// (`escaping_array_keeps_oob_safe_lowering`) pins the native store it would remove.
|
|
5894
|
+
if self.emit_mode == crate::NativeEmitMode::Gba
|
|
5895
|
+
&& matches!(rust_type, RustType::Vec(_))
|
|
5896
|
+
&& self.arrays_passed_to_calls.contains(name.as_ref())
|
|
5897
|
+
{
|
|
5898
|
+
rust_type = RustType::Value;
|
|
5899
|
+
}
|
|
5900
|
+
|
|
5780
5901
|
// M5 native-fn body: `let r = genRandom(1)` without a `: number` annotation.
|
|
5781
5902
|
if self.native_fn_body_emit && rust_type == RustType::Value {
|
|
5782
5903
|
if let Some(Expr::Call { callee, .. }) = init.as_ref() {
|
|
@@ -5904,12 +6025,22 @@ impl Codegen {
|
|
|
5904
6025
|
return Ok(());
|
|
5905
6026
|
}
|
|
5906
6027
|
|
|
5907
|
-
// Module-level const
|
|
6028
|
+
// Module-level const numeric arrays — no local slot in `run()`. The element type
|
|
6029
|
+
// must match the emitted representation, or a consumer reading the type context
|
|
6030
|
+
// coerces against a layout that is not there.
|
|
5908
6031
|
if self.module_const_f64_arrays.contains_key(name.as_ref())
|
|
5909
6032
|
&& self.outer_vars_stack.len() == 1
|
|
5910
6033
|
{
|
|
6034
|
+
let elem = if self
|
|
6035
|
+
.module_const_int_statics
|
|
6036
|
+
.contains(&Self::module_const_static(name.as_ref()))
|
|
6037
|
+
{
|
|
6038
|
+
RustType::I32
|
|
6039
|
+
} else {
|
|
6040
|
+
RustType::F64
|
|
6041
|
+
};
|
|
5911
6042
|
self.type_context
|
|
5912
|
-
.define(name.as_ref(), RustType::Vec(Box::new(
|
|
6043
|
+
.define(name.as_ref(), RustType::Vec(Box::new(elem)));
|
|
5913
6044
|
return Ok(());
|
|
5914
6045
|
}
|
|
5915
6046
|
|
|
@@ -7954,7 +8085,7 @@ impl Codegen {
|
|
|
7954
8085
|
));
|
|
7955
8086
|
}
|
|
7956
8087
|
if let Some(static_name) = self.module_const_f64_array_rust_ref(name.as_ref()) {
|
|
7957
|
-
let v =
|
|
8088
|
+
let v = self.module_const_f64_array_as_value(&static_name);
|
|
7958
8089
|
return Ok(if self.value_fn_depth > 0 || !self.loop_stack.is_empty() {
|
|
7959
8090
|
format!("({}).clone()", v)
|
|
7960
8091
|
} else {
|
|
@@ -14053,13 +14184,22 @@ impl Codegen {
|
|
|
14053
14184
|
// so a RHS that reads the same cell (`a[i] += x`, `a[i] = a[j]`) releases its shared borrow
|
|
14054
14185
|
// first (no overlapping lock → no deadlock), then store into the real element through the cell.
|
|
14055
14186
|
if wrapped {
|
|
14056
|
-
|
|
14057
|
-
|
|
14187
|
+
// #611: I32 GROWS TOO. A boxed array absorbs `a[i] = v` past the end by growing; a
|
|
14188
|
+
// typed one used to fall through to a bare element store, so the same source line
|
|
14189
|
+
// behaved differently depending only on whether the annotation was present — the write
|
|
14190
|
+
// was lost with no error at compile time or run time. That silent divergence between
|
|
14191
|
+
// the typed and boxed forms is the worst of the available options, so the typed form
|
|
14192
|
+
// now matches the boxed one. Pad is `0`, which is what the READ path already answers
|
|
14193
|
+
// for an out-of-range `i32` element (`.get(i).copied().unwrap_or(0)`).
|
|
14194
|
+
let needs_resize = matches!(
|
|
14195
|
+
elem_type.as_ref(),
|
|
14196
|
+
RustType::F64 | RustType::Bool | RustType::I32
|
|
14197
|
+
) && !in_bounds;
|
|
14058
14198
|
let store = if needs_resize {
|
|
14059
|
-
let pad =
|
|
14060
|
-
"f64::NAN"
|
|
14061
|
-
|
|
14062
|
-
"false"
|
|
14199
|
+
let pad = match elem_type.as_ref() {
|
|
14200
|
+
RustType::F64 => "f64::NAN",
|
|
14201
|
+
RustType::I32 => "0",
|
|
14202
|
+
_ => "false",
|
|
14063
14203
|
};
|
|
14064
14204
|
format!(
|
|
14065
14205
|
"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 +14225,22 @@ impl Codegen {
|
|
|
14085
14225
|
format!("{{ {}[{}] = {}; Value::Null }}", esc_obj, idx_usize, native_val)
|
|
14086
14226
|
}
|
|
14087
14227
|
}
|
|
14088
|
-
|
|
14089
|
-
|
|
14090
|
-
|
|
14091
|
-
|
|
14092
|
-
"
|
|
14228
|
+
// #611: same grow-to-index rule on the unwrapped path — see the note above.
|
|
14229
|
+
RustType::F64 | RustType::Bool | RustType::I32 => {
|
|
14230
|
+
let pad = match elem_type.as_ref() {
|
|
14231
|
+
RustType::F64 => "f64::NAN",
|
|
14232
|
+
RustType::I32 => "0",
|
|
14233
|
+
_ => "false",
|
|
14093
14234
|
};
|
|
14094
|
-
format!(
|
|
14095
|
-
"{{ let _idx = {}; if _idx >= {}.len() {{ {}.resize(_idx + 1, {}); }} {}[_idx] = {}
|
|
14235
|
+
let store = format!(
|
|
14236
|
+
"{{ let _idx = {}; if _idx >= {}.len() {{ {}.resize(_idx + 1, {}); }} {}[_idx] = {}",
|
|
14096
14237
|
idx_usize, esc_obj, esc_obj, pad, esc_obj, native_val
|
|
14097
|
-
)
|
|
14238
|
+
);
|
|
14239
|
+
if side_effect_only {
|
|
14240
|
+
format!("{} }}", store)
|
|
14241
|
+
} else {
|
|
14242
|
+
format!("{}; Value::Null }}", store)
|
|
14243
|
+
}
|
|
14098
14244
|
}
|
|
14099
14245
|
_ if side_effect_only => {
|
|
14100
14246
|
format!("{}[{}] = {}", esc_obj, idx_usize, native_val)
|
|
@@ -16798,7 +16944,15 @@ impl Codegen {
|
|
|
16798
16944
|
{
|
|
16799
16945
|
let sum_esc = Self::escape_ident(sum_name.as_ref());
|
|
16800
16946
|
let arms: Vec<String> = (0..n)
|
|
16801
|
-
|
|
16947
|
+
// The fasta ladder is specialised around f64 arms, so an integer-represented
|
|
16948
|
+
// codes array widens here rather than perturbing that path.
|
|
16949
|
+
.map(|i| {
|
|
16950
|
+
if self.module_const_int_statics.contains(codes_static.as_str()) {
|
|
16951
|
+
format!("{} => {}[{}] as f64", i, codes_static, i)
|
|
16952
|
+
} else {
|
|
16953
|
+
format!("{} => {}[{}]", i, codes_static, i)
|
|
16954
|
+
}
|
|
16955
|
+
})
|
|
16802
16956
|
.collect();
|
|
16803
16957
|
self.writeln(&format!(
|
|
16804
16958
|
"{} = (({} + match ({} as usize) {{ {}, _ => 0.0 }}) % 2147483647_f64);",
|
|
@@ -16956,6 +17110,105 @@ impl Codegen {
|
|
|
16956
17110
|
self.native_fn_emit_name.is_some() || self.native_vec_ret.is_some()
|
|
16957
17111
|
}
|
|
16958
17112
|
|
|
17113
|
+
/// #629: move module-level `let`s with LITERAL initialisers ahead of any function declared
|
|
17114
|
+
/// above them that references the name. Returns `None` when nothing needs moving, so the common
|
|
17115
|
+
/// program keeps its exact statement slice and this costs one scan.
|
|
17116
|
+
///
|
|
17117
|
+
/// Only bindings that are actually forward-referenced move, and only past the point where they
|
|
17118
|
+
/// are first needed — the relative order of everything else is preserved.
|
|
17119
|
+
fn hoist_forward_referenced_module_bindings(stmts: &[Statement]) -> Option<Vec<Statement>> {
|
|
17120
|
+
// Names bound at module level by a literal `let`, with the index of their declaration.
|
|
17121
|
+
let mut literal_decls: Vec<(usize, String)> = Vec::new();
|
|
17122
|
+
for (i, s) in stmts.iter().enumerate() {
|
|
17123
|
+
if let Statement::VarDecl {
|
|
17124
|
+
name,
|
|
17125
|
+
init: Some(e),
|
|
17126
|
+
..
|
|
17127
|
+
} = s
|
|
17128
|
+
{
|
|
17129
|
+
if Self::is_literal_initialiser(e) {
|
|
17130
|
+
literal_decls.push((i, name.to_string()));
|
|
17131
|
+
}
|
|
17132
|
+
}
|
|
17133
|
+
}
|
|
17134
|
+
if literal_decls.is_empty() {
|
|
17135
|
+
return None;
|
|
17136
|
+
}
|
|
17137
|
+
// A NAME DECLARED MORE THAN ONCE AT MODULE LEVEL IS NEVER MOVED. An earlier mention of such
|
|
17138
|
+
// a name refers to the EARLIER binding, not forward to this one, so hoisting this
|
|
17139
|
+
// declaration over it silently changes which value the earlier code sees. That is a wrong
|
|
17140
|
+
// ANSWER, not a build break: tests/core/js_compat_gaps.tish declares `text` twice — a string
|
|
17141
|
+
// at the top and a different string further down — and moving the second above the first
|
|
17142
|
+
// made an intervening `slice` read the wrong source, printing `hello 123 world 45` where
|
|
17143
|
+
// `.mdx` was expected.
|
|
17144
|
+
let mut decl_counts: std::collections::HashMap<&str, usize> =
|
|
17145
|
+
std::collections::HashMap::new();
|
|
17146
|
+
for s in stmts {
|
|
17147
|
+
if let Statement::VarDecl { name, .. } = s {
|
|
17148
|
+
*decl_counts.entry(name.as_ref()).or_insert(0) += 1;
|
|
17149
|
+
}
|
|
17150
|
+
}
|
|
17151
|
+
// A binding needs moving when a FUNCTION BODY above its declaration mentions it. Deferred
|
|
17152
|
+
// execution is what makes a forward reference legal — the body does not run until called.
|
|
17153
|
+
//
|
|
17154
|
+
// Straight-line code above the declaration is deliberately NOT counted: there the mention
|
|
17155
|
+
// either refers to a previous binding of the name or is a genuine use-before-init, and in
|
|
17156
|
+
// neither case may the declaration be moved over it.
|
|
17157
|
+
let mut needed: Vec<(usize, usize)> = Vec::new(); // (decl index, first referencing index)
|
|
17158
|
+
for (di, name) in &literal_decls {
|
|
17159
|
+
if decl_counts.get(name.as_str()).copied().unwrap_or(0) > 1 {
|
|
17160
|
+
continue;
|
|
17161
|
+
}
|
|
17162
|
+
let mut first = None;
|
|
17163
|
+
for (i, s) in stmts.iter().enumerate().take(*di) {
|
|
17164
|
+
if matches!(s, Statement::FunDecl { .. }) && Self::stmt_mentions_ident(s, name) {
|
|
17165
|
+
first = Some(i);
|
|
17166
|
+
break;
|
|
17167
|
+
}
|
|
17168
|
+
}
|
|
17169
|
+
if let Some(f) = first {
|
|
17170
|
+
needed.push((*di, f));
|
|
17171
|
+
}
|
|
17172
|
+
}
|
|
17173
|
+
if needed.is_empty() {
|
|
17174
|
+
return None;
|
|
17175
|
+
}
|
|
17176
|
+
// Rebuild: each moved declaration is inserted immediately before the first statement that
|
|
17177
|
+
// references it, keeping every other statement in place.
|
|
17178
|
+
let mut out: Vec<Statement> = Vec::with_capacity(stmts.len());
|
|
17179
|
+
let moved: std::collections::HashSet<usize> = needed.iter().map(|(d, _)| *d).collect();
|
|
17180
|
+
for (i, s) in stmts.iter().enumerate() {
|
|
17181
|
+
for (di, fi) in &needed {
|
|
17182
|
+
if *fi == i {
|
|
17183
|
+
out.push(stmts[*di].clone());
|
|
17184
|
+
}
|
|
17185
|
+
}
|
|
17186
|
+
if !moved.contains(&i) {
|
|
17187
|
+
out.push(s.clone());
|
|
17188
|
+
}
|
|
17189
|
+
}
|
|
17190
|
+
Some(out)
|
|
17191
|
+
}
|
|
17192
|
+
|
|
17193
|
+
/// An initialiser that cannot observe or disturb program state, so moving it is invisible.
|
|
17194
|
+
/// Deliberately narrow — array/object literals are included only when every element is itself
|
|
17195
|
+
/// literal, so no call or property read comes along for the ride.
|
|
17196
|
+
fn is_literal_initialiser(e: &Expr) -> bool {
|
|
17197
|
+
match e {
|
|
17198
|
+
Expr::Literal { .. } => true,
|
|
17199
|
+
Expr::Unary { operand, .. } => Self::is_literal_initialiser(operand),
|
|
17200
|
+
Expr::Array { elements, .. } => elements.iter().all(|el| match el {
|
|
17201
|
+
ArrayElement::Expr(e) => Self::is_literal_initialiser(e),
|
|
17202
|
+
_ => false,
|
|
17203
|
+
}),
|
|
17204
|
+
Expr::Object { props, .. } => props.iter().all(|p| match p {
|
|
17205
|
+
ObjectProp::KeyValue(_, v, _) => Self::is_literal_initialiser(v),
|
|
17206
|
+
_ => false,
|
|
17207
|
+
}),
|
|
17208
|
+
_ => false,
|
|
17209
|
+
}
|
|
17210
|
+
}
|
|
17211
|
+
|
|
16959
17212
|
fn emit_statements_with_folds(&mut self, statements: &[Statement]) -> Result<(), CompileError> {
|
|
16960
17213
|
let mut i = 0;
|
|
16961
17214
|
while i < statements.len() {
|
|
@@ -17155,41 +17408,82 @@ impl Codegen {
|
|
|
17155
17408
|
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
17156
17409
|
Self::walk_stmts_for_module_const_disqualifiers(statements, g, bad);
|
|
17157
17410
|
}
|
|
17411
|
+
// EVERY child expression has to be visited, not just the sub-statements. These arms used
|
|
17412
|
+
// to walk only the bodies, so a use in a loop or branch CONDITION was invisible — which
|
|
17413
|
+
// is how `while (i < SB_FORCE.length)` (card-gba qb-engine.tish) kept an array eligible
|
|
17414
|
+
// for hoisting even though `.length` is a disqualifying member use. Same defect class as
|
|
17415
|
+
// the missing closure arm below: the walker's silence meant "safe", not "absent".
|
|
17158
17416
|
Statement::If {
|
|
17417
|
+
cond,
|
|
17159
17418
|
then_branch,
|
|
17160
17419
|
else_branch,
|
|
17161
17420
|
..
|
|
17162
17421
|
} => {
|
|
17422
|
+
Self::walk_expr_for_module_const_disqualifiers(cond, g, bad);
|
|
17163
17423
|
Self::walk_stmt_for_module_const_disqualifiers(then_branch, g, bad);
|
|
17164
17424
|
if let Some(eb) = else_branch {
|
|
17165
17425
|
Self::walk_stmt_for_module_const_disqualifiers(eb, g, bad);
|
|
17166
17426
|
}
|
|
17167
17427
|
}
|
|
17168
|
-
Statement::While { body, .. } | Statement::DoWhile { body, .. } => {
|
|
17428
|
+
Statement::While { cond, body, .. } | Statement::DoWhile { body, cond, .. } => {
|
|
17429
|
+
Self::walk_expr_for_module_const_disqualifiers(cond, g, bad);
|
|
17169
17430
|
Self::walk_stmt_for_module_const_disqualifiers(body, g, bad);
|
|
17170
17431
|
}
|
|
17171
|
-
Statement::For {
|
|
17432
|
+
Statement::For {
|
|
17433
|
+
init,
|
|
17434
|
+
cond,
|
|
17435
|
+
update,
|
|
17436
|
+
body,
|
|
17437
|
+
..
|
|
17438
|
+
} => {
|
|
17172
17439
|
if let Some(i) = init {
|
|
17173
17440
|
Self::walk_stmt_for_module_const_disqualifiers(i, g, bad);
|
|
17174
17441
|
}
|
|
17442
|
+
if let Some(c) = cond {
|
|
17443
|
+
Self::walk_expr_for_module_const_disqualifiers(c, g, bad);
|
|
17444
|
+
}
|
|
17445
|
+
if let Some(u) = update {
|
|
17446
|
+
Self::walk_expr_for_module_const_disqualifiers(u, g, bad);
|
|
17447
|
+
}
|
|
17175
17448
|
Self::walk_stmt_for_module_const_disqualifiers(body, g, bad);
|
|
17176
17449
|
}
|
|
17177
|
-
Statement::ForOf { body, .. }
|
|
17178
|
-
| Statement::ForIn {
|
|
17450
|
+
Statement::ForOf { iterable, body, .. }
|
|
17451
|
+
| Statement::ForIn {
|
|
17452
|
+
object: iterable,
|
|
17453
|
+
body,
|
|
17454
|
+
..
|
|
17455
|
+
} => {
|
|
17456
|
+
// ITERATING THE ARRAY IS A SUPPORTED USE, not a disqualifying one: `for (x of X)`
|
|
17457
|
+
// lowers to a native index loop over the hoisted static (`for _fof_i0 in
|
|
17458
|
+
// 0..G_X.len()`), so the name never survives into the output. Walking the iterable
|
|
17459
|
+
// unconditionally would see a bare `Ident` and refuse the hoist, which loses the
|
|
17460
|
+
// very optimisation the pass exists for (tests/core/typed_array_forof.tish).
|
|
17461
|
+
// Anything else in iterable position is walked normally.
|
|
17462
|
+
if !matches!(iterable, Expr::Ident { name, .. } if name.as_ref() == g) {
|
|
17463
|
+
Self::walk_expr_for_module_const_disqualifiers(iterable, g, bad);
|
|
17464
|
+
}
|
|
17179
17465
|
Self::walk_stmt_for_module_const_disqualifiers(body, g, bad);
|
|
17180
17466
|
}
|
|
17181
17467
|
Statement::Switch {
|
|
17468
|
+
expr,
|
|
17182
17469
|
cases,
|
|
17183
17470
|
default_body,
|
|
17184
17471
|
..
|
|
17185
17472
|
} => {
|
|
17186
|
-
|
|
17473
|
+
Self::walk_expr_for_module_const_disqualifiers(expr, g, bad);
|
|
17474
|
+
for (case_expr, stmts) in cases {
|
|
17475
|
+
if let Some(e) = case_expr {
|
|
17476
|
+
Self::walk_expr_for_module_const_disqualifiers(e, g, bad);
|
|
17477
|
+
}
|
|
17187
17478
|
Self::walk_stmts_for_module_const_disqualifiers(stmts, g, bad);
|
|
17188
17479
|
}
|
|
17189
17480
|
if let Some(stmts) = default_body {
|
|
17190
17481
|
Self::walk_stmts_for_module_const_disqualifiers(stmts, g, bad);
|
|
17191
17482
|
}
|
|
17192
17483
|
}
|
|
17484
|
+
Statement::Throw { value, .. } => {
|
|
17485
|
+
Self::walk_expr_for_module_const_disqualifiers(value, g, bad);
|
|
17486
|
+
}
|
|
17193
17487
|
Statement::Try {
|
|
17194
17488
|
body,
|
|
17195
17489
|
catch_body,
|
|
@@ -17220,10 +17514,32 @@ impl Codegen {
|
|
|
17220
17514
|
Self::walk_expr_for_module_const_disqualifiers(e, g, bad);
|
|
17221
17515
|
}
|
|
17222
17516
|
}
|
|
17223
|
-
|
|
17517
|
+
// CONSERVATIVE DEFAULT. This walker decides whether it is safe to move an array into a
|
|
17518
|
+
// `const`, so "I do not model this statement" must mean "refuse", never "permit" — the
|
|
17519
|
+
// old `_ => {}` meant the latter, and a wrong permit is a program that does not compile.
|
|
17520
|
+
// Over-rejecting only costs the hoist; under-rejecting cost four ROMs their build.
|
|
17521
|
+
s => {
|
|
17522
|
+
if Self::stmt_mentions_ident(s, g) {
|
|
17523
|
+
*bad = true;
|
|
17524
|
+
}
|
|
17525
|
+
}
|
|
17224
17526
|
}
|
|
17225
17527
|
}
|
|
17226
17528
|
|
|
17529
|
+
/// Complete-traversal membership test, used as the safe fallback by both walkers above.
|
|
17530
|
+
fn stmt_mentions_ident(s: &Statement, g: &str) -> bool {
|
|
17531
|
+
let mut idents = HashSet::new();
|
|
17532
|
+
Self::collect_stmt_idents(s, &mut idents);
|
|
17533
|
+
idents.contains(g)
|
|
17534
|
+
}
|
|
17535
|
+
|
|
17536
|
+
/// As `stmt_mentions_ident`, for an expression subtree.
|
|
17537
|
+
fn expr_mentions_ident(e: &Expr, g: &str) -> bool {
|
|
17538
|
+
let mut idents = HashSet::new();
|
|
17539
|
+
Self::collect_expr_idents(e, &mut idents);
|
|
17540
|
+
idents.contains(g)
|
|
17541
|
+
}
|
|
17542
|
+
|
|
17227
17543
|
fn walk_expr_for_module_const_disqualifiers(e: &Expr, g: &str, bad: &mut bool) {
|
|
17228
17544
|
if *bad {
|
|
17229
17545
|
return;
|
|
@@ -17281,9 +17597,51 @@ impl Codegen {
|
|
|
17281
17597
|
callee.as_ref(),
|
|
17282
17598
|
Expr::Ident { name, .. } if name.as_ref() == "cumulative"
|
|
17283
17599
|
);
|
|
17600
|
+
// A METHOD CALL ON THE ARRAY IS SUPPORTED — but only the ones that go through the
|
|
17601
|
+
// `Value` bridge. `X.join(", ")` emits the object as
|
|
17602
|
+
// `NumArrayBacking::Packed(G_X…)`, so the local name does not survive and the hoist
|
|
17603
|
+
// is fine (tests/core/template_literals.tish).
|
|
17604
|
+
//
|
|
17605
|
+
// THE FUSEABLE HOFs ARE NOT. `X.map(...)`, `X.reduce(...)` and their siblings lower
|
|
17606
|
+
// to a NATIVE iterator chain — `xs.iter().copied().map(...)` — which names the local
|
|
17607
|
+
// directly and is NOT rewritten to the static. Exempting them hoisted an array whose
|
|
17608
|
+
// every use then referenced a binding that no longer existed:
|
|
17609
|
+
//
|
|
17610
|
+
// let mut sum: f64 = { … for b in xs.iter().copied() { … } … };
|
|
17611
|
+
// error[E0425]: cannot find value `xs` in this scope
|
|
17612
|
+
//
|
|
17613
|
+
// Caught by the perf gauntlet's `typed_array_hof` fixture on the NATIVE target —
|
|
17614
|
+
// this walker is not GBA-gated, so a too-permissive exemption breaks the host.
|
|
17615
|
+
//
|
|
17616
|
+
// Nor does the exemption extend to a member READ such as `X.length`, which lowers to
|
|
17617
|
+
// a bare `X.len()` — one of the two shapes that broke the ROMs.
|
|
17618
|
+
// ALLOWLIST, NOT DENYLIST. This started as "any method call except the fuseable
|
|
17619
|
+
// HOFs", which is allow-by-default — and every MUTATING method walked straight
|
|
17620
|
+
// through it. `arr2.unshift(1, 2)` hoisted its receiver into a `const` and then
|
|
17621
|
+
// tried to mutate it, so the local vanished and rustc reported
|
|
17622
|
+
// `cannot find value arr2` (tests/core/array_methods.tish, under
|
|
17623
|
+
// TISH_PACKED_ARRAYS=1). `push`/`pop`/`splice`/`sort`/`reverse`/`fill` are the same
|
|
17624
|
+
// shape.
|
|
17625
|
+
//
|
|
17626
|
+
// So only methods PROVEN to route the receiver through the `Value` bridge are
|
|
17627
|
+
// exempt. `join` is the one this pass actually needs
|
|
17628
|
+
// (tests/core/template_literals.tish emits
|
|
17629
|
+
// `array_join(&Value::NumberArray(…Packed(G_NUMS…)))`, where the local name does not
|
|
17630
|
+
// survive). Anything else — mutators, the HOFs that lower to a native iterator chain
|
|
17631
|
+
// over the bare local, and any method added later — disqualifies the hoist, which is
|
|
17632
|
+
// the safe direction: it costs an optimisation, not a build.
|
|
17633
|
+
let member_call_on_g = matches!(
|
|
17634
|
+
callee.as_ref(),
|
|
17635
|
+
Expr::Member { object, prop, .. }
|
|
17636
|
+
if matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == g)
|
|
17637
|
+
&& matches!(
|
|
17638
|
+
prop,
|
|
17639
|
+
MemberProp::Name { name: m, .. } if m.as_ref() == "join"
|
|
17640
|
+
)
|
|
17641
|
+
);
|
|
17284
17642
|
if matches!(callee.as_ref(), Expr::Ident { name, .. } if name.as_ref() == g) {
|
|
17285
17643
|
*bad = true;
|
|
17286
|
-
} else {
|
|
17644
|
+
} else if !member_call_on_g {
|
|
17287
17645
|
Self::walk_expr_for_module_const_disqualifiers(callee, g, bad);
|
|
17288
17646
|
}
|
|
17289
17647
|
for (i, a) in args.iter().enumerate() {
|
|
@@ -17334,7 +17692,75 @@ impl Codegen {
|
|
|
17334
17692
|
}
|
|
17335
17693
|
}
|
|
17336
17694
|
}
|
|
17337
|
-
|
|
17695
|
+
// Everything below was missing, and the omission is what made this pass unsafe: an
|
|
17696
|
+
// admission check whose walker reports "no disqualifying use" for a shape it does not
|
|
17697
|
+
// recognise is silently permissive. `ArrowFunction` was the expensive one — every use
|
|
17698
|
+
// inside a CLOSURE landed in `_ => {}`, so an array a closure writes got hoisted to a
|
|
17699
|
+
// `const` anyway. Reads then renamed to `G_X` while the write kept the boxed local, and
|
|
17700
|
+
// that local is declared after the closure: rustc reports `cannot find value` on a name
|
|
17701
|
+
// the game never misspelled (card-gba queensblood/triad/gwent; tish-gba hyrule's
|
|
17702
|
+
// `rangLive`). Renaming the write would not have been a fix either — a `const` cannot be
|
|
17703
|
+
// assigned through, so a written array must not be hoisted at all. The sibling scalar
|
|
17704
|
+
// walker has always had these arms; this one is a copy that lost them.
|
|
17705
|
+
//
|
|
17706
|
+
// Indexing stays deliberately absent from the disqualifier list: `X[i]` is the whole
|
|
17707
|
+
// point of the hoist, so descending here must not become "any closure mention kills it",
|
|
17708
|
+
// which would give back the #594 reachability this pass exists to provide.
|
|
17709
|
+
Expr::ArrowFunction { body, .. } => match body {
|
|
17710
|
+
ArrowBody::Expr(e) => Self::walk_expr_for_module_const_disqualifiers(e, g, bad),
|
|
17711
|
+
ArrowBody::Block(s) => Self::walk_stmt_for_module_const_disqualifiers(s, g, bad),
|
|
17712
|
+
},
|
|
17713
|
+
// A write to a member of the const, by the same rule the `Member` arm above uses.
|
|
17714
|
+
Expr::MemberAssign { object, value, .. } => {
|
|
17715
|
+
if matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == g) {
|
|
17716
|
+
*bad = true;
|
|
17717
|
+
} else {
|
|
17718
|
+
Self::walk_expr_for_module_const_disqualifiers(object, g, bad);
|
|
17719
|
+
}
|
|
17720
|
+
Self::walk_expr_for_module_const_disqualifiers(value, g, bad);
|
|
17721
|
+
}
|
|
17722
|
+
Expr::Delete { target, .. }
|
|
17723
|
+
if matches!(target.as_ref(), Expr::Ident { name, .. } if name.as_ref() == g) =>
|
|
17724
|
+
{
|
|
17725
|
+
*bad = true;
|
|
17726
|
+
}
|
|
17727
|
+
// The remaining shapes need no special rule: a bare mention of the name is already
|
|
17728
|
+
// disqualifying via the `Ident` arm, so plain descent is sufficient.
|
|
17729
|
+
Expr::New { callee, args, .. } => {
|
|
17730
|
+
Self::walk_expr_for_module_const_disqualifiers(callee, g, bad);
|
|
17731
|
+
for a in args {
|
|
17732
|
+
if let CallArg::Expr(e) | CallArg::Spread(e) = a {
|
|
17733
|
+
Self::walk_expr_for_module_const_disqualifiers(e, g, bad);
|
|
17734
|
+
}
|
|
17735
|
+
}
|
|
17736
|
+
}
|
|
17737
|
+
Expr::NullishCoalesce { left, right, .. } => {
|
|
17738
|
+
Self::walk_expr_for_module_const_disqualifiers(left, g, bad);
|
|
17739
|
+
Self::walk_expr_for_module_const_disqualifiers(right, g, bad);
|
|
17740
|
+
}
|
|
17741
|
+
Expr::TypeOf { operand, .. } => {
|
|
17742
|
+
Self::walk_expr_for_module_const_disqualifiers(operand, g, bad);
|
|
17743
|
+
}
|
|
17744
|
+
Expr::TemplateLiteral { exprs, .. } => {
|
|
17745
|
+
for e in exprs {
|
|
17746
|
+
Self::walk_expr_for_module_const_disqualifiers(e, g, bad);
|
|
17747
|
+
}
|
|
17748
|
+
}
|
|
17749
|
+
// Leaves, listed explicitly so they do NOT reach the fallback below. A literal cannot
|
|
17750
|
+
// mention a binding, and there is one in nearly every expression in a real program —
|
|
17751
|
+
// routing those through a whole-subtree ident scan turned this walk superlinear and hung
|
|
17752
|
+
// the compiler outright (a `tish build` of `scii` spun at 100% CPU for 40+ minutes in the
|
|
17753
|
+
// frontend, no rustc involved). The fallback is for shapes that might CONTAIN a
|
|
17754
|
+
// reference, not for every constant in the file.
|
|
17755
|
+
Expr::Literal { .. } => {}
|
|
17756
|
+
// CONSERVATIVE DEFAULT — see the statement walker. An expression shape this match does
|
|
17757
|
+
// not model (today: `Await`, `JsxElement`, and anything the AST gains later) disqualifies
|
|
17758
|
+
// if the name appears anywhere beneath it, rather than being waved through.
|
|
17759
|
+
other => {
|
|
17760
|
+
if Self::expr_mentions_ident(other, g) {
|
|
17761
|
+
*bad = true;
|
|
17762
|
+
}
|
|
17763
|
+
}
|
|
17338
17764
|
}
|
|
17339
17765
|
}
|
|
17340
17766
|
|
|
@@ -17518,15 +17944,41 @@ impl Codegen {
|
|
|
17518
17944
|
)
|
|
17519
17945
|
}
|
|
17520
17946
|
|
|
17521
|
-
|
|
17947
|
+
/// The boxed bridge. `NumArrayBacking::Packed` is a `Vec<f64>`, so an `[i32; N]` hoist widens on
|
|
17948
|
+
/// the way out — this is the escape hatch where the array is handed to code that wants a
|
|
17949
|
+
/// `Value`, and it is a whole-array copy either way.
|
|
17950
|
+
fn module_const_f64_array_as_value(&self, static_name: &str) -> String {
|
|
17951
|
+
let elems = if self.module_const_int_statics.contains(static_name) {
|
|
17952
|
+
format!("{}.iter().map(|v| *v as f64)", static_name)
|
|
17953
|
+
} else {
|
|
17954
|
+
format!("{}.iter().copied()", static_name)
|
|
17955
|
+
};
|
|
17522
17956
|
format!(
|
|
17523
|
-
"Value::NumberArray(VmRef::new(tishlang_runtime::NumArrayBacking::Packed({}.
|
|
17524
|
-
|
|
17957
|
+
"Value::NumberArray(VmRef::new(tishlang_runtime::NumArrayBacking::Packed({}.collect::<Vec<f64>>())))",
|
|
17958
|
+
elems
|
|
17525
17959
|
)
|
|
17526
17960
|
}
|
|
17527
17961
|
|
|
17528
17962
|
fn emit_module_const_f64_arrays(&mut self) -> Result<(), CompileError> {
|
|
17529
17963
|
for (name, vals) in self.module_const_f64_arrays.clone() {
|
|
17964
|
+
let static_name = Self::module_const_static(name.as_str());
|
|
17965
|
+
// `[i32; N]` where the values permit it — half the ROM, and reads that stay in the
|
|
17966
|
+
// integer domain instead of round-tripping through soft float. See the field comment on
|
|
17967
|
+
// `module_const_int_statics`.
|
|
17968
|
+
if self.module_const_int_statics.contains(&static_name) {
|
|
17969
|
+
let lit = vals
|
|
17970
|
+
.iter()
|
|
17971
|
+
.map(|v| format!("{}", *v as i32))
|
|
17972
|
+
.collect::<Vec<_>>()
|
|
17973
|
+
.join(", ");
|
|
17974
|
+
self.writeln(&format!(
|
|
17975
|
+
"const {}: [i32; {}] = [{}];",
|
|
17976
|
+
static_name,
|
|
17977
|
+
vals.len(),
|
|
17978
|
+
lit
|
|
17979
|
+
));
|
|
17980
|
+
continue;
|
|
17981
|
+
}
|
|
17530
17982
|
let lit = vals
|
|
17531
17983
|
.iter()
|
|
17532
17984
|
.map(|v| Self::f64_lit(*v))
|
|
@@ -17534,7 +17986,7 @@ impl Codegen {
|
|
|
17534
17986
|
.join(", ");
|
|
17535
17987
|
self.writeln(&format!(
|
|
17536
17988
|
"const {}: [f64; {}] = [{}];",
|
|
17537
|
-
|
|
17989
|
+
static_name,
|
|
17538
17990
|
vals.len(),
|
|
17539
17991
|
lit
|
|
17540
17992
|
));
|
|
@@ -17938,35 +18390,74 @@ impl Codegen {
|
|
|
17938
18390
|
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
17939
18391
|
Self::walk_stmts_for_global_disqualifiers(statements, g, bad);
|
|
17940
18392
|
}
|
|
18393
|
+
// Conditions are visited, not just bodies. This walker had the same blind spot the
|
|
18394
|
+
// module-const one did: `while (i < X.length)` put a disqualifying use somewhere the
|
|
18395
|
+
// walk never reached, so the global was hoisted anyway and the emitted program
|
|
18396
|
+
// referenced a name that no longer existed. Latent here only because a scalar is rarely
|
|
18397
|
+
// spelled that way — the defect is identical.
|
|
17941
18398
|
Statement::If {
|
|
18399
|
+
cond,
|
|
17942
18400
|
then_branch,
|
|
17943
18401
|
else_branch,
|
|
17944
18402
|
..
|
|
17945
18403
|
} => {
|
|
18404
|
+
Self::walk_expr_for_global_disqualifiers(cond, g, bad);
|
|
17946
18405
|
Self::walk_stmt_for_global_disqualifiers(then_branch, g, bad);
|
|
17947
18406
|
if let Some(eb) = else_branch {
|
|
17948
18407
|
Self::walk_stmt_for_global_disqualifiers(eb, g, bad);
|
|
17949
18408
|
}
|
|
17950
18409
|
}
|
|
17951
|
-
Statement::While { body, .. }
|
|
17952
|
-
|
|
17953
|
-
|
|
17954
|
-
|
|
17955
|
-
|
|
18410
|
+
Statement::While { cond, body, .. } | Statement::DoWhile { body, cond, .. } => {
|
|
18411
|
+
Self::walk_expr_for_global_disqualifiers(cond, g, bad);
|
|
18412
|
+
Self::walk_stmt_for_global_disqualifiers(body, g, bad);
|
|
18413
|
+
}
|
|
18414
|
+
Statement::For {
|
|
18415
|
+
init,
|
|
18416
|
+
cond,
|
|
18417
|
+
update,
|
|
18418
|
+
body,
|
|
18419
|
+
..
|
|
18420
|
+
} => {
|
|
18421
|
+
if let Some(i) = init {
|
|
18422
|
+
Self::walk_stmt_for_global_disqualifiers(i, g, bad);
|
|
18423
|
+
}
|
|
18424
|
+
if let Some(c) = cond {
|
|
18425
|
+
Self::walk_expr_for_global_disqualifiers(c, g, bad);
|
|
18426
|
+
}
|
|
18427
|
+
if let Some(u) = update {
|
|
18428
|
+
Self::walk_expr_for_global_disqualifiers(u, g, bad);
|
|
18429
|
+
}
|
|
18430
|
+
Self::walk_stmt_for_global_disqualifiers(body, g, bad);
|
|
18431
|
+
}
|
|
18432
|
+
Statement::ForOf { iterable, body, .. }
|
|
18433
|
+
| Statement::ForIn {
|
|
18434
|
+
object: iterable,
|
|
18435
|
+
body,
|
|
18436
|
+
..
|
|
18437
|
+
} => {
|
|
18438
|
+
Self::walk_expr_for_global_disqualifiers(iterable, g, bad);
|
|
17956
18439
|
Self::walk_stmt_for_global_disqualifiers(body, g, bad);
|
|
17957
18440
|
}
|
|
17958
18441
|
Statement::Switch {
|
|
18442
|
+
expr,
|
|
17959
18443
|
cases,
|
|
17960
18444
|
default_body,
|
|
17961
18445
|
..
|
|
17962
18446
|
} => {
|
|
17963
|
-
|
|
18447
|
+
Self::walk_expr_for_global_disqualifiers(expr, g, bad);
|
|
18448
|
+
for (case_expr, stmts) in cases {
|
|
18449
|
+
if let Some(e) = case_expr {
|
|
18450
|
+
Self::walk_expr_for_global_disqualifiers(e, g, bad);
|
|
18451
|
+
}
|
|
17964
18452
|
Self::walk_stmts_for_global_disqualifiers(stmts, g, bad);
|
|
17965
18453
|
}
|
|
17966
18454
|
if let Some(d) = default_body {
|
|
17967
18455
|
Self::walk_stmts_for_global_disqualifiers(d, g, bad);
|
|
17968
18456
|
}
|
|
17969
18457
|
}
|
|
18458
|
+
Statement::Throw { value, .. } => {
|
|
18459
|
+
Self::walk_expr_for_global_disqualifiers(value, g, bad);
|
|
18460
|
+
}
|
|
17970
18461
|
Statement::Try {
|
|
17971
18462
|
body,
|
|
17972
18463
|
catch_body,
|
|
@@ -18165,6 +18656,151 @@ impl Codegen {
|
|
|
18165
18656
|
ok
|
|
18166
18657
|
}
|
|
18167
18658
|
|
|
18659
|
+
/// #597: every `let x: T[]` name in the program, at any nesting depth. These are the candidates
|
|
18660
|
+
/// whose escape has to be checked before they may lower to a native `Vec`.
|
|
18661
|
+
fn collect_annotated_array_names(stmts: &[Statement], out: &mut Vec<String>) {
|
|
18662
|
+
for s in stmts {
|
|
18663
|
+
Self::collect_annotated_array_names_stmt(s, out);
|
|
18664
|
+
}
|
|
18665
|
+
}
|
|
18666
|
+
|
|
18667
|
+
fn collect_annotated_array_names_stmt(s: &Statement, out: &mut Vec<String>) {
|
|
18668
|
+
match s {
|
|
18669
|
+
Statement::VarDecl {
|
|
18670
|
+
name,
|
|
18671
|
+
type_ann: Some(t),
|
|
18672
|
+
..
|
|
18673
|
+
} => {
|
|
18674
|
+
if matches!(crate::types::RustType::from_annotation(t), RustType::Vec(_)) {
|
|
18675
|
+
out.push(name.to_string());
|
|
18676
|
+
}
|
|
18677
|
+
}
|
|
18678
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
18679
|
+
for s in statements {
|
|
18680
|
+
Self::collect_annotated_array_names_stmt(s, out);
|
|
18681
|
+
}
|
|
18682
|
+
}
|
|
18683
|
+
Statement::If {
|
|
18684
|
+
then_branch,
|
|
18685
|
+
else_branch,
|
|
18686
|
+
..
|
|
18687
|
+
} => {
|
|
18688
|
+
Self::collect_annotated_array_names_stmt(then_branch, out);
|
|
18689
|
+
if let Some(e) = else_branch {
|
|
18690
|
+
Self::collect_annotated_array_names_stmt(e, out);
|
|
18691
|
+
}
|
|
18692
|
+
}
|
|
18693
|
+
Statement::While { body, .. }
|
|
18694
|
+
| Statement::DoWhile { body, .. }
|
|
18695
|
+
| Statement::ForOf { body, .. }
|
|
18696
|
+
| Statement::ForIn { body, .. }
|
|
18697
|
+
| Statement::FunDecl { body, .. } => {
|
|
18698
|
+
Self::collect_annotated_array_names_stmt(body, out);
|
|
18699
|
+
}
|
|
18700
|
+
Statement::For { init, body, .. } => {
|
|
18701
|
+
if let Some(i) = init {
|
|
18702
|
+
Self::collect_annotated_array_names_stmt(i, out);
|
|
18703
|
+
}
|
|
18704
|
+
Self::collect_annotated_array_names_stmt(body, out);
|
|
18705
|
+
}
|
|
18706
|
+
Statement::Switch {
|
|
18707
|
+
cases,
|
|
18708
|
+
default_body,
|
|
18709
|
+
..
|
|
18710
|
+
} => {
|
|
18711
|
+
for (_, stmts) in cases {
|
|
18712
|
+
for s in stmts {
|
|
18713
|
+
Self::collect_annotated_array_names_stmt(s, out);
|
|
18714
|
+
}
|
|
18715
|
+
}
|
|
18716
|
+
if let Some(stmts) = default_body {
|
|
18717
|
+
for s in stmts {
|
|
18718
|
+
Self::collect_annotated_array_names_stmt(s, out);
|
|
18719
|
+
}
|
|
18720
|
+
}
|
|
18721
|
+
}
|
|
18722
|
+
Statement::Try {
|
|
18723
|
+
body,
|
|
18724
|
+
catch_body,
|
|
18725
|
+
finally_body,
|
|
18726
|
+
..
|
|
18727
|
+
} => {
|
|
18728
|
+
Self::collect_annotated_array_names_stmt(body, out);
|
|
18729
|
+
if let Some(c) = catch_body {
|
|
18730
|
+
Self::collect_annotated_array_names_stmt(c, out);
|
|
18731
|
+
}
|
|
18732
|
+
if let Some(f) = finally_body {
|
|
18733
|
+
Self::collect_annotated_array_names_stmt(f, out);
|
|
18734
|
+
}
|
|
18735
|
+
}
|
|
18736
|
+
_ => {}
|
|
18737
|
+
}
|
|
18738
|
+
}
|
|
18739
|
+
|
|
18740
|
+
/// #597: array names that ESCAPE — handed to a call as a bare argument, or otherwise used as a
|
|
18741
|
+
/// whole value rather than indexed.
|
|
18742
|
+
///
|
|
18743
|
+
/// Reuses `scan_param_use`, the same analysis the read-only param path already trusts for this
|
|
18744
|
+
/// exact hazard: its `forwarded` flag is documented as "the callee could mutate the array, and a
|
|
18745
|
+
/// write to the caller's array would be lost on the owned copy". That is #597 from the other
|
|
18746
|
+
/// side, so the answer should come from one analysis rather than a second, subtly-different one.
|
|
18747
|
+
///
|
|
18748
|
+
/// Over-approximating only costs an array its native lowering. Under-approximating brings back a
|
|
18749
|
+
/// silently wrong result, so the bias is deliberate.
|
|
18750
|
+
fn collect_escaping_array_names(stmts: &[Statement], names: &[String]) -> std::collections::HashSet<String> {
|
|
18751
|
+
// `for_each_stmt_expr` deliberately does NOT descend into `FunDecl` bodies, so the forwarding
|
|
18752
|
+
// call is normally invisible to it — the array is declared inside a function and passed
|
|
18753
|
+
// inside that same function, which is exactly the reported shape. Gather nested bodies as
|
|
18754
|
+
// additional roots first.
|
|
18755
|
+
fn add_roots<'a>(s: &'a Statement, roots: &mut Vec<&'a Statement>) {
|
|
18756
|
+
roots.push(s);
|
|
18757
|
+
match s {
|
|
18758
|
+
Statement::FunDecl { body, .. }
|
|
18759
|
+
| Statement::While { body, .. }
|
|
18760
|
+
| Statement::DoWhile { body, .. }
|
|
18761
|
+
| Statement::ForOf { body, .. }
|
|
18762
|
+
| Statement::ForIn { body, .. }
|
|
18763
|
+
| Statement::For { body, .. } => add_roots(body, roots),
|
|
18764
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
18765
|
+
for s in statements {
|
|
18766
|
+
add_roots(s, roots);
|
|
18767
|
+
}
|
|
18768
|
+
}
|
|
18769
|
+
Statement::If {
|
|
18770
|
+
then_branch,
|
|
18771
|
+
else_branch,
|
|
18772
|
+
..
|
|
18773
|
+
} => {
|
|
18774
|
+
add_roots(then_branch, roots);
|
|
18775
|
+
if let Some(e) = else_branch {
|
|
18776
|
+
add_roots(e, roots);
|
|
18777
|
+
}
|
|
18778
|
+
}
|
|
18779
|
+
_ => {}
|
|
18780
|
+
}
|
|
18781
|
+
}
|
|
18782
|
+
let mut roots: Vec<&Statement> = Vec::new();
|
|
18783
|
+
for s in stmts {
|
|
18784
|
+
add_roots(s, &mut roots);
|
|
18785
|
+
}
|
|
18786
|
+
let mut out = std::collections::HashSet::new();
|
|
18787
|
+
for n in names {
|
|
18788
|
+
let mut f = ParamUse::default();
|
|
18789
|
+
for s in &roots {
|
|
18790
|
+
Self::for_each_stmt_expr(s, &mut |e| Self::scan_param_use(e, n, false, &mut f));
|
|
18791
|
+
}
|
|
18792
|
+
// `forwarded` ONLY, not `escaped`. `escaped` covers any whole-value use — including
|
|
18793
|
+
// `return arr`, which hands ownership out and does not create the aliasing hazard this
|
|
18794
|
+
// is about. Using it deopted arrays that are legitimately native
|
|
18795
|
+
// (`numeric_returning_fn_keeps_pushed_array_native`). #597 is specifically "passed as a
|
|
18796
|
+
// call argument", which is what `forwarded` means.
|
|
18797
|
+
if f.forwarded {
|
|
18798
|
+
out.insert(n.clone());
|
|
18799
|
+
}
|
|
18800
|
+
}
|
|
18801
|
+
out
|
|
18802
|
+
}
|
|
18803
|
+
|
|
18168
18804
|
fn for_each_expr_root(stmts: &[Statement], f: &mut dyn FnMut(&Expr)) {
|
|
18169
18805
|
for s in stmts {
|
|
18170
18806
|
Self::for_each_expr_root_stmt(s, f);
|
|
@@ -20084,6 +20720,14 @@ impl Codegen {
|
|
|
20084
20720
|
}
|
|
20085
20721
|
}
|
|
20086
20722
|
}
|
|
20723
|
+
// #630: A NAME REMOVED HERE IS REJECTED FOR GOOD. `collect_bad_vec_callsites` is a
|
|
20724
|
+
// SOUNDNESS verdict — the fn is called somewhere in a way the native-vec ABI cannot
|
|
20725
|
+
// represent — and that fact is a property of the program, which does not change during this
|
|
20726
|
+
// analysis. Re-admitting it later can only produce the same wrong answer.
|
|
20727
|
+
//
|
|
20728
|
+
// Recording it is also what makes the forwarding fixpoint below terminate; see the note
|
|
20729
|
+
// there.
|
|
20730
|
+
let mut rejected: std::collections::HashSet<String> = std::collections::HashSet::new();
|
|
20087
20731
|
loop {
|
|
20088
20732
|
let mut remove: Vec<String> = Vec::new();
|
|
20089
20733
|
for s in &program.statements {
|
|
@@ -20095,6 +20739,7 @@ impl Codegen {
|
|
|
20095
20739
|
break;
|
|
20096
20740
|
}
|
|
20097
20741
|
for n in remove {
|
|
20742
|
+
rejected.insert(n.clone());
|
|
20098
20743
|
sigs.remove(&n);
|
|
20099
20744
|
}
|
|
20100
20745
|
if sigs.is_empty() {
|
|
@@ -20130,6 +20775,22 @@ impl Codegen {
|
|
|
20130
20775
|
if Self::ann_is_simple(return_type.as_ref(), "fixed") {
|
|
20131
20776
|
continue;
|
|
20132
20777
|
}
|
|
20778
|
+
// #630: AND IT MUST NOT RE-ADD A NAME THE REMOVAL PASS ALREADY REJECTED. This is
|
|
20779
|
+
// what makes the analysis terminate. Without it the two fixpoints fight: the
|
|
20780
|
+
// forwarding pass admits `F`, the removal pass below proves `F` unsound and
|
|
20781
|
+
// drops it, `added` is true so the outer loop runs again, and `F` — re-evaluated
|
|
20782
|
+
// against a program that has not changed — is admitted again. Forever.
|
|
20783
|
+
//
|
|
20784
|
+
// The only exits were `!added` and `sigs.is_empty()`, and an oscillating pair
|
|
20785
|
+
// satisfies neither: measured on the reporter's project it sat at a steady
|
|
20786
|
+
// `sigs=3` for 200+ iterations at 100% CPU, with no cargo child and nothing
|
|
20787
|
+
// printed, which is why it read as a dead build rather than a slow one.
|
|
20788
|
+
//
|
|
20789
|
+
// With this guard every outer iteration must admit a name never admitted before,
|
|
20790
|
+
// so the loop is bounded by the number of top-level fns.
|
|
20791
|
+
if rejected.contains(name.as_ref()) {
|
|
20792
|
+
continue;
|
|
20793
|
+
}
|
|
20133
20794
|
if let Some(sig) = Self::infer_vec_fn_sig_forwarding(params, body, &sigs) {
|
|
20134
20795
|
let has_array_param = sig
|
|
20135
20796
|
.params
|
|
@@ -20156,6 +20817,7 @@ impl Codegen {
|
|
|
20156
20817
|
break;
|
|
20157
20818
|
}
|
|
20158
20819
|
for n in remove {
|
|
20820
|
+
rejected.insert(n.clone());
|
|
20159
20821
|
sigs.remove(&n);
|
|
20160
20822
|
}
|
|
20161
20823
|
if sigs.is_empty() {
|
|
@@ -24862,33 +25524,47 @@ impl Codegen {
|
|
|
24862
25524
|
iv
|
|
24863
25525
|
)
|
|
24864
25526
|
};
|
|
24865
|
-
let
|
|
24866
|
-
|
|
25527
|
+
let is_int = self.module_const_int_statics.contains(&static_name);
|
|
25528
|
+
// The proven-in-bounds read on an integer array is the whole point: a
|
|
25529
|
+
// plain word load, reported as I32 so the consumer stays in the integer
|
|
25530
|
+
// domain instead of converting back and forth through f64.
|
|
25531
|
+
if in_bounds {
|
|
25532
|
+
let access = format!("{}[{}]", static_name, idx_usize);
|
|
25533
|
+
return Ok((
|
|
25534
|
+
access,
|
|
25535
|
+
if is_int { RustType::I32 } else { RustType::F64 },
|
|
25536
|
+
));
|
|
25537
|
+
}
|
|
25538
|
+
let const_len = self
|
|
25539
|
+
.cum_static_and_len(name.as_ref())
|
|
25540
|
+
.map(|(_, n)| n)
|
|
25541
|
+
.or_else(|| {
|
|
25542
|
+
self.module_const_f64_arrays
|
|
25543
|
+
.get(name.as_ref())
|
|
25544
|
+
.map(|v| v.len())
|
|
25545
|
+
})
|
|
25546
|
+
.unwrap_or(0);
|
|
25547
|
+
// OUT-OF-RANGE STAYS f64. An unproven index still has to be able to
|
|
25548
|
+
// answer `NaN`, and there is no `i32` that means "absent" — picking a
|
|
25549
|
+
// sentinel here would silently turn a missing element into a real value.
|
|
25550
|
+
// So the bounds-checked form keeps today's exact semantics and only the
|
|
25551
|
+
// STORAGE narrows; the integer win is claimed above, where the index is
|
|
25552
|
+
// proven and no sentinel is reachable.
|
|
25553
|
+
let elem = if is_int {
|
|
25554
|
+
format!("{}[_i] as f64", static_name)
|
|
24867
25555
|
} else {
|
|
24868
|
-
|
|
24869
|
-
|
|
24870
|
-
|
|
24871
|
-
|
|
24872
|
-
|
|
24873
|
-
|
|
24874
|
-
|
|
24875
|
-
|
|
24876
|
-
|
|
24877
|
-
|
|
24878
|
-
|
|
24879
|
-
|
|
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
|
-
}
|
|
25556
|
+
format!("{}[_i]", static_name)
|
|
25557
|
+
};
|
|
25558
|
+
let access = if const_len > 0 {
|
|
25559
|
+
format!(
|
|
25560
|
+
"{{ let _i = {}; if _i < {} {{ {} }} else {{ f64::NAN }} }}",
|
|
25561
|
+
idx_usize, const_len, elem
|
|
25562
|
+
)
|
|
25563
|
+
} else {
|
|
25564
|
+
format!(
|
|
25565
|
+
"{{ let _i = {}; if _i < {}.len() {{ {} }} else {{ f64::NAN }} }}",
|
|
25566
|
+
idx_usize, static_name, elem
|
|
25567
|
+
)
|
|
24892
25568
|
};
|
|
24893
25569
|
return Ok((access, RustType::F64));
|
|
24894
25570
|
}
|
|
@@ -24960,6 +25636,19 @@ impl Codegen {
|
|
|
24960
25636
|
// `borrow()` guard a non-Copy element must be cloned OUT (it can't be
|
|
24961
25637
|
// moved from the borrow); a plain local keeps the direct place.
|
|
24962
25638
|
_ if idx_wrapped => format!("{}[{}].clone()", esc_obj, idx_ref),
|
|
25639
|
+
// #600: a STRING element is cloned even unwrapped. `let s: string =
|
|
25640
|
+
// NAMES[i]` binds the element, and binding a non-Copy value out of a
|
|
25641
|
+
// `Vec` is a move — `error[E0507]: cannot move out of index`. The
|
|
25642
|
+
// obvious spelling was the one that did not compile, with indexing
|
|
25643
|
+
// straight into a call as the only working form.
|
|
25644
|
+
//
|
|
25645
|
+
// Scoped to String deliberately. The struct case (#571) looks
|
|
25646
|
+
// identical but must NOT clone here: `states[id].timer` projects a
|
|
25647
|
+
// field out of the place, and cloning would copy the whole struct on
|
|
25648
|
+
// every field read — the O(n)-per-read footgun #558 exists about.
|
|
25649
|
+
// A string is already copied at every boxed boundary, so the clone
|
|
25650
|
+
// costs nothing that was not already being paid.
|
|
25651
|
+
RustType::String => format!("{}[{}].clone()", esc_obj, idx_ref),
|
|
24963
25652
|
_ => format!("{}[{}]", esc_obj, idx_ref),
|
|
24964
25653
|
};
|
|
24965
25654
|
// #567: wrap the wrapped-Vec read in a block that pre-binds the index
|