@tishlang/tish-lsp 3.2.2 → 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-lsp +0 -0
- package/crates/tish_compile/src/codegen.rs +1838 -162
- package/crates/tish_compile/src/types.rs +6 -0
- package/crates/tish_compile/tests/perf_codegen_i32_native_abi.rs +113 -0
- package/crates/tish_compile/tests/regr_gba_lowered_call_sites.rs +891 -0
- package/crates/tish_lsp/Cargo.toml +1 -1
- package/crates/tish_native/src/build.rs +24 -6
- package/crates/tish_runtime_gba/src/gba.rs +26 -6
- package/crates/tish_runtime_gba/src/lib.rs +29 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-lsp +0 -0
- package/platform/darwin-x64/tish-lsp +0 -0
- package/platform/linux-arm64/tish-lsp +0 -0
- package/platform/linux-x64/tish-lsp +0 -0
- package/platform/win32-x64/tish-lsp.exe +0 -0
|
@@ -834,6 +834,7 @@ pub fn compile_with_native_modules_emit_exports(
|
|
|
834
834
|
|| m.crate_name == "tishlang_ios"
|
|
835
835
|
});
|
|
836
836
|
g.emit_program(&program)?;
|
|
837
|
+
g.emit_gba_key_table();
|
|
837
838
|
Ok(g.output)
|
|
838
839
|
}
|
|
839
840
|
|
|
@@ -998,6 +999,10 @@ pub(crate) enum VecRetKind {
|
|
|
998
999
|
F64,
|
|
999
1000
|
Unit,
|
|
1000
1001
|
VecF64,
|
|
1002
|
+
/// #605. Every `return` yields a string, so the fn can be a native `-> String` instead of a
|
|
1003
|
+
/// boxed closure. OWNED, not `&str`: a returned string outlives the frame that built it, and a
|
|
1004
|
+
/// borrow would need a lifetime the generated free fn has nowhere to name.
|
|
1005
|
+
Str,
|
|
1001
1006
|
}
|
|
1002
1007
|
|
|
1003
1008
|
#[derive(Debug, Clone)]
|
|
@@ -1097,6 +1102,21 @@ pub(crate) struct Codegen {
|
|
|
1097
1102
|
/// Top-level `let name = [f64 literals…]` never reassigned — emitted as `const G_name: [f64; N]`
|
|
1098
1103
|
/// for direct indexing from native fns (fasta `codes`/`probs`).
|
|
1099
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>,
|
|
1100
1120
|
/// Precomputed cumulative arrays for `module_const_f64_arrays` used as `cumulative(p)` input
|
|
1101
1121
|
/// (`probs` → `G_probs_cum`).
|
|
1102
1122
|
module_const_f64_cum: std::collections::HashMap<String, String>,
|
|
@@ -1120,6 +1140,12 @@ pub(crate) struct Codegen {
|
|
|
1120
1140
|
/// free `fn f_native(f64,..)->f64` (all params `: number`, returns `number`, native-safe
|
|
1121
1141
|
/// body). Direct calls to these route to the native fn, bypassing the boxed `value_call`.
|
|
1122
1142
|
native_fns: std::collections::HashSet<String>,
|
|
1143
|
+
/// Set while emitting the body of an i32-ABI native fn, so `return` coerces to `i32`.
|
|
1144
|
+
native_fn_abi_i32: bool,
|
|
1145
|
+
/// The subset of `native_fns` emitted with an `fn(i32, ..) -> i32` signature instead of the
|
|
1146
|
+
/// f64 one — every parameter and the return annotated `: i32`. On ARM7TDMI this is the whole
|
|
1147
|
+
/// point: an f64 ABI makes every argument and every return a soft-float conversion.
|
|
1148
|
+
native_fns_i32: std::collections::HashSet<String>,
|
|
1123
1149
|
/// True while emitting an M5 `fn name_native` body — keeps VarDecl inits on the native path.
|
|
1124
1150
|
native_fn_body_emit: bool,
|
|
1125
1151
|
/// M5 fn currently being emitted (`mandel_native`, `fastaRandom_native`, …).
|
|
@@ -1209,6 +1235,17 @@ pub(crate) struct Codegen {
|
|
|
1209
1235
|
/// `RustType::Value`. This is the rust-AOT analogue of the VM array-JIT bailing to the
|
|
1210
1236
|
/// interpreter on a non-numeric element. See `collect_demoted_numeric_locals`.
|
|
1211
1237
|
demoted_numeric_locals: std::collections::HashSet<String>,
|
|
1238
|
+
/// Top-level fns whose EVERY return is provably numeric → the native type a call to one yields.
|
|
1239
|
+
///
|
|
1240
|
+
/// Without it a call is opaque, so a local assigned from one demotes to a boxed `Value` and
|
|
1241
|
+
/// every arithmetic consumer falls back to the generic `ops::add` plus two soft-float
|
|
1242
|
+
/// conversions — most of the ~92 ticks a user-fn call costs on GBA
|
|
1243
|
+
/// (`docs/issues/user-fn-call-cost-on-gba.md`).
|
|
1244
|
+
///
|
|
1245
|
+
/// KEYED ON A PROOF, NOT THE ANNOTATION. `function f(): i32 { return "no" }` compiles and
|
|
1246
|
+
/// returns the string — return types are erased — so trusting the declaration would reinterpret
|
|
1247
|
+
/// a `String` as a number.
|
|
1248
|
+
user_fn_ret: std::collections::HashMap<String, RustType>,
|
|
1212
1249
|
/// Integer-range lattice (#174): names of `f64` locals the analysis proves always hold an
|
|
1213
1250
|
/// integer within `[min, max]`, both strictly inside `(-2^53, 2^53)` so `as i64` is exact and
|
|
1214
1251
|
/// `i64` arithmetic is bit-identical to the `f64` the interpreter/VM use. Lets the codegen
|
|
@@ -1293,6 +1330,16 @@ pub(crate) struct Codegen {
|
|
|
1293
1330
|
/// `mod __scheme_<name>_<j> { … }` blocks and their per-scheme registration. The handle a tish
|
|
1294
1331
|
/// name binds to is its index AMONG same-scheme entries (= its slot in that scheme's arena).
|
|
1295
1332
|
scheme_files: Vec<(String, String)>,
|
|
1333
|
+
/// GBA only: the program-wide intern table for OBJECT-LITERAL KEYS, in first-seen order,
|
|
1334
|
+
/// plus the name→index map. `RefCell` so `cached_object_key` can stay `&self`.
|
|
1335
|
+
///
|
|
1336
|
+
/// ⚠️ This is a HEAP fix, not a tidiness one. Off-GBA every key site gets its own `OnceLock`,
|
|
1337
|
+
/// which dedupes repeated evaluation of ONE site but never across sites — and a data table is
|
|
1338
|
+
/// N sibling object literals, so N separate sites each allocate their own copy of every key
|
|
1339
|
+
/// name. A real ROM measured 1,890 key sites over 474 distinct names: 1,416 duplicate `Arc<str>`
|
|
1340
|
+
/// allocations that exist only to spell "name" and "id" again. One table, indexed, kills all of
|
|
1341
|
+
/// them. See `cached_object_key`.
|
|
1342
|
+
gba_keys: std::cell::RefCell<(Vec<String>, std::collections::HashMap<String, usize>)>,
|
|
1296
1343
|
/// Program links `tish:macos` / `tish:ios` — skip HeadlessHost install.
|
|
1297
1344
|
has_native_ui_host: bool,
|
|
1298
1345
|
/// Program references browser global `document` — inject tish-canvas.
|
|
@@ -1351,12 +1398,16 @@ impl Codegen {
|
|
|
1351
1398
|
refcell_wrapped_vars: std::collections::HashSet::new(),
|
|
1352
1399
|
native_numeric_globals: std::collections::HashMap::new(),
|
|
1353
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(),
|
|
1354
1403
|
module_const_f64_cum: std::collections::HashMap::new(),
|
|
1355
1404
|
module_const_f64_aliases: std::collections::HashMap::new(),
|
|
1356
1405
|
map_instance_locals: std::collections::HashSet::new(),
|
|
1357
1406
|
collection_instance_locals: std::collections::HashSet::new(),
|
|
1358
1407
|
hoisted_string_chars: std::collections::HashMap::new(),
|
|
1359
1408
|
native_fns: std::collections::HashSet::new(),
|
|
1409
|
+
native_fns_i32: std::collections::HashSet::new(),
|
|
1410
|
+
native_fn_abi_i32: false,
|
|
1360
1411
|
native_fn_body_emit: false,
|
|
1361
1412
|
native_fn_emit_name: None,
|
|
1362
1413
|
native_fn_literal_args: std::collections::HashMap::new(),
|
|
@@ -1383,6 +1434,7 @@ impl Codegen {
|
|
|
1383
1434
|
aggregate_array_locals: std::collections::HashSet::new(),
|
|
1384
1435
|
agg_cur_ret: None,
|
|
1385
1436
|
demoted_numeric_locals: std::collections::HashSet::new(),
|
|
1437
|
+
user_fn_ret: std::collections::HashMap::new(),
|
|
1386
1438
|
int_range_locals: std::collections::HashMap::new(),
|
|
1387
1439
|
diag_coord_indices: std::collections::HashSet::new(),
|
|
1388
1440
|
int_i32_vec_locals: std::collections::HashSet::new(),
|
|
@@ -1408,6 +1460,7 @@ impl Codegen {
|
|
|
1408
1460
|
emit_mode: crate::NativeEmitMode::DesktopBin,
|
|
1409
1461
|
entry_exports: Vec::new(),
|
|
1410
1462
|
scheme_files: Vec::new(),
|
|
1463
|
+
gba_keys: std::cell::RefCell::new((Vec::new(), std::collections::HashMap::new())),
|
|
1411
1464
|
has_native_ui_host: false,
|
|
1412
1465
|
program_uses_document: false,
|
|
1413
1466
|
cse_subst: Vec::new(),
|
|
@@ -2393,6 +2446,20 @@ impl Codegen {
|
|
|
2393
2446
|
"box", "do", "final", "macro", "override", "priv", "try", "typeof", "unsized",
|
|
2394
2447
|
"virtual", "yield",
|
|
2395
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
|
+
}
|
|
2396
2463
|
if RUST_KEYWORDS.contains(&name) {
|
|
2397
2464
|
Cow::Owned(format!("r#{}", name))
|
|
2398
2465
|
} else {
|
|
@@ -3112,27 +3179,79 @@ impl Codegen {
|
|
|
3112
3179
|
// M5 (default-on via native_opts_enabled; TISH_NATIVE_OPT=0 opts out): emit a parallel
|
|
3113
3180
|
// native `fn f_native` for each eligible top-level numeric fn at top level; direct calls
|
|
3114
3181
|
// route to it in emit_typed_expr.
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3182
|
+
if crate::native_opts_enabled() {
|
|
3183
|
+
// MODULE STATE GETS A TOP-LEVEL HOME ON GBA TOO (#594). These passes used to be skipped
|
|
3184
|
+
// there because the numeric-global one emits `thread_local!`, which no_std has not got —
|
|
3185
|
+
// and the const-array pass was collateral, gated only because it shared the block. The
|
|
3186
|
+
// consequence was the bug this issue reports: a lowered free `fn` is emitted at top
|
|
3187
|
+
// level, so every module binding it reads is a local of `run()` that does not exist at
|
|
3188
|
+
// that scope, and the emitted crate fails with E0425.
|
|
3189
|
+
//
|
|
3190
|
+
// A const numeric array is `const G_X: [f64; N] = [..]` — no std in it at all. A mutable
|
|
3191
|
+
// numeric global becomes `SingleCore<Cell<f64>>`, the facade's single-core static whose
|
|
3192
|
+
// `with()` deliberately mirrors `LocalKey::with` (compat.rs), so every read and write the
|
|
3193
|
+
// emitter already produces (`G.with(|c| c.get())`) is unchanged.
|
|
3194
|
+
let gba = self.emit_mode == crate::NativeEmitMode::Gba;
|
|
3118
3195
|
self.native_numeric_globals =
|
|
3119
3196
|
Self::collect_native_numeric_globals(&program.statements);
|
|
3120
3197
|
if !self.native_numeric_globals.is_empty() {
|
|
3121
|
-
|
|
3198
|
+
if gba {
|
|
3199
|
+
self.emit_native_numeric_global_singlecore()?;
|
|
3200
|
+
} else {
|
|
3201
|
+
self.emit_native_numeric_global_tls()?;
|
|
3202
|
+
}
|
|
3122
3203
|
self.writeln("");
|
|
3123
3204
|
}
|
|
3124
3205
|
self.module_const_f64_arrays =
|
|
3125
3206
|
Self::collect_module_const_f64_arrays(&program.statements);
|
|
3126
|
-
self.module_const_f64_cum =
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3207
|
+
self.module_const_f64_cum = Self::collect_module_const_cum(
|
|
3208
|
+
&program.statements,
|
|
3209
|
+
&self.module_const_f64_arrays,
|
|
3210
|
+
);
|
|
3211
|
+
self.module_const_f64_aliases = Self::collect_module_const_aliases(
|
|
3212
|
+
&program.statements,
|
|
3213
|
+
&self.module_const_f64_cum,
|
|
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
|
+
}
|
|
3130
3246
|
if !self.module_const_f64_arrays.is_empty() {
|
|
3131
3247
|
self.emit_module_const_f64_arrays()?;
|
|
3132
3248
|
self.writeln("");
|
|
3133
3249
|
}
|
|
3134
|
-
let native_vec_names: std::collections::HashSet<String> =
|
|
3135
|
-
|
|
3250
|
+
let native_vec_names: std::collections::HashSet<String> = if gba {
|
|
3251
|
+
std::collections::HashSet::new()
|
|
3252
|
+
} else {
|
|
3253
|
+
Self::detect_native_vec_fns(program).keys().cloned().collect()
|
|
3254
|
+
};
|
|
3136
3255
|
let mut global_names: std::collections::HashSet<String> =
|
|
3137
3256
|
self.native_numeric_globals.keys().cloned().collect();
|
|
3138
3257
|
global_names.extend(self.module_const_f64_arrays.keys().cloned());
|
|
@@ -3141,6 +3260,21 @@ impl Codegen {
|
|
|
3141
3260
|
&global_names,
|
|
3142
3261
|
&native_vec_names,
|
|
3143
3262
|
);
|
|
3263
|
+
// Which of the survivors carry the all-`i32` signature. Computed AFTER the fixpoint,
|
|
3264
|
+
// so a function the proof rejected never reaches the i32 ABI either.
|
|
3265
|
+
self.native_fns_i32 = program
|
|
3266
|
+
.statements
|
|
3267
|
+
.iter()
|
|
3268
|
+
.filter_map(|s| match s {
|
|
3269
|
+
Statement::FunDecl { name, params, return_type, .. }
|
|
3270
|
+
if self.native_fns.contains(name.as_ref())
|
|
3271
|
+
&& Self::fn_sig_all_i32(params, return_type) =>
|
|
3272
|
+
{
|
|
3273
|
+
Some(name.to_string())
|
|
3274
|
+
}
|
|
3275
|
+
_ => None,
|
|
3276
|
+
})
|
|
3277
|
+
.collect();
|
|
3144
3278
|
self.native_fn_literal_args =
|
|
3145
3279
|
Self::collect_native_fn_literal_calls(&program.statements, &self.native_fns);
|
|
3146
3280
|
self.native_fn_param_names =
|
|
@@ -3149,11 +3283,17 @@ impl Codegen {
|
|
|
3149
3283
|
Self::collect_native_lcg_fns(&program.statements, &self.native_fns);
|
|
3150
3284
|
// #381 — fns on a call-graph cycle get guarded rotation copies (default ON;
|
|
3151
3285
|
// TISH_NATIVE_RECUR_GUARD=0 restores the plain unguarded emission).
|
|
3152
|
-
self.native_recur_fns = if crate::native_recur_guard_enabled()
|
|
3286
|
+
self.native_recur_fns = if crate::native_recur_guard_enabled()
|
|
3287
|
+
&& self.emit_mode != crate::NativeEmitMode::Gba
|
|
3288
|
+
{
|
|
3153
3289
|
Self::compute_native_recur_fns(&program.statements, &self.native_fns)
|
|
3154
3290
|
} else {
|
|
3155
3291
|
std::collections::HashMap::new()
|
|
3156
3292
|
};
|
|
3293
|
+
// A fn on a call-graph cycle keeps the f64 ABI: its stack-guard bail unwinds through a
|
|
3294
|
+
// NaN sentinel (#381), and an `i32` return has no value to spare for that. Recursion is
|
|
3295
|
+
// not what the i32 shape exists for.
|
|
3296
|
+
self.native_fns_i32.retain(|n| !self.native_recur_fns.contains_key(n));
|
|
3157
3297
|
if !self.native_fns.is_empty() {
|
|
3158
3298
|
self.emit_native_fns(&program.statements)?;
|
|
3159
3299
|
self.writeln("");
|
|
@@ -3196,6 +3336,9 @@ impl Codegen {
|
|
|
3196
3336
|
// `VarDecl` lowers them as boxed `Value` rather than native `f64` (else the store coerces
|
|
3197
3337
|
// and panics on a JS string-concat result like `s = s + arr[i]`). See
|
|
3198
3338
|
// `collect_demoted_numeric_locals` / `demoted_numeric_locals`.
|
|
3339
|
+
// BEFORE the demotion pass, which consults it: a local assigned from a provably-numeric
|
|
3340
|
+
// call must stop demoting, which is half of where the win comes from.
|
|
3341
|
+
self.user_fn_ret = self.collect_user_fn_rets(&program.statements);
|
|
3199
3342
|
self.demoted_numeric_locals = self.collect_demoted_numeric_locals(&program.statements);
|
|
3200
3343
|
self.int_valued_locals = Self::collect_int_valued_locals(&program.statements);
|
|
3201
3344
|
self.int_range_locals = self.collect_int_range_locals(&program.statements);
|
|
@@ -3594,7 +3737,25 @@ impl Codegen {
|
|
|
3594
3737
|
if self.is_async {
|
|
3595
3738
|
self.async_context_stack.push(true); // run() body is async Rust context
|
|
3596
3739
|
}
|
|
3597
|
-
|
|
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))?;
|
|
3598
3759
|
if self.is_async {
|
|
3599
3760
|
self.async_context_stack.pop();
|
|
3600
3761
|
}
|
|
@@ -3862,11 +4023,64 @@ impl Codegen {
|
|
|
3862
4023
|
/// unchanged, and `PropMap` dedup + hidden-class shapes key by text, not pointer — so shapes,
|
|
3863
4024
|
/// insertion order, and `Object.keys` are all unaffected. Same inline-`static` idiom as the
|
|
3864
4025
|
/// per-site `PropIC` caches.
|
|
4026
|
+
/// Append the GBA object-key intern table. Called ONCE, after the whole program is emitted,
|
|
4027
|
+
/// because the key set is only complete then — Rust does not care that a `fn` is defined below
|
|
4028
|
+
/// its callers.
|
|
4029
|
+
///
|
|
4030
|
+
/// ⚠️ `static mut` + `&raw mut` rather than a `OnceLock`: the target is `#![no_std]` and the GBA
|
|
4031
|
+
/// is single-core, so there is no thread to race with. Taking a raw pointer avoids the
|
|
4032
|
+
/// `static_mut_refs` lint that a plain `&mut __TISH_KEYS` would trip.
|
|
4033
|
+
fn emit_gba_key_table(&mut self) {
|
|
4034
|
+
if self.emit_mode != crate::NativeEmitMode::Gba {
|
|
4035
|
+
return;
|
|
4036
|
+
}
|
|
4037
|
+
let names: Vec<String> = self.gba_keys.borrow().0.clone();
|
|
4038
|
+
if names.is_empty() {
|
|
4039
|
+
return;
|
|
4040
|
+
}
|
|
4041
|
+
let list = names
|
|
4042
|
+
.iter()
|
|
4043
|
+
.map(|k| format!("{:?}", k))
|
|
4044
|
+
.collect::<Vec<_>>()
|
|
4045
|
+
.join(", ");
|
|
4046
|
+
self.write(&format!(
|
|
4047
|
+
"\n// Object-literal keys, interned program-wide: {} distinct names.\n\
|
|
4048
|
+
static __TISH_KEY_NAMES: [&str; {}] = [{}];\n\
|
|
4049
|
+
static mut __TISH_KEYS: Option<Vec<Arc<str>>> = None;\n\
|
|
4050
|
+
fn __tish_key(i: usize) -> Arc<str> {{\n\
|
|
4051
|
+
\x20 unsafe {{\n\
|
|
4052
|
+
\x20 let t = &raw mut __TISH_KEYS;\n\
|
|
4053
|
+
\x20 if (*t).is_none() {{\n\
|
|
4054
|
+
\x20 *t = Some(__TISH_KEY_NAMES.iter().map(|s| Arc::from(*s)).collect());\n\
|
|
4055
|
+
\x20 }}\n\
|
|
4056
|
+
\x20 match (*t).as_ref() {{\n\
|
|
4057
|
+
\x20 Some(v) => v[i].clone(),\n\
|
|
4058
|
+
\x20 None => Arc::from(__TISH_KEY_NAMES[i]),\n\
|
|
4059
|
+
\x20 }}\n\
|
|
4060
|
+
\x20 }}\n\
|
|
4061
|
+
}}\n",
|
|
4062
|
+
names.len(),
|
|
4063
|
+
names.len(),
|
|
4064
|
+
list
|
|
4065
|
+
));
|
|
4066
|
+
}
|
|
4067
|
+
|
|
3865
4068
|
fn cached_object_key(&self, k: &str) -> String {
|
|
3866
4069
|
if self.emit_mode == crate::NativeEmitMode::Gba {
|
|
3867
|
-
// No `OnceLock` on no_std GBA
|
|
3868
|
-
//
|
|
3869
|
-
|
|
4070
|
+
// No `OnceLock` on no_std GBA — and a per-site cache would be the wrong tool anyway.
|
|
4071
|
+
// Intern PROGRAM-WIDE instead: every distinct key name is allocated once, at first use,
|
|
4072
|
+
// and each site clones an `Arc` (= `Rc`) out of one table. See the `gba_keys` field.
|
|
4073
|
+
let mut t = self.gba_keys.borrow_mut();
|
|
4074
|
+
let idx = match t.1.get(k) {
|
|
4075
|
+
Some(&i) => i,
|
|
4076
|
+
None => {
|
|
4077
|
+
let i = t.0.len();
|
|
4078
|
+
t.0.push(k.to_string());
|
|
4079
|
+
t.1.insert(k.to_string(), i);
|
|
4080
|
+
i
|
|
4081
|
+
}
|
|
4082
|
+
};
|
|
4083
|
+
return format!("__tish_key({})", idx);
|
|
3870
4084
|
}
|
|
3871
4085
|
format!(
|
|
3872
4086
|
"{{ static K: ::std::sync::OnceLock<::std::sync::Arc<str>> = \
|
|
@@ -3967,9 +4181,26 @@ impl Codegen {
|
|
|
3967
4181
|
// `emit_int32_operand` (a `>>> 0` result is u32 reinterpreted to i32; signed
|
|
3968
4182
|
// bitwise ops yield i32 directly) — so store the i32 with NO `f64` round-trip.
|
|
3969
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
|
+
};
|
|
3970
4201
|
if let Some(int_code) = self.emit_int32_operand(value)? {
|
|
3971
4202
|
let escaped = Self::escape_ident(name.as_ref());
|
|
3972
|
-
return Ok(format!("
|
|
4203
|
+
return Ok(store(&escaped, format!("({}) as i32", int_code)));
|
|
3973
4204
|
}
|
|
3974
4205
|
let (val_code, val_ty) = self.emit_typed_expr(value)?;
|
|
3975
4206
|
// An RHS that is ALREADY an integer stores straight into the register. `d = d + 1`
|
|
@@ -3978,7 +4209,7 @@ impl Codegen {
|
|
|
3978
4209
|
// out of it was the whole cost of incrementing an annotated counter.
|
|
3979
4210
|
let escaped_int = Self::escape_ident(name.as_ref());
|
|
3980
4211
|
if val_ty.is_integer_scalar() {
|
|
3981
|
-
return Ok(format!("
|
|
4212
|
+
return Ok(store(&escaped_int, format!("({}) as i32", val_code)));
|
|
3982
4213
|
}
|
|
3983
4214
|
// Defensive: gate guarantees `Some`, but if a future RHS shape slips through,
|
|
3984
4215
|
// fall back to a sound f64-narrowed store rather than miscompiling.
|
|
@@ -3988,11 +4219,7 @@ impl Codegen {
|
|
|
3988
4219
|
val_code
|
|
3989
4220
|
};
|
|
3990
4221
|
let escaped = Self::escape_ident(name.as_ref());
|
|
3991
|
-
return Ok(
|
|
3992
|
-
"{} = {}",
|
|
3993
|
-
escaped,
|
|
3994
|
-
RustType::I32.from_value_expr(&v)
|
|
3995
|
-
));
|
|
4222
|
+
return Ok(store(&escaped, RustType::I32.from_value_expr(&v)));
|
|
3996
4223
|
}
|
|
3997
4224
|
// String self-append `s = s + rhs` -> in-place push_str (amortized O(1)). The
|
|
3998
4225
|
// general path boxes via `ops::add(Value::String(s.clone()), ...)` which clones
|
|
@@ -5643,6 +5870,34 @@ impl Codegen {
|
|
|
5643
5870
|
})
|
|
5644
5871
|
.unwrap_or(RustType::Value);
|
|
5645
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
|
+
|
|
5646
5901
|
// M5 native-fn body: `let r = genRandom(1)` without a `: number` annotation.
|
|
5647
5902
|
if self.native_fn_body_emit && rust_type == RustType::Value {
|
|
5648
5903
|
if let Some(Expr::Call { callee, .. }) = init.as_ref() {
|
|
@@ -5770,12 +6025,22 @@ impl Codegen {
|
|
|
5770
6025
|
return Ok(());
|
|
5771
6026
|
}
|
|
5772
6027
|
|
|
5773
|
-
// 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.
|
|
5774
6031
|
if self.module_const_f64_arrays.contains_key(name.as_ref())
|
|
5775
6032
|
&& self.outer_vars_stack.len() == 1
|
|
5776
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
|
+
};
|
|
5777
6042
|
self.type_context
|
|
5778
|
-
.define(name.as_ref(), RustType::Vec(Box::new(
|
|
6043
|
+
.define(name.as_ref(), RustType::Vec(Box::new(elem)));
|
|
5779
6044
|
return Ok(());
|
|
5780
6045
|
}
|
|
5781
6046
|
|
|
@@ -6421,6 +6686,24 @@ impl Codegen {
|
|
|
6421
6686
|
}
|
|
6422
6687
|
}
|
|
6423
6688
|
}
|
|
6689
|
+
VecRetKind::Str => {
|
|
6690
|
+
// #605. The body's own `return "…"`, emitted as an OWNED String so it
|
|
6691
|
+
// can leave the fn. `emit_typed_expr` already yields a `String` for a
|
|
6692
|
+
// string literal; anything else is converted rather than assumed,
|
|
6693
|
+
// because a wrong guess here is a type error in the emitted crate.
|
|
6694
|
+
let e = value.as_ref().ok_or_else(|| {
|
|
6695
|
+
CompileError::new("native-vec string fn: empty return", None)
|
|
6696
|
+
})?;
|
|
6697
|
+
let (c, ty) = self.emit_typed_expr(e)?;
|
|
6698
|
+
if ty == RustType::String {
|
|
6699
|
+
self.writeln(&format!("return {};", c));
|
|
6700
|
+
} else {
|
|
6701
|
+
self.writeln(&format!(
|
|
6702
|
+
"return {};",
|
|
6703
|
+
RustType::String.from_value_expr(&c)
|
|
6704
|
+
));
|
|
6705
|
+
}
|
|
6706
|
+
}
|
|
6424
6707
|
VecRetKind::Unit => {
|
|
6425
6708
|
self.writeln("return;");
|
|
6426
6709
|
}
|
|
@@ -7802,7 +8085,7 @@ impl Codegen {
|
|
|
7802
8085
|
));
|
|
7803
8086
|
}
|
|
7804
8087
|
if let Some(static_name) = self.module_const_f64_array_rust_ref(name.as_ref()) {
|
|
7805
|
-
let v =
|
|
8088
|
+
let v = self.module_const_f64_array_as_value(&static_name);
|
|
7806
8089
|
return Ok(if self.value_fn_depth > 0 || !self.loop_stack.is_empty() {
|
|
7807
8090
|
format!("({}).clone()", v)
|
|
7808
8091
|
} else {
|
|
@@ -8979,27 +9262,31 @@ impl Codegen {
|
|
|
8979
9262
|
// M5: boxed-context call to an eligible native fn → `Value::Number(name_native(..))`.
|
|
8980
9263
|
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
8981
9264
|
if self.native_fns.contains(fname.as_ref()) {
|
|
9265
|
+
let i32_abi = self.native_fns_i32.contains(fname.as_ref());
|
|
9266
|
+
let want =
|
|
9267
|
+
if i32_abi { RustType::I32 } else { RustType::F64 };
|
|
8982
9268
|
let mut argc: Vec<String> = Vec::with_capacity(args.len());
|
|
8983
9269
|
let mut ok = true;
|
|
8984
9270
|
for a in args {
|
|
8985
9271
|
if let CallArg::Expr(e) = a {
|
|
8986
9272
|
let (ac, at) = self.emit_typed_expr(e)?;
|
|
8987
|
-
argc.push(
|
|
8988
|
-
RustType::F64.from_value_expr(&ac)
|
|
8989
|
-
} else {
|
|
8990
|
-
ac
|
|
8991
|
-
});
|
|
9273
|
+
argc.push(self.coerce_native_arg(&ac, at, want.clone()));
|
|
8992
9274
|
} else {
|
|
8993
9275
|
ok = false;
|
|
8994
9276
|
break;
|
|
8995
9277
|
}
|
|
8996
9278
|
}
|
|
8997
9279
|
if ok {
|
|
8998
|
-
|
|
8999
|
-
"
|
|
9280
|
+
let call = format!(
|
|
9281
|
+
"{}({})",
|
|
9000
9282
|
self.native_call_target(fname.as_ref()),
|
|
9001
9283
|
argc.join(", ")
|
|
9002
|
-
)
|
|
9284
|
+
);
|
|
9285
|
+
return Ok(if i32_abi {
|
|
9286
|
+
format!("Value::Number(({}) as f64)", call)
|
|
9287
|
+
} else {
|
|
9288
|
+
format!("Value::Number({})", call)
|
|
9289
|
+
});
|
|
9003
9290
|
}
|
|
9004
9291
|
}
|
|
9005
9292
|
}
|
|
@@ -11981,6 +12268,21 @@ impl Codegen {
|
|
|
11981
12268
|
}
|
|
11982
12269
|
}
|
|
11983
12270
|
|
|
12271
|
+
// Int-domain shortcut for an integer-typed local: `let nw: i32 = w | P_MARK` binds the
|
|
12272
|
+
// integer form rather than `(int as f64) as i32`, so `nw` is i32-backed and every later use
|
|
12273
|
+
// — including storing it into an `i32[]` — stays integral. Without this the annotation
|
|
12274
|
+
// actively costs: measured 23,4xx ticks/1000 for `let v: i32 = j & 255; arr[j] = v`
|
|
12275
|
+
// against 1,5xx for `arr[j] = j & 255` written directly.
|
|
12276
|
+
if target_type.is_integer_scalar() {
|
|
12277
|
+
if let Some(int_code) = self.emit_int32_for_int_consumer(expr)? {
|
|
12278
|
+
return Ok(if *target_type == RustType::I32 {
|
|
12279
|
+
int_code
|
|
12280
|
+
} else {
|
|
12281
|
+
format!("({}) as {}", int_code, target_type.to_rust_type_str())
|
|
12282
|
+
});
|
|
12283
|
+
}
|
|
12284
|
+
}
|
|
12285
|
+
|
|
11984
12286
|
// #179 Stage C: `arr.length` on a native `Vec<_>` against an F64 target → native
|
|
11985
12287
|
// `(arr.len() as f64)` (no boxing). Lets `let n = arr.length` stay native f64.
|
|
11986
12288
|
if let (
|
|
@@ -12261,6 +12563,28 @@ impl Codegen {
|
|
|
12261
12563
|
RustType::from_annotation(ann) == RustType::F64
|
|
12262
12564
|
}
|
|
12263
12565
|
|
|
12566
|
+
fn ann_is_i32(ann: &TypeAnnotation) -> bool {
|
|
12567
|
+
RustType::from_annotation(ann) == RustType::I32
|
|
12568
|
+
}
|
|
12569
|
+
|
|
12570
|
+
/// Is every parameter AND the return annotated `: i32`?
|
|
12571
|
+
///
|
|
12572
|
+
/// ALL OR NOTHING, deliberately. An `: i32` parameter reached through the boxed path is
|
|
12573
|
+
/// ToInt32-coerced on entry; promoted to a native `f64` it would not be, so 3.7 would arrive as
|
|
12574
|
+
/// 3 one way and 3.7 the other. Admitting `: i32` per-annotation would do exactly that to a
|
|
12575
|
+
/// function that mixes the two. A function whose whole signature is `i32` has one convention
|
|
12576
|
+
/// and keeps the coercion at the boundary, where the caller does it once on an argument that is
|
|
12577
|
+
/// usually already an integer.
|
|
12578
|
+
fn fn_sig_all_i32(params: &[FunParam], return_type: &Option<TypeAnnotation>) -> bool {
|
|
12579
|
+
return_type.as_ref().is_some_and(Self::ann_is_i32)
|
|
12580
|
+
&& !params.is_empty()
|
|
12581
|
+
&& params.iter().all(|p| {
|
|
12582
|
+
matches!(p, FunParam::Simple(tp)
|
|
12583
|
+
if tp.default.is_none()
|
|
12584
|
+
&& tp.type_ann.as_ref().is_some_and(Self::ann_is_i32))
|
|
12585
|
+
})
|
|
12586
|
+
}
|
|
12587
|
+
|
|
12264
12588
|
// ── Soundness: demote `number` locals that a reassignment can turn non-numeric ──────────────
|
|
12265
12589
|
//
|
|
12266
12590
|
// `let s = 0` is inferred `number` → lowered to a native `f64`, and a reassignment stores into
|
|
@@ -12278,6 +12602,246 @@ impl Codegen {
|
|
|
12278
12602
|
// demoted). The map is name-flat across the whole program (a name demoted in one function is
|
|
12279
12603
|
// demoted in all) — still sound, and harmless to the perf gauntlet, where each kernel is its
|
|
12280
12604
|
// own program with unique accumulator names.
|
|
12605
|
+
/// Which top-level fns provably return a number, and as what native type. A fixpoint, because
|
|
12606
|
+
/// one qualifying fn makes calls to it numeric inside another; growing only, so it terminates.
|
|
12607
|
+
fn collect_user_fn_rets(
|
|
12608
|
+
&self,
|
|
12609
|
+
stmts: &[Statement],
|
|
12610
|
+
) -> std::collections::HashMap<String, RustType> {
|
|
12611
|
+
// MODULE SCOPE ONLY. `collect_annotated_types` over the whole program builds a NAME-FLAT
|
|
12612
|
+
// map, which `collect_demoted_numeric_locals` can use because its error direction is safe —
|
|
12613
|
+
// over-demoting only costs a box. Proving is the opposite: a numeric `s` in one function
|
|
12614
|
+
// would make a STRING `s` in another look numeric, and the return gets coerced.
|
|
12615
|
+
//
|
|
12616
|
+
// That is not hypothetical. It shipped: `function ruleTag(mask: i32) { let s = ""; …;
|
|
12617
|
+
// return s }` was proven numeric off an unrelated numeric `s`, and every ROM using it
|
|
12618
|
+
// panicked with "expected number". Each fn gets its own scope now.
|
|
12619
|
+
let mut module_env: HashMap<String, RustType> = HashMap::new();
|
|
12620
|
+
let top_only: Vec<Statement> = stmts
|
|
12621
|
+
.iter()
|
|
12622
|
+
.filter(|s| matches!(s, Statement::VarDecl { .. }))
|
|
12623
|
+
.cloned()
|
|
12624
|
+
.collect();
|
|
12625
|
+
Self::collect_annotated_types(&top_only, &self.type_aliases, &mut module_env);
|
|
12626
|
+
|
|
12627
|
+
let mut decls: Vec<(String, &Statement, HashMap<String, RustType>)> = Vec::new();
|
|
12628
|
+
for s in stmts {
|
|
12629
|
+
if let Statement::FunDecl { async_: false, name, params, body, .. } = s {
|
|
12630
|
+
let mut env = module_env.clone();
|
|
12631
|
+
// This fn's own params and locals — not a nested closure's, whose names are its own.
|
|
12632
|
+
for prm in params {
|
|
12633
|
+
if let FunParam::Simple(tp) = prm {
|
|
12634
|
+
if let Some(ann) = &tp.type_ann {
|
|
12635
|
+
let ty = RustType::from_annotation(ann);
|
|
12636
|
+
if ty.is_native() {
|
|
12637
|
+
env.insert(tp.name.to_string(), ty);
|
|
12638
|
+
}
|
|
12639
|
+
}
|
|
12640
|
+
}
|
|
12641
|
+
}
|
|
12642
|
+
Self::collect_own_locals(body.as_ref(), &self.type_aliases, &mut env);
|
|
12643
|
+
decls.push((name.to_string(), body.as_ref(), env));
|
|
12644
|
+
}
|
|
12645
|
+
}
|
|
12646
|
+
|
|
12647
|
+
// A REASSIGNED name is not this fn at the call site, and a SHADOWED one is not either —
|
|
12648
|
+
// `emit_typed_expr` sees a bare callee name with no scope information. Excluding every name
|
|
12649
|
+
// ever bound as a variable or parameter is blunt, but the alternative is a miscompile.
|
|
12650
|
+
let mut reassigns: Vec<(String, &Expr)> = Vec::new();
|
|
12651
|
+
Self::collect_reassignments_stmts(stmts, &mut reassigns);
|
|
12652
|
+
let mut excluded: std::collections::HashSet<String> =
|
|
12653
|
+
reassigns.iter().map(|(n, _)| n.clone()).collect();
|
|
12654
|
+
Self::collect_bound_names(stmts, &mut excluded);
|
|
12655
|
+
|
|
12656
|
+
let mut out: std::collections::HashMap<String, RustType> = std::collections::HashMap::new();
|
|
12657
|
+
loop {
|
|
12658
|
+
let mut changed = false;
|
|
12659
|
+
for (name, body, env) in &decls {
|
|
12660
|
+
if out.contains_key(name) || excluded.contains(name) {
|
|
12661
|
+
continue;
|
|
12662
|
+
}
|
|
12663
|
+
let mut rets: Vec<&Expr> = Vec::new();
|
|
12664
|
+
Self::collect_return_exprs(body, &mut rets);
|
|
12665
|
+
if rets.is_empty() {
|
|
12666
|
+
continue; // no return yields `undefined`, which is not a number
|
|
12667
|
+
}
|
|
12668
|
+
let mut ty: Option<RustType> = None;
|
|
12669
|
+
let mut ok = true;
|
|
12670
|
+
for e in &rets {
|
|
12671
|
+
let Some(et) = self.proof_numeric_type(e, env, &out) else {
|
|
12672
|
+
ok = false;
|
|
12673
|
+
break;
|
|
12674
|
+
};
|
|
12675
|
+
ty = match ty {
|
|
12676
|
+
None => Some(et),
|
|
12677
|
+
Some(prev) if prev == et => Some(et),
|
|
12678
|
+
Some(_) => Some(RustType::F64), // mixed I32/F64 settles on the wider
|
|
12679
|
+
};
|
|
12680
|
+
}
|
|
12681
|
+
if ok {
|
|
12682
|
+
if let Some(final_ty) = ty {
|
|
12683
|
+
out.insert(name.clone(), final_ty);
|
|
12684
|
+
changed = true;
|
|
12685
|
+
}
|
|
12686
|
+
}
|
|
12687
|
+
}
|
|
12688
|
+
if !changed {
|
|
12689
|
+
break;
|
|
12690
|
+
}
|
|
12691
|
+
}
|
|
12692
|
+
out
|
|
12693
|
+
}
|
|
12694
|
+
|
|
12695
|
+
/// Is `e` provably a NUMBER, and which native type describes it?
|
|
12696
|
+
///
|
|
12697
|
+
/// Separate from `expr_native_type`, which answers the stricter "can this be LOWERED natively"
|
|
12698
|
+
/// and is bound by `result_type_of_binop`, whose arms require BOTH operands `F64`. That rejects
|
|
12699
|
+
/// `arr[i] & 255` on a `Vec<i32>` — element `I32`, literal `F64` — which on GBA is nearly every
|
|
12700
|
+
/// masked read of a typed array. Widening that function would change lowering everywhere it is
|
|
12701
|
+
/// consulted, to serve a question it is not being asked.
|
|
12702
|
+
///
|
|
12703
|
+
/// Two unconditional JS facts carry it: `- * / % **` coerce with ToNumber, and `& | ^ << >>`
|
|
12704
|
+
/// with ToInt32 — a number, and an int32 one, even if an operand was a string. `>>>` is excluded
|
|
12705
|
+
/// (its uint32 can exceed `i32::MAX`) and `+` needs both sides proven, because it concatenates.
|
|
12706
|
+
fn proof_numeric_type(
|
|
12707
|
+
&self,
|
|
12708
|
+
e: &Expr,
|
|
12709
|
+
env: &HashMap<String, RustType>,
|
|
12710
|
+
fn_rets: &std::collections::HashMap<String, RustType>,
|
|
12711
|
+
) -> Option<RustType> {
|
|
12712
|
+
let numeric = |t: &RustType| matches!(t, RustType::F64 | RustType::I32);
|
|
12713
|
+
match e {
|
|
12714
|
+
Expr::Literal { value: Literal::Number(_), .. } => Some(RustType::F64),
|
|
12715
|
+
Expr::Ident { name, .. } => env.get(name.as_ref()).filter(|t| numeric(t)).cloned(),
|
|
12716
|
+
Expr::Index { object, optional: false, .. } => {
|
|
12717
|
+
if let Expr::Ident { name, .. } = object.as_ref() {
|
|
12718
|
+
if let Some(RustType::Vec(inner)) = env.get(name.as_ref()) {
|
|
12719
|
+
if numeric(inner) {
|
|
12720
|
+
return Some((**inner).clone());
|
|
12721
|
+
}
|
|
12722
|
+
}
|
|
12723
|
+
}
|
|
12724
|
+
None
|
|
12725
|
+
}
|
|
12726
|
+
Expr::Unary { op, operand, .. } => match op {
|
|
12727
|
+
UnaryOp::Neg | UnaryOp::Pos => {
|
|
12728
|
+
self.proof_numeric_type(operand, env, fn_rets).map(|_| RustType::F64)
|
|
12729
|
+
}
|
|
12730
|
+
_ => None,
|
|
12731
|
+
},
|
|
12732
|
+
Expr::Binary { left, op, right, .. } => match op {
|
|
12733
|
+
BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor | BinOp::Shl | BinOp::Shr => {
|
|
12734
|
+
Some(RustType::I32)
|
|
12735
|
+
}
|
|
12736
|
+
BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow => Some(RustType::F64),
|
|
12737
|
+
BinOp::Add => {
|
|
12738
|
+
let l = self.proof_numeric_type(left, env, fn_rets)?;
|
|
12739
|
+
let r = self.proof_numeric_type(right, env, fn_rets)?;
|
|
12740
|
+
Some(if l == RustType::I32 && r == RustType::I32 {
|
|
12741
|
+
RustType::I32
|
|
12742
|
+
} else {
|
|
12743
|
+
RustType::F64
|
|
12744
|
+
})
|
|
12745
|
+
}
|
|
12746
|
+
_ => None,
|
|
12747
|
+
},
|
|
12748
|
+
Expr::Conditional { then_branch, else_branch, .. } => {
|
|
12749
|
+
let a = self.proof_numeric_type(then_branch, env, fn_rets)?;
|
|
12750
|
+
let b = self.proof_numeric_type(else_branch, env, fn_rets)?;
|
|
12751
|
+
Some(if a == b { a } else { RustType::F64 })
|
|
12752
|
+
}
|
|
12753
|
+
Expr::Call { callee, .. } => match callee.as_ref() {
|
|
12754
|
+
Expr::Ident { name, .. } => fn_rets.get(name.as_ref()).cloned(),
|
|
12755
|
+
_ => None,
|
|
12756
|
+
},
|
|
12757
|
+
_ => None,
|
|
12758
|
+
}
|
|
12759
|
+
}
|
|
12760
|
+
|
|
12761
|
+
/// Annotated locals declared directly in this body — through blocks, ifs and loops, but NOT
|
|
12762
|
+
/// into a nested function, whose locals are its own scope and would re-create the name
|
|
12763
|
+
/// collision this exists to avoid.
|
|
12764
|
+
fn collect_own_locals(
|
|
12765
|
+
s: &Statement,
|
|
12766
|
+
aliases: &HashMap<String, RustType>,
|
|
12767
|
+
env: &mut HashMap<String, RustType>,
|
|
12768
|
+
) {
|
|
12769
|
+
match s {
|
|
12770
|
+
Statement::VarDecl { .. } => {
|
|
12771
|
+
Self::collect_annotated_types(core::slice::from_ref(s), aliases, env)
|
|
12772
|
+
}
|
|
12773
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
12774
|
+
for x in statements {
|
|
12775
|
+
Self::collect_own_locals(x, aliases, env);
|
|
12776
|
+
}
|
|
12777
|
+
}
|
|
12778
|
+
Statement::If { then_branch, else_branch, .. } => {
|
|
12779
|
+
Self::collect_own_locals(then_branch, aliases, env);
|
|
12780
|
+
if let Some(e) = else_branch {
|
|
12781
|
+
Self::collect_own_locals(e, aliases, env);
|
|
12782
|
+
}
|
|
12783
|
+
}
|
|
12784
|
+
Statement::While { body, .. } | Statement::For { body, .. } => {
|
|
12785
|
+
Self::collect_own_locals(body, aliases, env)
|
|
12786
|
+
}
|
|
12787
|
+
_ => {}
|
|
12788
|
+
}
|
|
12789
|
+
}
|
|
12790
|
+
|
|
12791
|
+
/// Every name bound as a variable or a function parameter, anywhere, at any depth.
|
|
12792
|
+
fn collect_bound_names(stmts: &[Statement], out: &mut std::collections::HashSet<String>) {
|
|
12793
|
+
for s in stmts {
|
|
12794
|
+
match s {
|
|
12795
|
+
Statement::VarDecl { name, .. } => {
|
|
12796
|
+
out.insert(name.to_string());
|
|
12797
|
+
}
|
|
12798
|
+
Statement::FunDecl { params, body, .. } => {
|
|
12799
|
+
for p in params {
|
|
12800
|
+
if let FunParam::Simple(tp) = p {
|
|
12801
|
+
out.insert(tp.name.to_string());
|
|
12802
|
+
}
|
|
12803
|
+
}
|
|
12804
|
+
Self::collect_bound_names(core::slice::from_ref(body.as_ref()), out);
|
|
12805
|
+
}
|
|
12806
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
12807
|
+
Self::collect_bound_names(statements, out)
|
|
12808
|
+
}
|
|
12809
|
+
Statement::If { then_branch, else_branch, .. } => {
|
|
12810
|
+
Self::collect_bound_names(core::slice::from_ref(then_branch.as_ref()), out);
|
|
12811
|
+
if let Some(e) = else_branch {
|
|
12812
|
+
Self::collect_bound_names(core::slice::from_ref(e.as_ref()), out);
|
|
12813
|
+
}
|
|
12814
|
+
}
|
|
12815
|
+
Statement::While { body, .. } | Statement::For { body, .. } => {
|
|
12816
|
+
Self::collect_bound_names(core::slice::from_ref(body.as_ref()), out)
|
|
12817
|
+
}
|
|
12818
|
+
_ => {}
|
|
12819
|
+
}
|
|
12820
|
+
}
|
|
12821
|
+
}
|
|
12822
|
+
|
|
12823
|
+
/// Every `return <expr>` in a body, through blocks, ifs and loops. NOT into a nested function.
|
|
12824
|
+
fn collect_return_exprs<'a>(s: &'a Statement, out: &mut Vec<&'a Expr>) {
|
|
12825
|
+
match s {
|
|
12826
|
+
Statement::Return { value: Some(e), .. } => out.push(e),
|
|
12827
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
12828
|
+
for x in statements {
|
|
12829
|
+
Self::collect_return_exprs(x, out);
|
|
12830
|
+
}
|
|
12831
|
+
}
|
|
12832
|
+
Statement::If { then_branch, else_branch, .. } => {
|
|
12833
|
+
Self::collect_return_exprs(then_branch, out);
|
|
12834
|
+
if let Some(e) = else_branch {
|
|
12835
|
+
Self::collect_return_exprs(e, out);
|
|
12836
|
+
}
|
|
12837
|
+
}
|
|
12838
|
+
Statement::While { body, .. } | Statement::For { body, .. } => {
|
|
12839
|
+
Self::collect_return_exprs(body, out)
|
|
12840
|
+
}
|
|
12841
|
+
_ => {}
|
|
12842
|
+
}
|
|
12843
|
+
}
|
|
12844
|
+
|
|
12281
12845
|
fn collect_demoted_numeric_locals(&self, stmts: &[Statement]) -> HashSet<String> {
|
|
12282
12846
|
// 1. Flat env: every annotated local/param name → its native `RustType`.
|
|
12283
12847
|
let mut env: HashMap<String, RustType> = HashMap::new();
|
|
@@ -13565,7 +14129,11 @@ impl Codegen {
|
|
|
13565
14129
|
let in_bounds = self.index_in_bounds(index, name.as_ref());
|
|
13566
14130
|
let idx_usize = self.emit_index_usize(index)?;
|
|
13567
14131
|
let native_val = if *elem_type == RustType::I32 {
|
|
13568
|
-
|
|
14132
|
+
// A bitwise RHS stays in the integer domain instead of `(int as f64) as i32`. Exact —
|
|
14133
|
+
// see `emit_int32_for_int_consumer`.
|
|
14134
|
+
if let Some(int_code) = self.emit_int32_for_int_consumer(value)? {
|
|
14135
|
+
int_code
|
|
14136
|
+
} else if let Expr::Binary {
|
|
13569
14137
|
left,
|
|
13570
14138
|
op: BinOp::Sub,
|
|
13571
14139
|
right,
|
|
@@ -13616,13 +14184,22 @@ impl Codegen {
|
|
|
13616
14184
|
// so a RHS that reads the same cell (`a[i] += x`, `a[i] = a[j]`) releases its shared borrow
|
|
13617
14185
|
// first (no overlapping lock → no deadlock), then store into the real element through the cell.
|
|
13618
14186
|
if wrapped {
|
|
13619
|
-
|
|
13620
|
-
|
|
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;
|
|
13621
14198
|
let store = if needs_resize {
|
|
13622
|
-
let pad =
|
|
13623
|
-
"f64::NAN"
|
|
13624
|
-
|
|
13625
|
-
"false"
|
|
14199
|
+
let pad = match elem_type.as_ref() {
|
|
14200
|
+
RustType::F64 => "f64::NAN",
|
|
14201
|
+
RustType::I32 => "0",
|
|
14202
|
+
_ => "false",
|
|
13626
14203
|
};
|
|
13627
14204
|
format!(
|
|
13628
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;",
|
|
@@ -13648,16 +14225,22 @@ impl Codegen {
|
|
|
13648
14225
|
format!("{{ {}[{}] = {}; Value::Null }}", esc_obj, idx_usize, native_val)
|
|
13649
14226
|
}
|
|
13650
14227
|
}
|
|
13651
|
-
|
|
13652
|
-
|
|
13653
|
-
|
|
13654
|
-
|
|
13655
|
-
"
|
|
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",
|
|
13656
14234
|
};
|
|
13657
|
-
format!(
|
|
13658
|
-
"{{ let _idx = {}; if _idx >= {}.len() {{ {}.resize(_idx + 1, {}); }} {}[_idx] = {}
|
|
14235
|
+
let store = format!(
|
|
14236
|
+
"{{ let _idx = {}; if _idx >= {}.len() {{ {}.resize(_idx + 1, {}); }} {}[_idx] = {}",
|
|
13659
14237
|
idx_usize, esc_obj, esc_obj, pad, esc_obj, native_val
|
|
13660
|
-
)
|
|
14238
|
+
);
|
|
14239
|
+
if side_effect_only {
|
|
14240
|
+
format!("{} }}", store)
|
|
14241
|
+
} else {
|
|
14242
|
+
format!("{}; Value::Null }}", store)
|
|
14243
|
+
}
|
|
13661
14244
|
}
|
|
13662
14245
|
_ if side_effect_only => {
|
|
13663
14246
|
format!("{}[{}] = {}", esc_obj, idx_usize, native_val)
|
|
@@ -13677,6 +14260,12 @@ impl Codegen {
|
|
|
13677
14260
|
return Ok(uv.clone());
|
|
13678
14261
|
}
|
|
13679
14262
|
}
|
|
14263
|
+
// Same int-domain shortcut as the element store, with one correction: `(neg as f64) as
|
|
14264
|
+
// usize` SATURATES to 0, while `(neg_i32) as usize` wraps to a huge value. `.max(0)` makes
|
|
14265
|
+
// the two identical for every input, and costs a compare against a soft-float call.
|
|
14266
|
+
if let Some(int_code) = self.emit_int32_for_int_consumer(index)? {
|
|
14267
|
+
return Ok(format!("({}).max(0) as usize", int_code));
|
|
14268
|
+
}
|
|
13680
14269
|
let (idx_code, idx_ty) = self.emit_typed_expr(index)?;
|
|
13681
14270
|
if idx_ty == RustType::F64 || idx_ty == RustType::I32 {
|
|
13682
14271
|
Ok(format!("({}) as usize", idx_code))
|
|
@@ -13800,6 +14389,19 @@ impl Codegen {
|
|
|
13800
14389
|
value: &Expr,
|
|
13801
14390
|
target_ty: &RustType,
|
|
13802
14391
|
) -> Result<String, CompileError> {
|
|
14392
|
+
// Same int-domain shortcut as the `Vec<i32>` element store: a bitwise RHS bound to an
|
|
14393
|
+
// integer target keeps its integer form instead of `(int as f64) as i32`. Without this,
|
|
14394
|
+
// `let v: i32 = w & 255` is f64-backed and every later use of `v` pays the conversion —
|
|
14395
|
+
// measured at 23,4xx ticks/1000 against 1,5xx for the direct form.
|
|
14396
|
+
if target_ty.is_integer_scalar() {
|
|
14397
|
+
if let Some(int_code) = self.emit_int32_for_int_consumer(value)? {
|
|
14398
|
+
return Ok(if *target_ty == RustType::I32 {
|
|
14399
|
+
int_code
|
|
14400
|
+
} else {
|
|
14401
|
+
format!("({}) as {}", int_code, target_ty.to_rust_type_str())
|
|
14402
|
+
});
|
|
14403
|
+
}
|
|
14404
|
+
}
|
|
13803
14405
|
let (val_code, val_ty) = self.emit_typed_expr(value)?;
|
|
13804
14406
|
if val_ty == *target_ty {
|
|
13805
14407
|
return Ok(val_code);
|
|
@@ -13814,6 +14416,26 @@ impl Codegen {
|
|
|
13814
14416
|
if *target_ty == RustType::F64 && val_ty.is_integer_scalar() {
|
|
13815
14417
|
return Ok(format!("({}) as f64", val_code));
|
|
13816
14418
|
}
|
|
14419
|
+
// FIXED CROSSES DIRECTLY, not through a `Value`. Without these arms a `fixed` reaching an
|
|
14420
|
+
// `f64` extern parameter (or the reverse) fell to the boxed round trip below — allocate a
|
|
14421
|
+
// Value::Number, then immediately unwrap it — which is the whole cost the typed-extern path
|
|
14422
|
+
// exists to remove, reintroduced at the boundary it was removed from (#607). Same scaling as
|
|
14423
|
+
// `RustType::Fixed`'s Value conversions in types.rs, so the values are identical either way.
|
|
14424
|
+
if *target_ty == RustType::F64 && val_ty == RustType::Fixed {
|
|
14425
|
+
return Ok(format!("(({}).to_raw() as f64 / 256.0)", val_code));
|
|
14426
|
+
}
|
|
14427
|
+
if *target_ty == RustType::Fixed && val_ty == RustType::F64 {
|
|
14428
|
+
return Ok(format!(
|
|
14429
|
+
"tishlang_runtime::Fixed::from_raw((({}) * 256.0) as i32)",
|
|
14430
|
+
val_code
|
|
14431
|
+
));
|
|
14432
|
+
}
|
|
14433
|
+
if *target_ty == RustType::Fixed && val_ty.is_integer_scalar() {
|
|
14434
|
+
return Ok(format!(
|
|
14435
|
+
"tishlang_runtime::Fixed::from_raw((({}) as i32).wrapping_mul(256))",
|
|
14436
|
+
val_code
|
|
14437
|
+
));
|
|
14438
|
+
}
|
|
13817
14439
|
let boxed = if val_ty.is_native() {
|
|
13818
14440
|
val_ty.to_value_expr(&val_code)
|
|
13819
14441
|
} else {
|
|
@@ -15699,6 +16321,9 @@ impl Codegen {
|
|
|
15699
16321
|
let rt = self.expr_native_type(right, env);
|
|
15700
16322
|
RustType::result_type_of_binop(*op, <, &rt).unwrap_or(RustType::Value)
|
|
15701
16323
|
}
|
|
16324
|
+
// A call to a fn whose every return is PROVEN numeric. Without this a call is opaque and
|
|
16325
|
+
// any local assigned from one demotes to a boxed `Value`. `env` shadowing wins: a local
|
|
16326
|
+
// of that name is not the fn.
|
|
15702
16327
|
// `vec[i]` where `vec` is a `number[]` (Vec<f64>) → the element type. A `Vec<f64>`
|
|
15703
16328
|
// can only hold numbers, so this never feeds a string into the accumulator.
|
|
15704
16329
|
Expr::Index {
|
|
@@ -15777,13 +16402,30 @@ impl Codegen {
|
|
|
15777
16402
|
}
|
|
15778
16403
|
RustType::Value
|
|
15779
16404
|
}
|
|
16405
|
+
// ONE Call arm, and the singularity is load-bearing: the numeric-return proof briefly
|
|
16406
|
+
// lived in its own `Expr::Call` arm ABOVE this one, which shadowed everything below —
|
|
16407
|
+
// native-fn results, the Math intrinsics and the #169 reduce modelling all became
|
|
16408
|
+
// unreachable (rustc said so, as a warning nobody read) and every accumulator they
|
|
16409
|
+
// feed demoted to a boxed `Value`. CI caught it as issue_169 + the #320 push test.
|
|
15780
16410
|
Expr::Call { callee, args, .. } => {
|
|
15781
|
-
// M5 native fn (`fn f_native(..) -> f64`); requires all-positional args.
|
|
15782
16411
|
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
16412
|
+
// M5 native fn (`fn f_native(..) -> f64`); requires all-positional args.
|
|
15783
16413
|
if self.native_fns.contains(fname.as_ref())
|
|
15784
16414
|
&& args.iter().all(|a| matches!(a, CallArg::Expr(_)))
|
|
15785
16415
|
{
|
|
15786
|
-
return
|
|
16416
|
+
return if self.native_fns_i32.contains(fname.as_ref()) {
|
|
16417
|
+
RustType::I32
|
|
16418
|
+
} else {
|
|
16419
|
+
RustType::F64
|
|
16420
|
+
};
|
|
16421
|
+
}
|
|
16422
|
+
// A call to a fn whose every return is PROVEN numeric (the boxed call's RESULT
|
|
16423
|
+
// stops being opaque — the dispatch is unchanged). `env` shadowing wins: a
|
|
16424
|
+
// local of that name is not the fn.
|
|
16425
|
+
if !env.contains_key(fname.as_ref()) {
|
|
16426
|
+
if let Some(ft) = self.user_fn_ret.get(fname.as_ref()) {
|
|
16427
|
+
return ft.clone();
|
|
16428
|
+
}
|
|
15787
16429
|
}
|
|
15788
16430
|
}
|
|
15789
16431
|
// Single-arg `Math.<intrinsic>(x)` lowered to a direct `f64` method → number.
|
|
@@ -16302,7 +16944,15 @@ impl Codegen {
|
|
|
16302
16944
|
{
|
|
16303
16945
|
let sum_esc = Self::escape_ident(sum_name.as_ref());
|
|
16304
16946
|
let arms: Vec<String> = (0..n)
|
|
16305
|
-
|
|
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
|
+
})
|
|
16306
16956
|
.collect();
|
|
16307
16957
|
self.writeln(&format!(
|
|
16308
16958
|
"{} = (({} + match ({} as usize) {{ {}, _ => 0.0 }}) % 2147483647_f64);",
|
|
@@ -16460,11 +17110,110 @@ impl Codegen {
|
|
|
16460
17110
|
self.native_fn_emit_name.is_some() || self.native_vec_ret.is_some()
|
|
16461
17111
|
}
|
|
16462
17112
|
|
|
16463
|
-
|
|
16464
|
-
|
|
16465
|
-
|
|
16466
|
-
|
|
16467
|
-
|
|
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
|
+
|
|
17212
|
+
fn emit_statements_with_folds(&mut self, statements: &[Statement]) -> Result<(), CompileError> {
|
|
17213
|
+
let mut i = 0;
|
|
17214
|
+
while i < statements.len() {
|
|
17215
|
+
if let Some(skip) = self.try_emit_const_cum_search_fold(&statements[i..])? {
|
|
17216
|
+
i += skip;
|
|
16468
17217
|
continue;
|
|
16469
17218
|
}
|
|
16470
17219
|
if self.native_fn_body_emit || self.native_vec_ret.is_some() {
|
|
@@ -16534,6 +17283,22 @@ impl Codegen {
|
|
|
16534
17283
|
}
|
|
16535
17284
|
|
|
16536
17285
|
/// Emit `thread_local! { static G_NAME: Cell<f64> = ... }` for each eligible global.
|
|
17286
|
+
/// #594, GBA: the no_std twin of `emit_native_numeric_global_tls`. `SingleCore` is the facade's
|
|
17287
|
+
/// single-core static wrapper — sound because the GBA runs one cooperative thread and no
|
|
17288
|
+
/// interrupt handler touches interpreter state — and its `with()` mirrors `LocalKey::with`, so
|
|
17289
|
+
/// the read/write emission is shared with the TLS path rather than duplicated.
|
|
17290
|
+
fn emit_native_numeric_global_singlecore(&mut self) -> Result<(), CompileError> {
|
|
17291
|
+
for (name, init) in self.native_numeric_globals.clone() {
|
|
17292
|
+
self.writeln(&format!(
|
|
17293
|
+
"static {}: tishlang_runtime::SingleCore<core::cell::Cell<f64>> = \
|
|
17294
|
+
tishlang_runtime::SingleCore::new(core::cell::Cell::new({}));",
|
|
17295
|
+
Self::native_global_static(&name),
|
|
17296
|
+
Self::f64_lit(init),
|
|
17297
|
+
));
|
|
17298
|
+
}
|
|
17299
|
+
Ok(())
|
|
17300
|
+
}
|
|
17301
|
+
|
|
16537
17302
|
fn emit_native_numeric_global_tls(&mut self) -> Result<(), CompileError> {
|
|
16538
17303
|
self.writeln("thread_local! {");
|
|
16539
17304
|
self.indent += 1;
|
|
@@ -16552,6 +17317,9 @@ impl Codegen {
|
|
|
16552
17317
|
/// Top-level `let name = [n0, n1, …]` with numeric literals, never reassigned — emitted as
|
|
16553
17318
|
/// `const G_name: [f64; N]` for direct indexing from native fns (fasta `codes`/`probs`).
|
|
16554
17319
|
fn collect_module_const_f64_arrays(stmts: &[Statement]) -> HashMap<String, Vec<f64>> {
|
|
17320
|
+
// Every name bound anywhere in the program by anything other than the candidate decls
|
|
17321
|
+
// themselves. Collected alongside the existing per-fn local scan below.
|
|
17322
|
+
let mut shadow_bindings: HashSet<String> = HashSet::new();
|
|
16555
17323
|
use std::collections::{HashMap, HashSet};
|
|
16556
17324
|
let mut candidates: HashMap<String, Vec<f64>> = HashMap::new();
|
|
16557
17325
|
for s in stmts {
|
|
@@ -16592,6 +17360,16 @@ impl Codegen {
|
|
|
16592
17360
|
candidates.remove(&n);
|
|
16593
17361
|
}
|
|
16594
17362
|
}
|
|
17363
|
+
// …AND ANY OTHER BINDING OF THE NAME, not just `let`/`const`. `collect_local_var_names`
|
|
17364
|
+
// sees VarDecl only, so a PARAMETER, a `for-of` variable or a `catch` binding named like
|
|
17365
|
+
// a module const did not disqualify it — and the const fast path in `emit_typed_expr`'s
|
|
17366
|
+
// Index arm is static, with no scope check, so `function f(T: number[], i) { return T[i] }`
|
|
17367
|
+
// read the MODULE `T` and returned the wrong number with no diagnostic. The sibling
|
|
17368
|
+
// numeric-global pass has always used `collect_binding_names` for exactly this reason.
|
|
17369
|
+
Self::collect_binding_names(s, &mut shadow_bindings);
|
|
17370
|
+
}
|
|
17371
|
+
for n in &shadow_bindings {
|
|
17372
|
+
candidates.remove(n);
|
|
16595
17373
|
}
|
|
16596
17374
|
let globals: HashSet<String> = candidates.keys().cloned().collect();
|
|
16597
17375
|
let mut reassigns: Vec<(String, &Expr)> = Vec::new();
|
|
@@ -16630,41 +17408,82 @@ impl Codegen {
|
|
|
16630
17408
|
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
16631
17409
|
Self::walk_stmts_for_module_const_disqualifiers(statements, g, bad);
|
|
16632
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".
|
|
16633
17416
|
Statement::If {
|
|
17417
|
+
cond,
|
|
16634
17418
|
then_branch,
|
|
16635
17419
|
else_branch,
|
|
16636
17420
|
..
|
|
16637
17421
|
} => {
|
|
17422
|
+
Self::walk_expr_for_module_const_disqualifiers(cond, g, bad);
|
|
16638
17423
|
Self::walk_stmt_for_module_const_disqualifiers(then_branch, g, bad);
|
|
16639
17424
|
if let Some(eb) = else_branch {
|
|
16640
17425
|
Self::walk_stmt_for_module_const_disqualifiers(eb, g, bad);
|
|
16641
17426
|
}
|
|
16642
17427
|
}
|
|
16643
|
-
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);
|
|
16644
17430
|
Self::walk_stmt_for_module_const_disqualifiers(body, g, bad);
|
|
16645
17431
|
}
|
|
16646
|
-
Statement::For {
|
|
17432
|
+
Statement::For {
|
|
17433
|
+
init,
|
|
17434
|
+
cond,
|
|
17435
|
+
update,
|
|
17436
|
+
body,
|
|
17437
|
+
..
|
|
17438
|
+
} => {
|
|
16647
17439
|
if let Some(i) = init {
|
|
16648
17440
|
Self::walk_stmt_for_module_const_disqualifiers(i, g, bad);
|
|
16649
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
|
+
}
|
|
16650
17448
|
Self::walk_stmt_for_module_const_disqualifiers(body, g, bad);
|
|
16651
17449
|
}
|
|
16652
|
-
Statement::ForOf { body, .. }
|
|
16653
|
-
| 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
|
+
}
|
|
16654
17465
|
Self::walk_stmt_for_module_const_disqualifiers(body, g, bad);
|
|
16655
17466
|
}
|
|
16656
17467
|
Statement::Switch {
|
|
17468
|
+
expr,
|
|
16657
17469
|
cases,
|
|
16658
17470
|
default_body,
|
|
16659
17471
|
..
|
|
16660
17472
|
} => {
|
|
16661
|
-
|
|
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
|
+
}
|
|
16662
17478
|
Self::walk_stmts_for_module_const_disqualifiers(stmts, g, bad);
|
|
16663
17479
|
}
|
|
16664
17480
|
if let Some(stmts) = default_body {
|
|
16665
17481
|
Self::walk_stmts_for_module_const_disqualifiers(stmts, g, bad);
|
|
16666
17482
|
}
|
|
16667
17483
|
}
|
|
17484
|
+
Statement::Throw { value, .. } => {
|
|
17485
|
+
Self::walk_expr_for_module_const_disqualifiers(value, g, bad);
|
|
17486
|
+
}
|
|
16668
17487
|
Statement::Try {
|
|
16669
17488
|
body,
|
|
16670
17489
|
catch_body,
|
|
@@ -16695,10 +17514,32 @@ impl Codegen {
|
|
|
16695
17514
|
Self::walk_expr_for_module_const_disqualifiers(e, g, bad);
|
|
16696
17515
|
}
|
|
16697
17516
|
}
|
|
16698
|
-
|
|
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
|
+
}
|
|
16699
17526
|
}
|
|
16700
17527
|
}
|
|
16701
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
|
+
|
|
16702
17543
|
fn walk_expr_for_module_const_disqualifiers(e: &Expr, g: &str, bad: &mut bool) {
|
|
16703
17544
|
if *bad {
|
|
16704
17545
|
return;
|
|
@@ -16756,9 +17597,51 @@ impl Codegen {
|
|
|
16756
17597
|
callee.as_ref(),
|
|
16757
17598
|
Expr::Ident { name, .. } if name.as_ref() == "cumulative"
|
|
16758
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
|
+
);
|
|
16759
17642
|
if matches!(callee.as_ref(), Expr::Ident { name, .. } if name.as_ref() == g) {
|
|
16760
17643
|
*bad = true;
|
|
16761
|
-
} else {
|
|
17644
|
+
} else if !member_call_on_g {
|
|
16762
17645
|
Self::walk_expr_for_module_const_disqualifiers(callee, g, bad);
|
|
16763
17646
|
}
|
|
16764
17647
|
for (i, a) in args.iter().enumerate() {
|
|
@@ -16809,7 +17692,75 @@ impl Codegen {
|
|
|
16809
17692
|
}
|
|
16810
17693
|
}
|
|
16811
17694
|
}
|
|
16812
|
-
|
|
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
|
+
}
|
|
16813
17764
|
}
|
|
16814
17765
|
}
|
|
16815
17766
|
|
|
@@ -16993,15 +17944,41 @@ impl Codegen {
|
|
|
16993
17944
|
)
|
|
16994
17945
|
}
|
|
16995
17946
|
|
|
16996
|
-
|
|
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
|
+
};
|
|
16997
17956
|
format!(
|
|
16998
|
-
"Value::NumberArray(VmRef::new(tishlang_runtime::NumArrayBacking::Packed({}.
|
|
16999
|
-
|
|
17957
|
+
"Value::NumberArray(VmRef::new(tishlang_runtime::NumArrayBacking::Packed({}.collect::<Vec<f64>>())))",
|
|
17958
|
+
elems
|
|
17000
17959
|
)
|
|
17001
17960
|
}
|
|
17002
17961
|
|
|
17003
17962
|
fn emit_module_const_f64_arrays(&mut self) -> Result<(), CompileError> {
|
|
17004
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
|
+
}
|
|
17005
17982
|
let lit = vals
|
|
17006
17983
|
.iter()
|
|
17007
17984
|
.map(|v| Self::f64_lit(*v))
|
|
@@ -17009,7 +17986,7 @@ impl Codegen {
|
|
|
17009
17986
|
.join(", ");
|
|
17010
17987
|
self.writeln(&format!(
|
|
17011
17988
|
"const {}: [f64; {}] = [{}];",
|
|
17012
|
-
|
|
17989
|
+
static_name,
|
|
17013
17990
|
vals.len(),
|
|
17014
17991
|
lit
|
|
17015
17992
|
));
|
|
@@ -17413,35 +18390,74 @@ impl Codegen {
|
|
|
17413
18390
|
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
17414
18391
|
Self::walk_stmts_for_global_disqualifiers(statements, g, bad);
|
|
17415
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.
|
|
17416
18398
|
Statement::If {
|
|
18399
|
+
cond,
|
|
17417
18400
|
then_branch,
|
|
17418
18401
|
else_branch,
|
|
17419
18402
|
..
|
|
17420
18403
|
} => {
|
|
18404
|
+
Self::walk_expr_for_global_disqualifiers(cond, g, bad);
|
|
17421
18405
|
Self::walk_stmt_for_global_disqualifiers(then_branch, g, bad);
|
|
17422
18406
|
if let Some(eb) = else_branch {
|
|
17423
18407
|
Self::walk_stmt_for_global_disqualifiers(eb, g, bad);
|
|
17424
18408
|
}
|
|
17425
18409
|
}
|
|
17426
|
-
Statement::While { body, .. }
|
|
17427
|
-
|
|
17428
|
-
|
|
17429
|
-
|
|
17430
|
-
|
|
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);
|
|
17431
18439
|
Self::walk_stmt_for_global_disqualifiers(body, g, bad);
|
|
17432
18440
|
}
|
|
17433
18441
|
Statement::Switch {
|
|
18442
|
+
expr,
|
|
17434
18443
|
cases,
|
|
17435
18444
|
default_body,
|
|
17436
18445
|
..
|
|
17437
18446
|
} => {
|
|
17438
|
-
|
|
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
|
+
}
|
|
17439
18452
|
Self::walk_stmts_for_global_disqualifiers(stmts, g, bad);
|
|
17440
18453
|
}
|
|
17441
18454
|
if let Some(d) = default_body {
|
|
17442
18455
|
Self::walk_stmts_for_global_disqualifiers(d, g, bad);
|
|
17443
18456
|
}
|
|
17444
18457
|
}
|
|
18458
|
+
Statement::Throw { value, .. } => {
|
|
18459
|
+
Self::walk_expr_for_global_disqualifiers(value, g, bad);
|
|
18460
|
+
}
|
|
17445
18461
|
Statement::Try {
|
|
17446
18462
|
body,
|
|
17447
18463
|
catch_body,
|
|
@@ -17640,6 +18656,151 @@ impl Codegen {
|
|
|
17640
18656
|
ok
|
|
17641
18657
|
}
|
|
17642
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
|
+
|
|
17643
18804
|
fn for_each_expr_root(stmts: &[Statement], f: &mut dyn FnMut(&Expr)) {
|
|
17644
18805
|
for s in stmts {
|
|
17645
18806
|
Self::for_each_expr_root_stmt(s, f);
|
|
@@ -17826,6 +18987,12 @@ impl Codegen {
|
|
|
17826
18987
|
Some(rt) => Self::ann_is_number(rt),
|
|
17827
18988
|
None => true,
|
|
17828
18989
|
};
|
|
18990
|
+
// THE SECOND SHAPE: an all-`i32` signature, which is what a device with no FPU
|
|
18991
|
+
// wants and what the existing gate excluded. The fixpoint below is unchanged and
|
|
18992
|
+
// still has to prove the body native-safe and every return numeric — the annotation
|
|
18993
|
+
// never decides that on its own (see `liar` in the issue).
|
|
18994
|
+
let i32_ok = Self::fn_sig_all_i32(params, return_type);
|
|
18995
|
+
let (params_ok, ret_ok) = (params_ok || i32_ok, ret_ok || i32_ok);
|
|
17829
18996
|
// #320: 0-param numeric fns (e.g. k_nucleotide's `nextBase()` — mutates a numeric
|
|
17830
18997
|
// global, returns number) are eligible too; the fixpoint below still proves the body
|
|
17831
18998
|
// native-safe + all-numeric-returns, so `fn name_native() -> f64` is sound.
|
|
@@ -18219,6 +19386,27 @@ impl Codegen {
|
|
|
18219
19386
|
}
|
|
18220
19387
|
}
|
|
18221
19388
|
|
|
19389
|
+
/// Can a top-level native `fn` name this indexed object? A parameter, a global with a static,
|
|
19390
|
+
/// a native-vec, or a local it declared itself — anything else is a binding that only exists
|
|
19391
|
+
/// inside `run()`.
|
|
19392
|
+
fn native_safe_indexable(
|
|
19393
|
+
object: &Expr,
|
|
19394
|
+
params: &std::collections::HashSet<String>,
|
|
19395
|
+
globals: &std::collections::HashSet<String>,
|
|
19396
|
+
nums: &HashSet<String>,
|
|
19397
|
+
native_vec_names: &std::collections::HashSet<String>,
|
|
19398
|
+
) -> bool {
|
|
19399
|
+
match object {
|
|
19400
|
+
Expr::Ident { name, .. } => {
|
|
19401
|
+
params.contains(name.as_ref())
|
|
19402
|
+
|| globals.contains(name.as_ref())
|
|
19403
|
+
|| nums.contains(name.as_ref())
|
|
19404
|
+
|| native_vec_names.contains(name.as_ref())
|
|
19405
|
+
}
|
|
19406
|
+
_ => false,
|
|
19407
|
+
}
|
|
19408
|
+
}
|
|
19409
|
+
|
|
18222
19410
|
fn native_safe_expr(
|
|
18223
19411
|
expr: &Expr,
|
|
18224
19412
|
params: &std::collections::HashSet<String>,
|
|
@@ -18241,24 +19429,45 @@ impl Codegen {
|
|
|
18241
19429
|
&& Self::native_safe_expr(value, params, cand, globals, nums, native_vec_names)
|
|
18242
19430
|
}
|
|
18243
19431
|
Expr::Binary { left, op, right, .. } => {
|
|
18244
|
-
matches!(
|
|
19432
|
+
let arith = matches!(
|
|
18245
19433
|
op,
|
|
18246
19434
|
BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow
|
|
18247
19435
|
| BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge
|
|
18248
19436
|
| BinOp::StrictEq | BinOp::StrictNe | BinOp::And | BinOp::Or
|
|
18249
|
-
)
|
|
19437
|
+
);
|
|
19438
|
+
// Bitwise, unlike in `numeric_shaped`, still recurses into its operands: there the
|
|
19439
|
+
// question is what the RESULT is (always a number), here it is whether the emitter
|
|
19440
|
+
// can lower the whole expression natively. Gated with its sibling so that off the
|
|
19441
|
+
// GBA numeric vocabulary eligibility is byte-identical to before.
|
|
19442
|
+
let bitwise = crate::types::gba_numerics_enabled()
|
|
19443
|
+
&& matches!(
|
|
19444
|
+
op,
|
|
19445
|
+
BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor
|
|
19446
|
+
| BinOp::Shl | BinOp::Shr | BinOp::UShr
|
|
19447
|
+
);
|
|
19448
|
+
(arith || bitwise)
|
|
19449
|
+
&& Self::native_safe_expr(left, params, cand, globals, nums, native_vec_names)
|
|
18250
19450
|
&& Self::native_safe_expr(right, params, cand, globals, nums, native_vec_names)
|
|
18251
19451
|
}
|
|
18252
19452
|
Expr::Unary { op, operand, .. } => {
|
|
18253
|
-
matches!(op, UnaryOp::Neg | UnaryOp::Pos | UnaryOp::Not)
|
|
19453
|
+
(matches!(op, UnaryOp::Neg | UnaryOp::Pos | UnaryOp::Not)
|
|
19454
|
+
|| (crate::types::gba_numerics_enabled()
|
|
19455
|
+
&& matches!(op, UnaryOp::BitNot)))
|
|
18254
19456
|
&& Self::native_safe_expr(operand, params, cand, globals, nums, native_vec_names)
|
|
18255
19457
|
}
|
|
18256
19458
|
Expr::PostfixInc { name, .. }
|
|
18257
19459
|
| Expr::PrefixInc { name, .. }
|
|
18258
19460
|
| Expr::PostfixDec { name, .. }
|
|
18259
19461
|
| Expr::PrefixDec { name, .. } => nums.contains(name.as_ref()),
|
|
19462
|
+
// THE INDEXED THING MUST BE IN SCOPE FOR A FREE `fn`, not merely be an identifier.
|
|
19463
|
+
// This accepted any name at all, and a native fn is emitted at top level where a
|
|
19464
|
+
// module-scope binding — a `let` inside `run()`, captured by the boxed closures — does
|
|
19465
|
+
// not exist: `function reads(i: i32): i32 { return ARR[i] & 255 }` compiled to a
|
|
19466
|
+
// `reads_native` whose body named `ARR`, and rustc rejected the generated program with
|
|
19467
|
+
// E0425. Masked until now because the globals set was only ever populated with things
|
|
19468
|
+
// that DO have a top-level static.
|
|
18260
19469
|
Expr::Index { object, index, .. } => {
|
|
18261
|
-
|
|
19470
|
+
Self::native_safe_indexable(object, params, globals, nums, native_vec_names)
|
|
18262
19471
|
&& Self::native_safe_expr(index, params, cand, globals, nums, native_vec_names)
|
|
18263
19472
|
}
|
|
18264
19473
|
Expr::IndexAssign {
|
|
@@ -18267,7 +19476,7 @@ impl Codegen {
|
|
|
18267
19476
|
value,
|
|
18268
19477
|
..
|
|
18269
19478
|
} => {
|
|
18270
|
-
|
|
19479
|
+
Self::native_safe_indexable(object, params, globals, nums, native_vec_names)
|
|
18271
19480
|
&& Self::native_safe_expr(index, params, cand, globals, nums, native_vec_names)
|
|
18272
19481
|
&& Self::native_safe_expr(value, params, cand, globals, nums, native_vec_names)
|
|
18273
19482
|
}
|
|
@@ -18347,6 +19556,26 @@ impl Codegen {
|
|
|
18347
19556
|
|| globals.contains(name.as_ref())
|
|
18348
19557
|
|| nums.contains(name.as_ref())
|
|
18349
19558
|
}
|
|
19559
|
+
// A BITWISE RESULT IS A NUMBER WHATEVER WENT IN. `& | ^ << >> >>>` are ToInt32/ToUint32
|
|
19560
|
+
// contexts in the language, so `"7" & 255` is 7 and `{} | 0` is 0 — the operands need
|
|
19561
|
+
// no proof of their own, which is why this arm does not recurse into them. Their
|
|
19562
|
+
// absence is not a small omission on a device with no FPU: `(a + b) | 0`, `a & 255` and
|
|
19563
|
+
// `w >> 16` are what integer code on such a target is made of, and every function
|
|
19564
|
+
// containing one was ineligible for a native `fn` no matter how it was annotated.
|
|
19565
|
+
Expr::Binary { op, .. }
|
|
19566
|
+
if crate::types::gba_numerics_enabled()
|
|
19567
|
+
&& matches!(
|
|
19568
|
+
op,
|
|
19569
|
+
BinOp::BitAnd
|
|
19570
|
+
| BinOp::BitOr
|
|
19571
|
+
| BinOp::BitXor
|
|
19572
|
+
| BinOp::Shl
|
|
19573
|
+
| BinOp::Shr
|
|
19574
|
+
| BinOp::UShr
|
|
19575
|
+
) =>
|
|
19576
|
+
{
|
|
19577
|
+
true
|
|
19578
|
+
}
|
|
18350
19579
|
Expr::Binary { left, op, right, .. } => {
|
|
18351
19580
|
matches!(
|
|
18352
19581
|
op,
|
|
@@ -18354,6 +19583,8 @@ impl Codegen {
|
|
|
18354
19583
|
) && Self::numeric_shaped(left, params, cand, globals, nums)
|
|
18355
19584
|
&& Self::numeric_shaped(right, params, cand, globals, nums)
|
|
18356
19585
|
}
|
|
19586
|
+
// Unary `~` is ToInt32 for the same reason.
|
|
19587
|
+
Expr::Unary { op: UnaryOp::BitNot, .. } if crate::types::gba_numerics_enabled() => true,
|
|
18357
19588
|
Expr::Unary { op, operand, .. } => {
|
|
18358
19589
|
matches!(op, UnaryOp::Neg | UnaryOp::Pos)
|
|
18359
19590
|
&& Self::numeric_shaped(operand, params, cand, globals, nums)
|
|
@@ -18500,6 +19731,46 @@ impl Codegen {
|
|
|
18500
19731
|
/// guarded fn is copy 0, the one with the stack check, so every entry from outside the cycle is
|
|
18501
19732
|
/// checked. Inside rotation copy `i`, an intra-SCC call targets the callee's copy `i+1` (the
|
|
18502
19733
|
/// checked copy 0 again after K levels).
|
|
19734
|
+
/// Bring one argument to a native fn's parameter type.
|
|
19735
|
+
///
|
|
19736
|
+
/// The f64 ABI accepts an `i32` expression as-is (Rust widens on the `as` the emitter already
|
|
19737
|
+
/// writes); the i32 ABI has to ToInt32 an f64 one, which is exactly the coercion the boxed
|
|
19738
|
+
/// path applied on entry — moved to the caller, where the value is usually already an integer
|
|
19739
|
+
/// and the conversion folds away.
|
|
19740
|
+
fn coerce_native_arg(&self, code: &str, from: RustType, want: RustType) -> String {
|
|
19741
|
+
if from == want {
|
|
19742
|
+
return code.to_string();
|
|
19743
|
+
}
|
|
19744
|
+
match (from, want) {
|
|
19745
|
+
(RustType::Value, w) => w.from_value_expr(code),
|
|
19746
|
+
(RustType::I32, RustType::F64) => format!("(({}) as f64)", code),
|
|
19747
|
+
// FIXED IS NOT AN f64 AND THE FALLTHROUGH BELOW USED TO PRETEND IT WAS. `fixed` is agb's
|
|
19748
|
+
// `Num<i32,8>`; handed to a native `f64` parameter it is a hard E0308 in the generated
|
|
19749
|
+
// program, not a slow path — which is how four call sites in one file of hyrule stopped
|
|
19750
|
+
// that game building the moment its helper became M5-eligible. The scaling is the same
|
|
19751
|
+
// one `RustType::Fixed`'s Value boundary uses (`types.rs:603`), so a value crossing here
|
|
19752
|
+
// reads identically to one crossing there.
|
|
19753
|
+
(RustType::Fixed, RustType::F64) => format!("(({}).to_raw() as f64 / 256.0)", code),
|
|
19754
|
+
// And the other direction, for the same reason: a native f64 reaching a `fixed`
|
|
19755
|
+
// parameter. Truncating, matching `Fixed::from_value_expr` (`types.rs:507`).
|
|
19756
|
+
(RustType::F64, RustType::Fixed) => format!(
|
|
19757
|
+
"tishlang_runtime::Fixed::from_raw((({}) * 256.0) as i32)",
|
|
19758
|
+
code
|
|
19759
|
+
),
|
|
19760
|
+
(RustType::I32, RustType::Fixed) => format!(
|
|
19761
|
+
"tishlang_runtime::Fixed::from_raw((({}) as i32).wrapping_mul(256))",
|
|
19762
|
+
code
|
|
19763
|
+
),
|
|
19764
|
+
// DIVIDE, DO NOT SHIFT. `>> 8` is an arithmetic shift and FLOORS: fixed -1.5 would
|
|
19765
|
+
// become -2 and -0.0039 would become -1. Rust integer division truncates toward zero,
|
|
19766
|
+
// which is what ToInt32 and `as i32` do, so -1.5 -> -1 and -0.0039 -> 0. The two agree
|
|
19767
|
+
// on every non-negative value, which is exactly why this would have shipped.
|
|
19768
|
+
(RustType::Fixed, RustType::I32) => format!("(({}).to_raw() / 256)", code),
|
|
19769
|
+
(_, RustType::I32) => format!("tishlang_runtime::to_int32({})", code),
|
|
19770
|
+
_ => code.to_string(),
|
|
19771
|
+
}
|
|
19772
|
+
}
|
|
19773
|
+
|
|
18503
19774
|
fn native_call_target(&self, fname: &str) -> String {
|
|
18504
19775
|
let base = format!("{}_native", Self::escape_ident(fname));
|
|
18505
19776
|
if let (Some((cur_scc, i, k)), Some(&(callee_scc, _))) =
|
|
@@ -18521,11 +19792,13 @@ impl Codegen {
|
|
|
18521
19792
|
if !self.native_fns.contains(name.as_ref()) {
|
|
18522
19793
|
continue;
|
|
18523
19794
|
}
|
|
19795
|
+
let i32_abi = self.native_fns_i32.contains(name.as_ref());
|
|
19796
|
+
let abi = if i32_abi { "i32" } else { "f64" };
|
|
18524
19797
|
let plist: Vec<String> = params
|
|
18525
19798
|
.iter()
|
|
18526
19799
|
.filter_map(|p| match p {
|
|
18527
19800
|
FunParam::Simple(tp) => {
|
|
18528
|
-
Some(format!("mut {}:
|
|
19801
|
+
Some(format!("mut {}: {}", Self::escape_ident(tp.name.as_ref()), abi))
|
|
18529
19802
|
}
|
|
18530
19803
|
_ => None,
|
|
18531
19804
|
})
|
|
@@ -18545,18 +19818,30 @@ impl Codegen {
|
|
|
18545
19818
|
self.type_context.push_scope();
|
|
18546
19819
|
for p in params {
|
|
18547
19820
|
if let FunParam::Simple(tp) = p {
|
|
18548
|
-
self.type_context.define(
|
|
19821
|
+
self.type_context.define(
|
|
19822
|
+
tp.name.as_ref(),
|
|
19823
|
+
if i32_abi { RustType::I32 } else { RustType::F64 },
|
|
19824
|
+
);
|
|
18549
19825
|
}
|
|
18550
19826
|
}
|
|
18551
19827
|
let copy_suffix = if copy_idx == 0 { String::new() } else { format!("_r{}", copy_idx) };
|
|
18552
|
-
self.writeln(&format!("fn {}_native{}({}) ->
|
|
19828
|
+
self.writeln(&format!("fn {}_native{}({}) -> {} {{", Self::escape_ident(name.as_ref()), copy_suffix, plist.join(", "), abi));
|
|
19829
|
+
let saved_i32_abi = self.native_fn_abi_i32;
|
|
19830
|
+
self.native_fn_abi_i32 = i32_abi;
|
|
18553
19831
|
self.indent += 1;
|
|
18554
19832
|
if copies > 1 {
|
|
18555
19833
|
self.native_recur_rotation = Some((scc_id, copy_idx, copies));
|
|
18556
19834
|
if copy_idx == 0 {
|
|
18557
19835
|
// The guard: one thread-local read + pointer compare; the cold bail parks
|
|
18558
19836
|
// the catchable RangeError and unwinds via the NaN sentinel (#381).
|
|
18559
|
-
|
|
19837
|
+
// The f64 bail unwinds through a NaN sentinel, which an `i32` return has no
|
|
19838
|
+
// room for. `compute_native_recur_fns` therefore never gives an i32-ABI fn
|
|
19839
|
+
// rotation copies — see the guard where it is called.
|
|
19840
|
+
self.writeln(if i32_abi {
|
|
19841
|
+
"if tishlang_runtime::stack_low() { return 0; }"
|
|
19842
|
+
} else {
|
|
19843
|
+
"if tishlang_runtime::stack_low() { return tishlang_runtime::recursion_tripped_f64(); }"
|
|
19844
|
+
});
|
|
18560
19845
|
}
|
|
18561
19846
|
}
|
|
18562
19847
|
if let Some((global, mul, add, modulus)) = self.native_lcg_fns.get(name.as_ref()) {
|
|
@@ -18628,10 +19913,11 @@ impl Codegen {
|
|
|
18628
19913
|
self.native_lcg_hoist.take();
|
|
18629
19914
|
self.native_lcg_hoist_int = false;
|
|
18630
19915
|
if !self.native_fn_body_returned {
|
|
18631
|
-
self.writeln("0.0");
|
|
19916
|
+
self.writeln(if i32_abi { "0" } else { "0.0" });
|
|
18632
19917
|
}
|
|
18633
19918
|
self.native_fn_emit_name = None;
|
|
18634
19919
|
}
|
|
19920
|
+
self.native_fn_abi_i32 = saved_i32_abi;
|
|
18635
19921
|
self.indent -= 1;
|
|
18636
19922
|
self.writeln("}");
|
|
18637
19923
|
self.type_context.pop_scope();
|
|
@@ -18688,7 +19974,20 @@ impl Codegen {
|
|
|
18688
19974
|
self.skip_iter_local = Some(iter);
|
|
18689
19975
|
}
|
|
18690
19976
|
}
|
|
18691
|
-
|
|
19977
|
+
// A NATIVE FN BODY IS A SCOPE, and it has to say so. `VarDecl` registers every
|
|
19978
|
+
// name it emits into `outer_vars_stack.last_mut()`; without a frame of its own,
|
|
19979
|
+
// a native fn's locals land in the MODULE frame — where they do not exist, because
|
|
19980
|
+
// the body was emitted inside `fn name_native(..)` at top level. Any later closure
|
|
19981
|
+
// referencing a same-named local of its own then sees the phantom in `outer_vars`
|
|
19982
|
+
// and emits `let i_cell = VmRef::new(i.clone());` against nothing: E0425, 49 of the
|
|
19983
|
+
// 114 tish-gba examples, every one of them with an `i` or `b` loop counter.
|
|
19984
|
+
//
|
|
19985
|
+
// The boxed FunDecl path has always pushed here (both of its arms do); this path
|
|
19986
|
+
// was written without one and only became reachable when M5 was enabled for GBA.
|
|
19987
|
+
self.outer_vars_stack.push(Vec::new());
|
|
19988
|
+
let r = self.emit_statements_with_folds(statements);
|
|
19989
|
+
self.outer_vars_stack.pop();
|
|
19990
|
+
r?;
|
|
18692
19991
|
if self.pending_stayed_var.is_none() {
|
|
18693
19992
|
self.skip_iter_local = None;
|
|
18694
19993
|
}
|
|
@@ -18696,10 +19995,31 @@ impl Codegen {
|
|
|
18696
19995
|
Statement::Return { value, .. } => {
|
|
18697
19996
|
let e = value.as_ref().expect("eligible return has a value");
|
|
18698
19997
|
let (code, ty) = self.emit_typed_expr(e)?;
|
|
18699
|
-
let
|
|
19998
|
+
let want = if self.native_fn_abi_i32 { RustType::I32 } else { RustType::F64 };
|
|
19999
|
+
// The int-domain shortcut FIRST for an i32 return. A bitwise expression types as
|
|
20000
|
+
// `F64` by design (`types.rs`), so taking the generic path below would emit
|
|
20001
|
+
// `to_int32(<i32 work> as f64)` — the exact round trip this ABI exists to remove,
|
|
20002
|
+
// reintroduced at the return. `emit_int32_operand` is the same helper the operator
|
|
20003
|
+
// fixes use.
|
|
20004
|
+
if want == RustType::I32 {
|
|
20005
|
+
if let Some(code) = self.emit_int32_operand(e)? {
|
|
20006
|
+
if let Some((global, _, _, _)) = self.native_lcg_hoist.as_ref() {
|
|
20007
|
+
let _ = global;
|
|
20008
|
+
}
|
|
20009
|
+
self.writeln(&format!("return {};", code));
|
|
20010
|
+
self.native_fn_body_returned = true;
|
|
20011
|
+
return Ok(());
|
|
20012
|
+
}
|
|
20013
|
+
}
|
|
20014
|
+
let (code, ty) = (code, ty);
|
|
20015
|
+
let f = if ty == want {
|
|
18700
20016
|
code
|
|
18701
20017
|
} else if ty == RustType::Value {
|
|
18702
|
-
|
|
20018
|
+
want.from_value_expr(&code)
|
|
20019
|
+
} else if want == RustType::I32 {
|
|
20020
|
+
// An f64-typed expression returned from an `i32` fn takes the same ToInt32 the
|
|
20021
|
+
// boxed path would have applied at the consumer.
|
|
20022
|
+
format!("tishlang_runtime::to_int32({})", code)
|
|
18703
20023
|
} else {
|
|
18704
20024
|
code
|
|
18705
20025
|
};
|
|
@@ -19336,6 +20656,17 @@ impl Codegen {
|
|
|
19336
20656
|
// fn either — its scalar params would be typed `f64` and mistype the arg (e.g. `fn rec(m){
|
|
19337
20657
|
// log.push(m) }` called `rec("ran")`). Same gate the M4/M5/S-0 paths use.
|
|
19338
20658
|
let nonnumeric = Self::fns_called_with_nonnumeric_arg(&program.statements);
|
|
20659
|
+
// Every top-level fn name. A CALL to one is validated separately (`vec_body_calls_native`);
|
|
20660
|
+
// what matters here is that the identifier itself is not a `run()` local the way a module
|
|
20661
|
+
// `let` or a builtin like `Math` is.
|
|
20662
|
+
let fn_names: std::collections::HashSet<String> = program
|
|
20663
|
+
.statements
|
|
20664
|
+
.iter()
|
|
20665
|
+
.filter_map(|s| match s {
|
|
20666
|
+
Statement::FunDecl { name, .. } => Some(name.to_string()),
|
|
20667
|
+
_ => None,
|
|
20668
|
+
})
|
|
20669
|
+
.collect();
|
|
19339
20670
|
for s in &program.statements {
|
|
19340
20671
|
if let Statement::FunDecl {
|
|
19341
20672
|
async_: false,
|
|
@@ -19362,12 +20693,41 @@ impl Codegen {
|
|
|
19362
20693
|
.params
|
|
19363
20694
|
.iter()
|
|
19364
20695
|
.any(|(_, k)| matches!(k, VecParamKind::Array { .. }));
|
|
19365
|
-
|
|
20696
|
+
// #605: a scalars-in/STRING-out fn has neither an array parameter nor a local
|
|
20697
|
+
// vec op, so it never reached this group and stayed a boxed closure — which is
|
|
20698
|
+
// what "VecRetKind has no String" amounts to in practice. The return shape earns
|
|
20699
|
+
// admission on its own, but ONLY under three extra conditions, each of which a
|
|
20700
|
+
// review found by breaking the version without it:
|
|
20701
|
+
//
|
|
20702
|
+
// 1. TOTAL. `vec_fn_return_kind` rejects an explicit bare `return;` but cannot
|
|
20703
|
+
// see an IMPLICIT fall-through, and the lowered fn's tail yields
|
|
20704
|
+
// `String::new()` where the boxed one yields `Value::Null` — so
|
|
20705
|
+
// `function label(n){ if(n>0){return "pos"} }` returned "" instead of null.
|
|
20706
|
+
// A wrong value, silently, from ordinary code.
|
|
20707
|
+
// 2. NO STRING-COMPARED PARAM. `scan_param_use` counts `===` as arithmetic, so
|
|
20708
|
+
// `tag === "fire"` "proves" `tag` numeric and the param takes the f64 ABI —
|
|
20709
|
+
// a `panic!("expected number")` on a no_std cart, or E0308. (infer.rs treats
|
|
20710
|
+
// the same operator as OVERLOADED for exactly this reason.)
|
|
20711
|
+
// 3. NO MODULE READS, on EVERY target. A string or object module binding has a
|
|
20712
|
+
// top-level home nowhere, and this is the first rule that admits a fn shaped
|
|
20713
|
+
// to read one, so the GBA-only gate below is not enough.
|
|
20714
|
+
let string_out = sig.ret == VecRetKind::Str
|
|
20715
|
+
&& Self::body_always_returns(body)
|
|
20716
|
+
&& Self::body_names_only_its_own(params, body, &fn_names);
|
|
20717
|
+
if has_array_param || Self::body_uses_local_vec_ops(body) || string_out {
|
|
19366
20718
|
sigs.insert(name.to_string(), sig);
|
|
19367
20719
|
}
|
|
19368
20720
|
}
|
|
19369
20721
|
}
|
|
19370
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();
|
|
19371
20731
|
loop {
|
|
19372
20732
|
let mut remove: Vec<String> = Vec::new();
|
|
19373
20733
|
for s in &program.statements {
|
|
@@ -19379,6 +20739,7 @@ impl Codegen {
|
|
|
19379
20739
|
break;
|
|
19380
20740
|
}
|
|
19381
20741
|
for n in remove {
|
|
20742
|
+
rejected.insert(n.clone());
|
|
19382
20743
|
sigs.remove(&n);
|
|
19383
20744
|
}
|
|
19384
20745
|
if sigs.is_empty() {
|
|
@@ -19414,6 +20775,22 @@ impl Codegen {
|
|
|
19414
20775
|
if Self::ann_is_simple(return_type.as_ref(), "fixed") {
|
|
19415
20776
|
continue;
|
|
19416
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
|
+
}
|
|
19417
20794
|
if let Some(sig) = Self::infer_vec_fn_sig_forwarding(params, body, &sigs) {
|
|
19418
20795
|
let has_array_param = sig
|
|
19419
20796
|
.params
|
|
@@ -19440,6 +20817,7 @@ impl Codegen {
|
|
|
19440
20817
|
break;
|
|
19441
20818
|
}
|
|
19442
20819
|
for n in remove {
|
|
20820
|
+
rejected.insert(n.clone());
|
|
19443
20821
|
sigs.remove(&n);
|
|
19444
20822
|
}
|
|
19445
20823
|
if sigs.is_empty() {
|
|
@@ -19874,6 +21252,100 @@ impl Codegen {
|
|
|
19874
21252
|
None
|
|
19875
21253
|
}
|
|
19876
21254
|
|
|
21255
|
+
/// Does `body` name ONLY things a free `fn` can see — its own parameters, its own locals, and
|
|
21256
|
+
/// other top-level fns? Anything else (a module `let`, or a builtin like `Math`/`console`) is a
|
|
21257
|
+
/// local of `run()` in the emitted program, and a free fn naming one is E0425.
|
|
21258
|
+
///
|
|
21259
|
+
/// Stricter than the module-binding check it replaced for this door, and deliberately so:
|
|
21260
|
+
/// `regex_redux`'s `nextBase()` is scalars-in/string-out, reaches for `Math.floor`, and the
|
|
21261
|
+
/// native-vec emitter lowers that to a `get_prop` against the `Math` binding — which is not a
|
|
21262
|
+
/// module `let`, so a module-only guard waved it through and the fixture stopped building.
|
|
21263
|
+
fn body_names_only_its_own(
|
|
21264
|
+
params: &[FunParam],
|
|
21265
|
+
body: &Statement,
|
|
21266
|
+
fn_names: &std::collections::HashSet<String>,
|
|
21267
|
+
) -> bool {
|
|
21268
|
+
let mut referenced = HashSet::new();
|
|
21269
|
+
Self::collect_stmt_idents(body, &mut referenced);
|
|
21270
|
+
let mut bound: HashSet<String> = params
|
|
21271
|
+
.iter()
|
|
21272
|
+
.flat_map(|p| p.bound_names())
|
|
21273
|
+
.map(|n| n.to_string())
|
|
21274
|
+
.collect();
|
|
21275
|
+
Self::collect_local_var_names(body, &mut bound);
|
|
21276
|
+
referenced
|
|
21277
|
+
.iter()
|
|
21278
|
+
.all(|n| bound.contains(n) || fn_names.contains(n))
|
|
21279
|
+
}
|
|
21280
|
+
|
|
21281
|
+
/// Does every path through `body` end in an explicit `return <expr>`? Conservative: only a
|
|
21282
|
+
/// trailing `return`, or an if/else where BOTH arms are total, counts. Anything else may fall
|
|
21283
|
+
/// off the end, and a lowered fn's tail default (`String::new()` / `0.0`) is not what the boxed
|
|
21284
|
+
/// path yields there (`Value::Null`).
|
|
21285
|
+
fn body_always_returns(body: &Statement) -> bool {
|
|
21286
|
+
match body {
|
|
21287
|
+
Statement::Return { value: Some(_), .. } => true,
|
|
21288
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
21289
|
+
statements.last().is_some_and(Self::body_always_returns)
|
|
21290
|
+
}
|
|
21291
|
+
Statement::If { then_branch, else_branch, .. } => {
|
|
21292
|
+
else_branch.as_ref().is_some_and(|e| {
|
|
21293
|
+
Self::body_always_returns(then_branch) && Self::body_always_returns(e)
|
|
21294
|
+
})
|
|
21295
|
+
}
|
|
21296
|
+
_ => false,
|
|
21297
|
+
}
|
|
21298
|
+
}
|
|
21299
|
+
|
|
21300
|
+
/// Every module-level `let`/`const` name. On GBA these are locals of `run()` — nothing gives
|
|
21301
|
+
/// them a top-level home — so a free `fn` naming one does not compile.
|
|
21302
|
+
fn collect_module_level_binding_names(
|
|
21303
|
+
statements: &[Statement],
|
|
21304
|
+
) -> std::collections::HashSet<String> {
|
|
21305
|
+
let mut out = std::collections::HashSet::new();
|
|
21306
|
+
for s in statements {
|
|
21307
|
+
Self::collect_module_binding_names_of(s, &mut out);
|
|
21308
|
+
}
|
|
21309
|
+
out
|
|
21310
|
+
}
|
|
21311
|
+
|
|
21312
|
+
/// One statement's module-level bindings. `Multi` matters: `let a = 1, b = 2` parses to it, and
|
|
21313
|
+
/// codegen emits each declarator into the CURRENT scope — so at module level those are `run()`
|
|
21314
|
+
/// locals exactly like a plain `let`, and missing them let a free fn name one (E0425).
|
|
21315
|
+
fn collect_module_binding_names_of(s: &Statement, out: &mut std::collections::HashSet<String>) {
|
|
21316
|
+
match s {
|
|
21317
|
+
Statement::VarDecl { name, .. } => {
|
|
21318
|
+
out.insert(name.to_string());
|
|
21319
|
+
}
|
|
21320
|
+
Statement::VarDeclDestructure { pattern, .. } => {
|
|
21321
|
+
Self::collect_destruct_names(pattern, out);
|
|
21322
|
+
}
|
|
21323
|
+
Statement::Multi { statements, .. } => {
|
|
21324
|
+
for inner in statements {
|
|
21325
|
+
Self::collect_module_binding_names_of(inner, out);
|
|
21326
|
+
}
|
|
21327
|
+
}
|
|
21328
|
+
_ => {}
|
|
21329
|
+
}
|
|
21330
|
+
}
|
|
21331
|
+
|
|
21332
|
+
/// Does `body` read a module binding it would not be able to see as a free `fn`? Names the body
|
|
21333
|
+
/// binds itself (params are checked by the caller; locals here) shadow the module one, so they
|
|
21334
|
+
/// do not count — that is the same shadow rule #556 established for parameters.
|
|
21335
|
+
fn body_reads_module_binding(
|
|
21336
|
+
body: &Statement,
|
|
21337
|
+
module_bindings: &std::collections::HashSet<String>,
|
|
21338
|
+
) -> bool {
|
|
21339
|
+
// NO SHADOW SUBTRACTION, deliberately. `collect_local_var_names` flattens every nested
|
|
21340
|
+
// block, branch and loop into one set with no scope structure, so an inner
|
|
21341
|
+
// `if (..) { let CFG = 0 }` would mask a module `CFG` read elsewhere in the body and let the
|
|
21342
|
+
// fn lower with a free name (E0425). Over-rejecting costs a fn its lowering; under-rejecting
|
|
21343
|
+
// costs the whole program its build.
|
|
21344
|
+
let mut referenced = HashSet::new();
|
|
21345
|
+
Self::collect_stmt_idents(body, &mut referenced);
|
|
21346
|
+
referenced.iter().any(|n| module_bindings.contains(n))
|
|
21347
|
+
}
|
|
21348
|
+
|
|
19877
21349
|
/// Detect, validate, and emit the native-vec fn group. On any gap the group is left empty.
|
|
19878
21350
|
fn setup_native_vec_fns(&mut self, program: &Program) {
|
|
19879
21351
|
use std::collections::HashMap;
|
|
@@ -19881,6 +21353,55 @@ impl Codegen {
|
|
|
19881
21353
|
// #177 aggregate fns (`advance`/`offsetMomentum` on `Vec<Struct>`) take precedence — do not
|
|
19882
21354
|
// also emit a broken `name_nv(&Vec<f64>)` wrapper for them.
|
|
19883
21355
|
sigs.retain(|name, _| !self.aggregate_fns.contains_key(name));
|
|
21356
|
+
// ON GBA, A MODULE BINDING IS NOT REACHABLE FROM A FREE `fn`. The passes that give module
|
|
21357
|
+
// state a top-level home — `native_numeric_globals`' TLS cells and `module_const_f64_arrays`'
|
|
21358
|
+
// statics — are the std-emitting ones, and they are gated off for GBA. So every module
|
|
21359
|
+
// `let`/`const` is a local of `run()` there, while `name_nv` is emitted at top level: a body
|
|
21360
|
+
// that reads one compiles to `error[E0425]: cannot find value CFG in this scope` and the
|
|
21361
|
+
// game does not build (#594).
|
|
21362
|
+
//
|
|
21363
|
+
// Reject those fns here rather than emitting a broken program. They stay on the boxed path,
|
|
21364
|
+
// which is correct and merely slower. Scoped to GBA deliberately: off-target the statics DO
|
|
21365
|
+
// exist, the reference resolves, and eligibility must stay byte-identical to before.
|
|
21366
|
+
//
|
|
21367
|
+
// This does not close #594's other half — a lowered fn that can actually SEE module state,
|
|
21368
|
+
// which needs those bindings emitted as statics on GBA too. It stops the miscompile.
|
|
21369
|
+
if self.emit_mode == crate::NativeEmitMode::Gba {
|
|
21370
|
+
let mut module_bindings = Self::collect_module_level_binding_names(&program.statements);
|
|
21371
|
+
// …EXCEPT the ones that now have a top-level home. Since #594 a module const numeric
|
|
21372
|
+
// array is a `const` and a mutable numeric global is a `SingleCore` static, both
|
|
21373
|
+
// reachable from a free fn — rejecting a fn for reading those would give up exactly the
|
|
21374
|
+
// lowering this issue exists to enable. What remains unreachable is everything else:
|
|
21375
|
+
// boxed objects, strings, arrays of non-numbers.
|
|
21376
|
+
for k in self.module_const_f64_arrays.keys() {
|
|
21377
|
+
module_bindings.remove(k);
|
|
21378
|
+
}
|
|
21379
|
+
for k in self.native_numeric_globals.keys() {
|
|
21380
|
+
module_bindings.remove(k);
|
|
21381
|
+
}
|
|
21382
|
+
for k in self.module_const_f64_cum.keys() {
|
|
21383
|
+
module_bindings.remove(k);
|
|
21384
|
+
}
|
|
21385
|
+
for k in self.module_const_f64_aliases.keys() {
|
|
21386
|
+
module_bindings.remove(k);
|
|
21387
|
+
}
|
|
21388
|
+
if !module_bindings.is_empty() {
|
|
21389
|
+
let bodies_by_name: std::collections::HashMap<&str, &Statement> = program
|
|
21390
|
+
.statements
|
|
21391
|
+
.iter()
|
|
21392
|
+
.filter_map(|s| match s {
|
|
21393
|
+
Statement::FunDecl { name, body, .. } => Some((name.as_ref(), body.as_ref())),
|
|
21394
|
+
_ => None,
|
|
21395
|
+
})
|
|
21396
|
+
.collect();
|
|
21397
|
+
sigs.retain(|name, _| {
|
|
21398
|
+
let Some(body) = bodies_by_name.get(name.as_str()) else {
|
|
21399
|
+
return true;
|
|
21400
|
+
};
|
|
21401
|
+
!Self::body_reads_module_binding(body, &module_bindings)
|
|
21402
|
+
});
|
|
21403
|
+
}
|
|
21404
|
+
}
|
|
19884
21405
|
if sigs.is_empty() {
|
|
19885
21406
|
return;
|
|
19886
21407
|
}
|
|
@@ -20209,7 +21730,7 @@ impl Codegen {
|
|
|
20209
21730
|
}
|
|
20210
21731
|
}
|
|
20211
21732
|
Expr::Binary { left, op, right, .. } => {
|
|
20212
|
-
let
|
|
21733
|
+
let arith = matches!(
|
|
20213
21734
|
op,
|
|
20214
21735
|
BinOp::Add
|
|
20215
21736
|
| BinOp::Sub
|
|
@@ -20221,9 +21742,17 @@ impl Codegen {
|
|
|
20221
21742
|
| BinOp::Le
|
|
20222
21743
|
| BinOp::Gt
|
|
20223
21744
|
| BinOp::Ge
|
|
20224
|
-
| BinOp::StrictEq
|
|
20225
|
-
| BinOp::StrictNe
|
|
20226
21745
|
);
|
|
21746
|
+
// `===`/`!==` ARE OVERLOADED — they compare strings as readily as numbers, so one is
|
|
21747
|
+
// proof of a numeric operand ONLY when the other side is a numeric literal.
|
|
21748
|
+
// `tag === "fire"` used to mark `tag` numeric, which handed a string parameter the
|
|
21749
|
+
// f64 ABI: `panic!("expected number")` on a no_std cart, or E0308. `infer.rs`
|
|
21750
|
+
// classifies the same operator as overloaded for exactly this reason, and the two
|
|
21751
|
+
// disagreeing is what let it through here.
|
|
21752
|
+
let num_lit = |x: &Expr| matches!(x, Expr::Literal { value: Literal::Number(_), .. });
|
|
21753
|
+
let eq_numeric = matches!(op, BinOp::StrictEq | BinOp::StrictNe)
|
|
21754
|
+
&& (num_lit(left) || num_lit(right));
|
|
21755
|
+
let a = arith || eq_numeric;
|
|
20227
21756
|
Self::scan_param_use(left, p, a, f);
|
|
20228
21757
|
Self::scan_param_use(right, p, a, f);
|
|
20229
21758
|
}
|
|
@@ -20336,6 +21865,17 @@ impl Codegen {
|
|
|
20336
21865
|
{
|
|
20337
21866
|
return Some(VecRetKind::F64);
|
|
20338
21867
|
}
|
|
21868
|
+
// #605: EVERY return a string literal. Held to the same standard as the numeric arm above —
|
|
21869
|
+
// agreement across all returns, proven from the expressions, never from the annotation,
|
|
21870
|
+
// because a return type is erased and unchecked (the `liar()` case). A mixed
|
|
21871
|
+
// string-and-number fn falls through to `None` and stays boxed, which is the only shape a
|
|
21872
|
+
// single native signature cannot express.
|
|
21873
|
+
if ret_exprs
|
|
21874
|
+
.iter()
|
|
21875
|
+
.all(|e| matches!(e, Expr::Literal { value: Literal::String(_), .. }))
|
|
21876
|
+
{
|
|
21877
|
+
return Some(VecRetKind::Str);
|
|
21878
|
+
}
|
|
20339
21879
|
None
|
|
20340
21880
|
}
|
|
20341
21881
|
|
|
@@ -20658,6 +22198,7 @@ impl Codegen {
|
|
|
20658
22198
|
match sig.ret {
|
|
20659
22199
|
VecRetKind::F64 => Some(RustType::F64),
|
|
20660
22200
|
VecRetKind::VecF64 => Some(RustType::Vec(Box::new(RustType::F64))),
|
|
22201
|
+
VecRetKind::Str => Some(RustType::String),
|
|
20661
22202
|
VecRetKind::Unit => None,
|
|
20662
22203
|
}
|
|
20663
22204
|
}
|
|
@@ -20696,6 +22237,7 @@ impl Codegen {
|
|
|
20696
22237
|
let ret_str = match sig.ret {
|
|
20697
22238
|
VecRetKind::F64 => " -> f64",
|
|
20698
22239
|
VecRetKind::VecF64 => " -> Vec<f64>",
|
|
22240
|
+
VecRetKind::Str => " -> String",
|
|
20699
22241
|
VecRetKind::Unit => "",
|
|
20700
22242
|
};
|
|
20701
22243
|
self.writeln("#[allow(non_snake_case, unused)]");
|
|
@@ -20759,6 +22301,11 @@ impl Codegen {
|
|
|
20759
22301
|
if sig.ret == VecRetKind::F64 {
|
|
20760
22302
|
self.writeln("0.0");
|
|
20761
22303
|
}
|
|
22304
|
+
// Same total-function guarantee for the string arm: falling off the end yields "" rather
|
|
22305
|
+
// than leaving the emitted fn without a value on that path.
|
|
22306
|
+
if sig.ret == VecRetKind::Str {
|
|
22307
|
+
self.writeln("String::new()");
|
|
22308
|
+
}
|
|
20762
22309
|
self.native_vec_ret = saved_ret;
|
|
20763
22310
|
self.vec_ref_params = saved_refs;
|
|
20764
22311
|
self.indent -= 1;
|
|
@@ -20849,6 +22396,13 @@ impl Codegen {
|
|
|
20849
22396
|
call
|
|
20850
22397
|
}
|
|
20851
22398
|
}
|
|
22399
|
+
VecRetKind::Str => {
|
|
22400
|
+
if as_value {
|
|
22401
|
+
format!("Value::String(({}).into())", call)
|
|
22402
|
+
} else {
|
|
22403
|
+
call
|
|
22404
|
+
}
|
|
22405
|
+
}
|
|
20852
22406
|
VecRetKind::Unit => {
|
|
20853
22407
|
if as_value {
|
|
20854
22408
|
format!("{{ {}; Value::Null }}", call)
|
|
@@ -23329,6 +24883,60 @@ impl Codegen {
|
|
|
23329
24883
|
/// the round-trips. Crucially, only bitwise/shift nodes recurse — an `f64` `*`/`+`/`-` node is
|
|
23330
24884
|
/// a *leaf* here, so e.g. `(h * 16777619) >>> 0` keeps its `f64` multiply (the 2^53 rule: the
|
|
23331
24885
|
/// product exceeds 2^53 and must round in `f64` *before* `ToUint32`, exactly as V8 does).
|
|
24886
|
+
/// Int-domain code for a bitwise/shift expression that is about to be consumed by something
|
|
24887
|
+
/// wanting an INTEGER — a `Vec<i32>` element store, an array index.
|
|
24888
|
+
///
|
|
24889
|
+
/// `result_type_of_binop` (types.rs) types every bitwise result `F64`, which is semantically
|
|
24890
|
+
/// right (JS bitwise returns a Number) and a real win against boxing. But the int-domain chain
|
|
24891
|
+
/// is then wrapped `(… ) as f64` and an integer consumer immediately casts it back, so on a
|
|
24892
|
+
/// target with no FPU every `arr[i] = w | BIT` pays a soft-float round trip in each direction.
|
|
24893
|
+
/// Measured on GBA (tish-gba `examples/bench-grid`): `arr[j] = 7` and `arr[j] = j` cost ~1.4
|
|
24894
|
+
/// ticks per 1000, `arr[j] = j & 255` cost 23.2, and a bitwise INDEX cost 19.9 — ~15x, twice per
|
|
24895
|
+
/// cell access for anything built on a packed word.
|
|
24896
|
+
///
|
|
24897
|
+
/// SOUNDNESS. Only the SIGNED-result ops qualify. Their value is by construction inside i32, so
|
|
24898
|
+
/// `(x as f64) as i32` is the identity and dropping both casts cannot change it. `>>>` is
|
|
24899
|
+
/// excluded deliberately: it yields a uint32 Number that can exceed `i32::MAX`, and Rust's
|
|
24900
|
+
/// float→int `as` SATURATES, so the f64 route and the int route genuinely disagree there.
|
|
24901
|
+
fn emit_int32_for_int_consumer(
|
|
24902
|
+
&mut self,
|
|
24903
|
+
e: &Expr,
|
|
24904
|
+
) -> Result<Option<String>, CompileError> {
|
|
24905
|
+
let Expr::Binary { op, .. } = e else {
|
|
24906
|
+
return Ok(None);
|
|
24907
|
+
};
|
|
24908
|
+
if !matches!(
|
|
24909
|
+
op,
|
|
24910
|
+
BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor | BinOp::Shl | BinOp::Shr
|
|
24911
|
+
) {
|
|
24912
|
+
return Ok(None);
|
|
24913
|
+
}
|
|
24914
|
+
self.emit_int32_operand(e)
|
|
24915
|
+
}
|
|
24916
|
+
|
|
24917
|
+
/// An operand that is provably an int32: either one `emit_int32_operand` lowers, or one the
|
|
24918
|
+
/// type system already calls `i32`.
|
|
24919
|
+
///
|
|
24920
|
+
/// The second case is what `emit_int32_operand` alone misses. An `: i32` local and a `Vec<i32>`
|
|
24921
|
+
/// element read need no ToInt32 lowering — they ARE integers — but that function's leaf fold
|
|
24922
|
+
/// only recognises `F64` leaves, so it declines them and falls back to a `to_int32` wrapper.
|
|
24923
|
+
/// Every accumulator has one on its left-hand side.
|
|
24924
|
+
fn int32_or_native_i32(&mut self, e: &Expr) -> Result<Option<String>, CompileError> {
|
|
24925
|
+
if let Some(c) = self.emit_int32_operand(e)? {
|
|
24926
|
+
// Reject the generic fold's own output: if it handed back a `to_int32(..)` wrapper it
|
|
24927
|
+
// has not actually reached the integer domain, and using it would keep the round trip
|
|
24928
|
+
// while claiming to have removed it.
|
|
24929
|
+
if !c.contains("to_int32") {
|
|
24930
|
+
return Ok(Some(c));
|
|
24931
|
+
}
|
|
24932
|
+
}
|
|
24933
|
+
let (code, ty) = self.emit_typed_expr(e)?;
|
|
24934
|
+
if ty == RustType::I32 {
|
|
24935
|
+
return Ok(Some(code));
|
|
24936
|
+
}
|
|
24937
|
+
Ok(None)
|
|
24938
|
+
}
|
|
24939
|
+
|
|
23332
24940
|
fn emit_int32_operand(&mut self, e: &Expr) -> Result<Option<String>, CompileError> {
|
|
23333
24941
|
if let Expr::Binary {
|
|
23334
24942
|
left, op, right, ..
|
|
@@ -23369,6 +24977,33 @@ impl Codegen {
|
|
|
23369
24977
|
};
|
|
23370
24978
|
return Ok(Some(code));
|
|
23371
24979
|
}
|
|
24980
|
+
// ARITHMETIC in a ToInt32 context: `acc + (w & MASK)`, `i - 1`.
|
|
24981
|
+
//
|
|
24982
|
+
// `+` is not itself a ToInt32 context, so this cannot be done in general — but THIS
|
|
24983
|
+
// FUNCTION'S CONTRACT is that its result will be ToInt32'd by the caller, and for int32
|
|
24984
|
+
// operands `to_int32(a + b) == a.wrapping_add(b)` exactly, since |a + b| < 2^32 is far
|
|
24985
|
+
// inside f64's exact-integer range. So inside this context, and only here, the integer
|
|
24986
|
+
// form is equivalent.
|
|
24987
|
+
//
|
|
24988
|
+
// It has to sit ABOVE the generic leaf fold below, which would otherwise swallow the
|
|
24989
|
+
// whole expression and hand back `to_int32((a as f64) + (b as f64))` — technically a
|
|
24990
|
+
// success, and exactly the round trip this exists to remove.
|
|
24991
|
+
//
|
|
24992
|
+
// Measured on GBA (tish-gba `examples/bench-grid`): `acc = acc + (arr[i] & 255)` ran at
|
|
24993
|
+
// 44.4 ticks an iteration against 1.64 for the same read unmasked — a 27x penalty on
|
|
24994
|
+
// every packed-word structure, since `w & FIELD` is how you read a field.
|
|
24995
|
+
if matches!(op, BinOp::Add | BinOp::Sub) {
|
|
24996
|
+
let li = self.int32_or_native_i32(left)?;
|
|
24997
|
+
let ri = self.int32_or_native_i32(right)?;
|
|
24998
|
+
if let (Some(li), Some(ri)) = (li, ri) {
|
|
24999
|
+
let f = if matches!(op, BinOp::Add) {
|
|
25000
|
+
"wrapping_add"
|
|
25001
|
+
} else {
|
|
25002
|
+
"wrapping_sub"
|
|
25003
|
+
};
|
|
25004
|
+
return Ok(Some(format!("({}).{}({})", li, f, ri)));
|
|
25005
|
+
}
|
|
25006
|
+
}
|
|
23372
25007
|
}
|
|
23373
25008
|
// `Math.imul(a, b)` in an int32 context → the raw i32 `wrapping_mul` (no f64 excursion, no
|
|
23374
25009
|
// `to_int32` round-trip on the product), when both operands are themselves int32-lowerable.
|
|
@@ -23889,33 +25524,47 @@ impl Codegen {
|
|
|
23889
25524
|
iv
|
|
23890
25525
|
)
|
|
23891
25526
|
};
|
|
23892
|
-
let
|
|
23893
|
-
|
|
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)
|
|
23894
25555
|
} else {
|
|
23895
|
-
|
|
23896
|
-
|
|
23897
|
-
|
|
23898
|
-
|
|
23899
|
-
|
|
23900
|
-
|
|
23901
|
-
|
|
23902
|
-
|
|
23903
|
-
|
|
23904
|
-
|
|
23905
|
-
|
|
23906
|
-
|
|
23907
|
-
idx_usize,
|
|
23908
|
-
const_len,
|
|
23909
|
-
static_name
|
|
23910
|
-
)
|
|
23911
|
-
} else {
|
|
23912
|
-
format!(
|
|
23913
|
-
"{{ let _i = {}; if _i < {}.len() {{ {}[_i] }} else {{ f64::NAN }} }}",
|
|
23914
|
-
idx_usize,
|
|
23915
|
-
static_name,
|
|
23916
|
-
static_name
|
|
23917
|
-
)
|
|
23918
|
-
}
|
|
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
|
+
)
|
|
23919
25568
|
};
|
|
23920
25569
|
return Ok((access, RustType::F64));
|
|
23921
25570
|
}
|
|
@@ -23944,45 +25593,15 @@ impl Codegen {
|
|
|
23944
25593
|
};
|
|
23945
25594
|
// #173 part 3: prove the index in-bounds BEFORE emitting it.
|
|
23946
25595
|
let in_bounds = self.index_in_bounds(index, name.as_ref());
|
|
23947
|
-
|
|
23948
|
-
|
|
23949
|
-
|
|
23950
|
-
|
|
23951
|
-
|
|
23952
|
-
|
|
23953
|
-
|
|
23954
|
-
|
|
23955
|
-
|
|
23956
|
-
if idx_ty == RustType::F64 || idx_ty == RustType::I32 {
|
|
23957
|
-
format!("({}) as usize", idx_code)
|
|
23958
|
-
} else {
|
|
23959
|
-
let iv = if idx_ty.is_native() {
|
|
23960
|
-
idx_ty.to_value_expr(&idx_code)
|
|
23961
|
-
} else {
|
|
23962
|
-
idx_code
|
|
23963
|
-
};
|
|
23964
|
-
format!(
|
|
23965
|
-
"{{ let _i = &{}; if let Value::Number(n) = _i {{ *n as usize }} else {{ panic!(\"array index must be a number\") }} }}",
|
|
23966
|
-
iv
|
|
23967
|
-
)
|
|
23968
|
-
}
|
|
23969
|
-
}
|
|
23970
|
-
} else {
|
|
23971
|
-
let (idx_code, idx_ty) = self.emit_typed_expr(index)?;
|
|
23972
|
-
if idx_ty == RustType::F64 || idx_ty == RustType::I32 {
|
|
23973
|
-
format!("({}) as usize", idx_code)
|
|
23974
|
-
} else {
|
|
23975
|
-
let iv = if idx_ty.is_native() {
|
|
23976
|
-
idx_ty.to_value_expr(&idx_code)
|
|
23977
|
-
} else {
|
|
23978
|
-
idx_code
|
|
23979
|
-
};
|
|
23980
|
-
format!(
|
|
23981
|
-
"{{ let _i = &{}; if let Value::Number(n) = _i {{ *n as usize }} else {{ panic!(\"array index must be a number\") }} }}",
|
|
23982
|
-
iv
|
|
23983
|
-
)
|
|
23984
|
-
}
|
|
23985
|
-
};
|
|
25596
|
+
// The READ index goes through the same helper as the element STORE.
|
|
25597
|
+
// These two arms used to be a hand-inlined copy of it that predated
|
|
25598
|
+
// the int-domain shortcut, so `arr[a | b]` lowered its store index
|
|
25599
|
+
// integrally and its read index as `((a|b) as f64) as usize` — two
|
|
25600
|
+
// soft-float calls on ARM7TDMI, on the read half of every packed-grid
|
|
25601
|
+
// access. Measured in tish-gba: a 63-cell board copy whose body is
|
|
25602
|
+
// `P[dc|r] = P[sc|r] & keep` cost 2,103 ticks, 33 per cell, against
|
|
25603
|
+
// ~1.6 for a module `i32[]` read plus write.
|
|
25604
|
+
let idx_usize = self.emit_index_usize(index)?;
|
|
23986
25605
|
// #567: inside the wrapped block the index is pre-bound to `__bi`
|
|
23987
25606
|
// (evaluated BEFORE the borrow, so an index that itself reads this cell
|
|
23988
25607
|
// — `arr[arr.length-1]` — doesn't double-lock the non-reentrant cell).
|
|
@@ -24017,6 +25636,19 @@ impl Codegen {
|
|
|
24017
25636
|
// `borrow()` guard a non-Copy element must be cloned OUT (it can't be
|
|
24018
25637
|
// moved from the borrow); a plain local keeps the direct place.
|
|
24019
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),
|
|
24020
25652
|
_ => format!("{}[{}]", esc_obj, idx_ref),
|
|
24021
25653
|
};
|
|
24022
25654
|
// #567: wrap the wrapped-Vec read in a block that pre-binds the index
|
|
@@ -24106,6 +25738,7 @@ impl Codegen {
|
|
|
24106
25738
|
let ty = match sig.ret {
|
|
24107
25739
|
VecRetKind::F64 => RustType::F64,
|
|
24108
25740
|
VecRetKind::VecF64 => RustType::Vec(Box::new(RustType::F64)),
|
|
25741
|
+
VecRetKind::Str => RustType::String,
|
|
24109
25742
|
VecRetKind::Unit => RustType::Value,
|
|
24110
25743
|
};
|
|
24111
25744
|
return Ok((code, ty));
|
|
@@ -24113,6 +25746,39 @@ impl Codegen {
|
|
|
24113
25746
|
}
|
|
24114
25747
|
}
|
|
24115
25748
|
}
|
|
25749
|
+
// A user fn whose every return is PROVEN numeric (`collect_user_fn_rets`). The call
|
|
25750
|
+
// stays boxed — this is not a calling convention — but its RESULT stops being
|
|
25751
|
+
// opaque, so the surrounding arithmetic stays integral instead of dropping to the
|
|
25752
|
+
// generic `ops::add` and its two soft-float conversions.
|
|
25753
|
+
//
|
|
25754
|
+
// The `_ =>` arm is unreachable by construction: membership required every return
|
|
25755
|
+
// to be numeric. Written out rather than `unreachable!()` so a proof bug surfaces as
|
|
25756
|
+
// the same diagnostic every other coercion in this file produces.
|
|
25757
|
+
// NOT for fns that also have an M5 native form: the direct-call path below emits
|
|
25758
|
+
// `name_native(..)` bare, and this block reaching them first wrapped that same call
|
|
25759
|
+
// in `Value::Number(..)` and match-unwrapped it straight back — the boxed round
|
|
25760
|
+
// trip the #320 push test pins against. A proof is the fallback, never the winner.
|
|
25761
|
+
if !self.user_fn_ret.is_empty() {
|
|
25762
|
+
if let Expr::Ident { name, .. } = callee.as_ref() {
|
|
25763
|
+
if self.native_fns.contains(name.as_ref()) {
|
|
25764
|
+
// fall through to the native direct-call path
|
|
25765
|
+
} else if let Some(ty) = self.user_fn_ret.get(name.as_ref()).cloned() {
|
|
25766
|
+
let boxed = self.emit_expr(expr)?;
|
|
25767
|
+
let conv = if ty == RustType::I32 {
|
|
25768
|
+
"tishlang_runtime::to_int32(*n)"
|
|
25769
|
+
} else {
|
|
25770
|
+
"*n"
|
|
25771
|
+
};
|
|
25772
|
+
return Ok((
|
|
25773
|
+
format!(
|
|
25774
|
+
"match &{} {{ Value::Number(n) => {}, _ => panic!(\"expected number\") }}",
|
|
25775
|
+
boxed, conv
|
|
25776
|
+
),
|
|
25777
|
+
ty,
|
|
25778
|
+
));
|
|
25779
|
+
}
|
|
25780
|
+
}
|
|
25781
|
+
}
|
|
24116
25782
|
// Typed extern (a `cargo:` import with a declared native signature) used INSIDE a
|
|
24117
25783
|
// native/typed expression: `entity_x(e) + 16`, `cvar(e, k) + 1`. Emit the direct
|
|
24118
25784
|
// `crate::name_typed(<coerced args>)` and report its NATIVE return type, so the
|
|
@@ -24145,16 +25811,24 @@ impl Codegen {
|
|
|
24145
25811
|
// M5: direct call to an eligible native fn -> `name_native(<native args>)`.
|
|
24146
25812
|
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
24147
25813
|
if self.native_fns.contains(fname.as_ref()) {
|
|
25814
|
+
// The ABI this callee actually has. Passing every argument through
|
|
25815
|
+
// `coerce_native_arg` — as the boxed-context site already does — is what
|
|
25816
|
+
// keeps a NATIVE argument of the other width from being handed over raw:
|
|
25817
|
+
// `manhattan(c, r, tac_unit_col(id), tac_unit_row(id))` types its extern
|
|
25818
|
+
// args I32 and the callee takes f64, which emitted `manhattan_native(.., i32)`
|
|
25819
|
+
// and rustc rejected it with E0308. Value args keep their existing
|
|
25820
|
+
// from_value_expr path (coerce_native_arg's first arm IS that path).
|
|
25821
|
+
let want = if self.native_fns_i32.contains(fname.as_ref()) {
|
|
25822
|
+
RustType::I32
|
|
25823
|
+
} else {
|
|
25824
|
+
RustType::F64
|
|
25825
|
+
};
|
|
24148
25826
|
let mut argc: Vec<String> = Vec::with_capacity(args.len());
|
|
24149
25827
|
let mut ok = true;
|
|
24150
25828
|
for a in args {
|
|
24151
25829
|
if let CallArg::Expr(e) = a {
|
|
24152
25830
|
let (ac, at) = self.emit_typed_expr(e)?;
|
|
24153
|
-
argc.push(
|
|
24154
|
-
RustType::F64.from_value_expr(&ac)
|
|
24155
|
-
} else {
|
|
24156
|
-
ac
|
|
24157
|
-
});
|
|
25831
|
+
argc.push(self.coerce_native_arg(&ac, at, want.clone()));
|
|
24158
25832
|
} else {
|
|
24159
25833
|
ok = false;
|
|
24160
25834
|
break;
|
|
@@ -24170,13 +25844,15 @@ impl Codegen {
|
|
|
24170
25844
|
RustType::F64,
|
|
24171
25845
|
));
|
|
24172
25846
|
}
|
|
25847
|
+
// …and report the ABI's OWN return type, for the same reason: an
|
|
25848
|
+
// i32-ABI callee reported as F64 mistypes every consumer downstream.
|
|
24173
25849
|
return Ok((
|
|
24174
25850
|
format!(
|
|
24175
25851
|
"{}({})",
|
|
24176
25852
|
self.native_call_target(fname.as_ref()),
|
|
24177
25853
|
argc.join(", ")
|
|
24178
25854
|
),
|
|
24179
|
-
|
|
25855
|
+
want,
|
|
24180
25856
|
));
|
|
24181
25857
|
}
|
|
24182
25858
|
}
|