@tishlang/tish-lsp 3.7.1 → 3.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tish-lsp +0 -0
- package/crates/tish_compile/src/codegen.rs +1230 -45
- package/crates/tish_compile/src/lib.rs +28 -2
- package/crates/tish_compile/src/types.rs +7 -0
- package/crates/tish_compile/tests/regr_655_gba_stack_guard.rs +41 -0
- package/crates/tish_compile/tests/regr_682_module_statics.rs +189 -0
- package/crates/tish_compile/tests/regr_682_module_statics_off.rs +53 -0
- package/crates/tish_compile/tests/regr_684_closure_write_capture.rs +104 -0
- package/crates/tish_compile/tests/regr_684_with_module_statics.rs +35 -0
- package/crates/tish_compile/tests/regr_shadowing_local_and_builtin.rs +106 -0
- package/crates/tish_lsp/Cargo.toml +1 -1
- package/crates/tish_native/src/build.rs +287 -22
- package/crates/tish_runtime_gba/src/lib.rs +51 -2
- 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
|
@@ -1122,6 +1122,24 @@ pub(crate) struct Codegen {
|
|
|
1122
1122
|
/// Variables currently wrapped in Rc<RefCell<Value>> for mutable capture in closures
|
|
1123
1123
|
/// These need special handling: reads via .borrow().clone(), writes via *var.borrow_mut()
|
|
1124
1124
|
refcell_wrapped_vars: std::collections::HashSet<String>,
|
|
1125
|
+
/// #684: module bindings whose `VmRef` cell is declared UP FRONT, at the top of `run()`, because
|
|
1126
|
+
/// a closure emitted ABOVE the `let` writes (or reads) them. The `let` then assigns into the
|
|
1127
|
+
/// pre-declared cell instead of creating one. See `collect_forward_referenced_cell_bindings`.
|
|
1128
|
+
predeclared_cell_bindings: std::collections::HashSet<String>,
|
|
1129
|
+
/// #682: is the module-statics promotion active for this compile? Decided once in
|
|
1130
|
+
/// `emit_program` from the target, the linked features and `TISH_MODULE_STATICS`.
|
|
1131
|
+
module_statics_on: bool,
|
|
1132
|
+
/// #682: top-level function names whose boxed closure lives in a module static
|
|
1133
|
+
/// (`__TISH_GF_<name>`) rather than a `run()` local. Reads resolve to the static, so nothing
|
|
1134
|
+
/// captures them.
|
|
1135
|
+
module_fn_statics: std::collections::HashSet<String>,
|
|
1136
|
+
/// #682: module-level DATA bindings whose `VmRef` cell is held by a module static
|
|
1137
|
+
/// (`__TISH_GV_<name>`) instead of being threaded into every closure as a `_cell` clone.
|
|
1138
|
+
module_var_statics: std::collections::HashSet<String>,
|
|
1139
|
+
/// #682: `(rust type, default value)` for each promoted binding's cell, recorded when its
|
|
1140
|
+
/// `let` is emitted. The accessor `fn` naming both is emitted after `run()`, so the type never
|
|
1141
|
+
/// has to be predicted ahead of inference.
|
|
1142
|
+
module_var_static_types: std::collections::HashMap<String, (String, String)>,
|
|
1125
1143
|
/// #176 (default-on via `native_opts_enabled`): top-level `let` bindings lowered to `thread_local Cell<f64>`
|
|
1126
1144
|
/// (`G_NAME`) when every use is a numeric read or a whole-binding numeric assign (fasta `seed`).
|
|
1127
1145
|
native_numeric_globals: std::collections::HashMap<String, f64>,
|
|
@@ -1166,6 +1184,12 @@ pub(crate) struct Codegen {
|
|
|
1166
1184
|
/// free `fn f_native(f64,..)->f64` (all params `: number`, returns `number`, native-safe
|
|
1167
1185
|
/// body). Direct calls to these route to the native fn, bypassing the boxed `value_call`.
|
|
1168
1186
|
native_fns: std::collections::HashSet<String>,
|
|
1187
|
+
/// Declared param types of each `native_fns` member, so a call site knows which argument slots
|
|
1188
|
+
/// are `&TishStruct_…` (passed by reference) rather than a coerced numeric.
|
|
1189
|
+
native_fn_param_types: std::collections::HashMap<String, Vec<RustType>>,
|
|
1190
|
+
/// Names ever used AS A VALUE (see [`Self::collect_value_used_expr`]). A `native_fns` member
|
|
1191
|
+
/// absent from this set needs no boxed `Value::native` wrapper in `run()`.
|
|
1192
|
+
value_used_names: std::collections::HashSet<String>,
|
|
1169
1193
|
/// Set while emitting the body of an i32-ABI native fn, so `return` coerces to `i32`.
|
|
1170
1194
|
native_fn_abi_i32: bool,
|
|
1171
1195
|
/// The subset of `native_fns` emitted with an `fn(i32, ..) -> i32` signature instead of the
|
|
@@ -1437,6 +1461,11 @@ impl Codegen {
|
|
|
1437
1461
|
outer_params_stack: Vec::new(),
|
|
1438
1462
|
outer_vars_stack: vec![Vec::new()], // Start with module-level scope
|
|
1439
1463
|
refcell_wrapped_vars: std::collections::HashSet::new(),
|
|
1464
|
+
predeclared_cell_bindings: std::collections::HashSet::new(),
|
|
1465
|
+
module_statics_on: false,
|
|
1466
|
+
module_fn_statics: std::collections::HashSet::new(),
|
|
1467
|
+
module_var_statics: std::collections::HashSet::new(),
|
|
1468
|
+
module_var_static_types: std::collections::HashMap::new(),
|
|
1440
1469
|
native_numeric_globals: std::collections::HashMap::new(),
|
|
1441
1470
|
module_const_f64_arrays: std::collections::HashMap::new(),
|
|
1442
1471
|
module_const_int_statics: std::collections::HashSet::new(),
|
|
@@ -1447,6 +1476,8 @@ impl Codegen {
|
|
|
1447
1476
|
collection_instance_locals: std::collections::HashSet::new(),
|
|
1448
1477
|
hoisted_string_chars: std::collections::HashMap::new(),
|
|
1449
1478
|
native_fns: std::collections::HashSet::new(),
|
|
1479
|
+
value_used_names: std::collections::HashSet::new(),
|
|
1480
|
+
native_fn_param_types: std::collections::HashMap::new(),
|
|
1450
1481
|
native_fns_i32: std::collections::HashSet::new(),
|
|
1451
1482
|
native_fn_abi_i32: false,
|
|
1452
1483
|
native_fn_body_emit: false,
|
|
@@ -3097,7 +3128,7 @@ impl Codegen {
|
|
|
3097
3128
|
// no_std GBA ROM: alloc-based, Arc aliases to Rc in the facade. The
|
|
3098
3129
|
// prelude `use tishlang_runtime::{…}` below is unchanged — those names
|
|
3099
3130
|
// are re-exported by the tishlang_runtime_gba facade.
|
|
3100
|
-
self.write("#![no_std]\n#![no_main]\n#![allow(unused, non_snake_case)]\n\n");
|
|
3131
|
+
self.write("#![no_std]\n#![no_main]\n#![allow(unused, non_snake_case, non_upper_case_globals)]\n\n");
|
|
3101
3132
|
self.write("extern crate alloc;\n");
|
|
3102
3133
|
self.write("use core::cell::RefCell;\n");
|
|
3103
3134
|
self.write("use alloc::rc::Rc;\n");
|
|
@@ -3109,7 +3140,7 @@ impl Codegen {
|
|
|
3109
3140
|
// f64 transcendentals for inline math (`Math.sqrt(x)` → `x.sqrt()`).
|
|
3110
3141
|
self.write("use tishlang_runtime::FloatExt;\n");
|
|
3111
3142
|
} else {
|
|
3112
|
-
self.write("#![allow(unused, non_snake_case)]\n\n");
|
|
3143
|
+
self.write("#![allow(unused, non_snake_case, non_upper_case_globals)]\n\n");
|
|
3113
3144
|
self.write("use std::cell::RefCell;\n");
|
|
3114
3145
|
self.write("use std::rc::Rc;\n");
|
|
3115
3146
|
self.write("use std::sync::Arc;\n");
|
|
@@ -3319,11 +3350,88 @@ impl Codegen {
|
|
|
3319
3350
|
let mut global_names: std::collections::HashSet<String> =
|
|
3320
3351
|
self.native_numeric_globals.keys().cloned().collect();
|
|
3321
3352
|
global_names.extend(self.module_const_f64_arrays.keys().cloned());
|
|
3353
|
+
// Publish the all-numeric externs before the proof runs, so a body calling into an
|
|
3354
|
+
// extension crate is not rejected for the call alone. `is_numeric` covers f64, i32 and
|
|
3355
|
+
// the narrow ints; anything else (string, array, struct) would need a boxing boundary
|
|
3356
|
+
// inside a lowered fn, which defeats the purpose.
|
|
3357
|
+
let numeric_externs: std::collections::HashSet<String> = self
|
|
3358
|
+
.extern_fns
|
|
3359
|
+
.iter()
|
|
3360
|
+
.filter(|(_, sigs)| {
|
|
3361
|
+
// ⚠️ NOT `is_numeric()` ALONE — that matches F64 only (types.rs), while every GBA
|
|
3362
|
+
// `declare fn f(row: i32): i32` is `I32`. Filtering on it rejected all 64 declares
|
|
3363
|
+
// in ffta-hud (probed: numeric_externs=0) and made this whole relaxation inert.
|
|
3364
|
+
// Same predicate the rest of the proof uses: f64, i32, or a narrow storage int.
|
|
3365
|
+
//
|
|
3366
|
+
// Widening this REQUIRES the f64 return coercion in `emit_native_fn_body` — an
|
|
3367
|
+
// i32 extern returned from an f64-ABI fn is otherwise emitted raw (E0308).
|
|
3368
|
+
let ok = |t: &RustType| t.is_numeric() || t.is_integer_scalar();
|
|
3369
|
+
!sigs.is_empty()
|
|
3370
|
+
&& sigs.iter().all(|s| ok(&s.ret) && s.params.iter().all(ok))
|
|
3371
|
+
})
|
|
3372
|
+
.map(|(n, _)| n.clone())
|
|
3373
|
+
.collect();
|
|
3374
|
+
if std::env::var("TISH_PROBE_EXTERNS").as_deref() == Ok("1") {
|
|
3375
|
+
let mut names: Vec<&String> = numeric_externs.iter().collect();
|
|
3376
|
+
names.sort();
|
|
3377
|
+
eprintln!(
|
|
3378
|
+
"[probe] extern_fns={} numeric_externs={} sample={:?}",
|
|
3379
|
+
self.extern_fns.len(),
|
|
3380
|
+
numeric_externs.len(),
|
|
3381
|
+
names.iter().take(8).collect::<Vec<_>>()
|
|
3382
|
+
);
|
|
3383
|
+
}
|
|
3384
|
+
Self::numeric_externs_with(|s| {
|
|
3385
|
+
s.clear();
|
|
3386
|
+
// `TISH_NO_EXTERN_LOWER=1` publishes an EMPTY set, which reverts this build to the
|
|
3387
|
+
// pre-extern-admission behaviour without rebuilding the compiler. It exists so a ROM
|
|
3388
|
+
// can be A/B'd against one binary: two 5-minute game builds instead of two toolchain
|
|
3389
|
+
// rebuilds plus two game builds, and no edit to a shared checkout between them.
|
|
3390
|
+
if std::env::var("TISH_NO_EXTERN_LOWER").as_deref() != Ok("1") {
|
|
3391
|
+
s.extend(numeric_externs.iter().cloned());
|
|
3392
|
+
}
|
|
3393
|
+
});
|
|
3322
3394
|
self.native_fns = Self::collect_native_fns(
|
|
3323
3395
|
&program.statements,
|
|
3324
3396
|
&global_names,
|
|
3325
3397
|
&native_vec_names,
|
|
3398
|
+
&self.type_aliases,
|
|
3326
3399
|
);
|
|
3400
|
+
self.native_fn_param_types = program
|
|
3401
|
+
.statements
|
|
3402
|
+
.iter()
|
|
3403
|
+
.filter_map(|st| match st {
|
|
3404
|
+
Statement::FunDecl { name, params, .. }
|
|
3405
|
+
if self.native_fns.contains(name.as_ref()) =>
|
|
3406
|
+
{
|
|
3407
|
+
let tys = params
|
|
3408
|
+
.iter()
|
|
3409
|
+
.filter_map(|p| match p {
|
|
3410
|
+
FunParam::Simple(tp) => Some(
|
|
3411
|
+
tp.type_ann
|
|
3412
|
+
.as_ref()
|
|
3413
|
+
.map(|a| {
|
|
3414
|
+
RustType::from_annotation_with_aliases(
|
|
3415
|
+
a,
|
|
3416
|
+
&self.type_aliases,
|
|
3417
|
+
)
|
|
3418
|
+
})
|
|
3419
|
+
.unwrap_or(RustType::F64),
|
|
3420
|
+
),
|
|
3421
|
+
_ => None,
|
|
3422
|
+
})
|
|
3423
|
+
.collect();
|
|
3424
|
+
Some((name.to_string(), tys))
|
|
3425
|
+
}
|
|
3426
|
+
_ => None,
|
|
3427
|
+
})
|
|
3428
|
+
.collect();
|
|
3429
|
+
// Which names are ever used as a VALUE rather than only called. Drives the
|
|
3430
|
+
// boxed-wrapper elision at `Statement::FunDecl` — see `collect_value_used_expr`.
|
|
3431
|
+
self.value_used_names.clear();
|
|
3432
|
+
for s in &program.statements {
|
|
3433
|
+
Self::collect_value_used_stmt(s, &mut self.value_used_names);
|
|
3434
|
+
}
|
|
3327
3435
|
// Which of the survivors carry the all-`i32` signature. Computed AFTER the fixpoint,
|
|
3328
3436
|
// so a function the proof rejected never reaches the i32 ABI either.
|
|
3329
3437
|
self.native_fns_i32 = program
|
|
@@ -3332,7 +3440,23 @@ impl Codegen {
|
|
|
3332
3440
|
.filter_map(|s| match s {
|
|
3333
3441
|
Statement::FunDecl { name, params, return_type, .. }
|
|
3334
3442
|
if self.native_fns.contains(name.as_ref())
|
|
3335
|
-
&& Self::fn_sig_all_i32(params, return_type)
|
|
3443
|
+
&& (Self::fn_sig_all_i32(params, return_type)
|
|
3444
|
+
// A struct-param fn: its struct slot is typed on its own, so
|
|
3445
|
+
// `fn_sig_all_i32` can never hold. Take the i32 return ABI when the
|
|
3446
|
+
// return is `: i32` and every NON-struct param is `: i32` too —
|
|
3447
|
+
// otherwise the body computes i32 while the signature says f64.
|
|
3448
|
+
// A struct param qualifies on ONE numeric field, matching the
|
|
3449
|
+
// candidacy gate. If this still demanded all-numeric, a mixed-struct
|
|
3450
|
+
// fn would lower its body to i32 arithmetic and then be handed the
|
|
3451
|
+
// f64 ABI — computing one thing and declaring another.
|
|
3452
|
+
|| (return_type.as_ref().is_some_and(Self::ann_is_i32)
|
|
3453
|
+
&& params.iter().any(|p| matches!(p, FunParam::Simple(tp)
|
|
3454
|
+
if tp.type_ann.as_ref().is_some_and(|a|
|
|
3455
|
+
!Self::struct_numeric_fields(a, &self.type_aliases).is_empty())))
|
|
3456
|
+
&& params.iter().all(|p| matches!(p, FunParam::Simple(tp)
|
|
3457
|
+
if tp.type_ann.as_ref().is_some_and(|a|
|
|
3458
|
+
Self::ann_is_i32(a)
|
|
3459
|
+
|| !Self::struct_numeric_fields(a, &self.type_aliases).is_empty()))))) =>
|
|
3336
3460
|
{
|
|
3337
3461
|
Some(name.to_string())
|
|
3338
3462
|
}
|
|
@@ -3347,9 +3471,11 @@ impl Codegen {
|
|
|
3347
3471
|
Self::collect_native_lcg_fns(&program.statements, &self.native_fns);
|
|
3348
3472
|
// #381 — fns on a call-graph cycle get guarded rotation copies (default ON;
|
|
3349
3473
|
// TISH_NATIVE_RECUR_GUARD=0 restores the plain unguarded emission).
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3474
|
+
// #655: GBA was excluded because the no_std runtime had no stack-pressure probe.
|
|
3475
|
+
// `tishlang_runtime_gba` now derives a real floor from the link map (`__iwram_end`),
|
|
3476
|
+
// and it matters MORE here than on the host: 32 KB of IWRAM, no MMU, no guard page,
|
|
3477
|
+
// so an unguarded overflow silently overwrites agb's live data instead of faulting.
|
|
3478
|
+
self.native_recur_fns = if crate::native_recur_guard_enabled() {
|
|
3353
3479
|
Self::compute_native_recur_fns(&program.statements, &self.native_fns)
|
|
3354
3480
|
} else {
|
|
3355
3481
|
std::collections::HashMap::new()
|
|
@@ -3429,6 +3555,46 @@ impl Codegen {
|
|
|
3429
3555
|
}
|
|
3430
3556
|
self.nonneg_locals = self.collect_nonneg_locals(&program.statements);
|
|
3431
3557
|
self.shift_half_of = Self::collect_shift_half_of(&program.statements);
|
|
3558
|
+
// #682 — MODULE FUNCTIONS GET A TOP-LEVEL HOME. Every top-level fn currently contributes a
|
|
3559
|
+
// `VmRef` cell, a boxed closure and one `_ref` clone per capturing sibling to `run()`'s
|
|
3560
|
+
// single frame; on a real GBA game that frame is 27–30 KB against a 32.5 KB IWRAM stack and
|
|
3561
|
+
// grows with program size. Parking the closure in a static takes all three out of the frame.
|
|
3562
|
+
//
|
|
3563
|
+
// OFF WHENEVER TISH CODE CAN RUN ON ANOTHER THREAD. The host storage is `thread_local!`
|
|
3564
|
+
// (forced by `Value`: `send-values` is off unless `http` is on, so `VmRef` is
|
|
3565
|
+
// `Rc<RefCell<_>>` and a plain `static` would not compile — the same constraint `RustLib`
|
|
3566
|
+
// documents). A `serve` handler runs on a tokio worker, which would read that worker's
|
|
3567
|
+
// *uninitialised* copy. `http`/`ws` are the only ways to get there, and the GBA — the target
|
|
3568
|
+
// this exists for — has neither.
|
|
3569
|
+
let threads_possible = self.program_may_call_tish_off_thread(program);
|
|
3570
|
+
self.module_statics_on = crate::module_statics_flag()
|
|
3571
|
+
.unwrap_or(self.emit_mode == crate::NativeEmitMode::Gba || !threads_possible);
|
|
3572
|
+
// `RustLib` publishes its exports by naming the `run()` local, so its bindings must stay
|
|
3573
|
+
// locals; the whole point there is that the frame outlives the call anyway.
|
|
3574
|
+
if self.emit_mode == crate::NativeEmitMode::RustLib {
|
|
3575
|
+
self.module_statics_on = false;
|
|
3576
|
+
}
|
|
3577
|
+
if crate::module_statics_flag().unwrap_or(true) {
|
|
3578
|
+
// The shim that keeps the builtin preamble's construction temporaries out of `run()`'s
|
|
3579
|
+
// frame. Generic over the closure's return type so the ~40 builtin types never have to
|
|
3580
|
+
// be spelled out; `#[inline(never)]` is the whole point — inlined, the temporaries come
|
|
3581
|
+
// straight back.
|
|
3582
|
+
self.writeln(
|
|
3583
|
+
"#[inline(never)] fn __tish_no_inline<T>(f: impl FnOnce() -> T) -> T { f() }",
|
|
3584
|
+
);
|
|
3585
|
+
self.writeln("");
|
|
3586
|
+
}
|
|
3587
|
+
if self.module_statics_on {
|
|
3588
|
+
self.module_fn_statics = self
|
|
3589
|
+
.prescan_function_decls(&program.statements)
|
|
3590
|
+
.into_iter()
|
|
3591
|
+
.filter(|n| {
|
|
3592
|
+
!(self.aggregate_alias.is_some() && self.aggregate_fns.contains_key(n))
|
|
3593
|
+
})
|
|
3594
|
+
.collect();
|
|
3595
|
+
self.emit_module_fn_statics()?;
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3432
3598
|
// Gba mode is sync in P2 (real async lowering is P5); `std::error::Error` in
|
|
3433
3599
|
// the signature is rewritten to `core::error::Error` by the no_std post-pass.
|
|
3434
3600
|
if self.is_async && self.emit_mode != crate::NativeEmitMode::Gba {
|
|
@@ -3440,6 +3606,29 @@ impl Codegen {
|
|
|
3440
3606
|
}
|
|
3441
3607
|
self.indent += 1;
|
|
3442
3608
|
|
|
3609
|
+
// #682 — THE BUILTIN PREAMBLE IS BUILT IN ITS OWN FRAME.
|
|
3610
|
+
//
|
|
3611
|
+
// `console`, `Math`, `JSON`, `Array`, the nine TypedArray constructors and the rest are
|
|
3612
|
+
// ~40 bindings, and each is built from a `Value::object(ObjectMap::from([ … ]))` whose pair
|
|
3613
|
+
// array is a large STACK temporary. Measured on a hello-world, that preamble alone is
|
|
3614
|
+
// 4,192 B of `run()`'s frame before a line of the program runs — 13% of the GBA's whole
|
|
3615
|
+
// stack, for a fixed set of objects that never change.
|
|
3616
|
+
//
|
|
3617
|
+
// The bindings themselves are cheap (one `Value` each); it is the construction that is not.
|
|
3618
|
+
// So the whole preamble is emitted into a closure handed to a `#[inline(never)]` shim: the
|
|
3619
|
+
// temporaries live and die in that call's frame, and `run()` receives only the values.
|
|
3620
|
+
// The shim is generic over the closure's return type, so nothing here has to spell out the
|
|
3621
|
+
// ~40 types (`document` is a `VmRef<Value>`, the rest are `Value`s).
|
|
3622
|
+
// Not gated on `module_statics_on`: a frame boundary has none of that switch's hazards —
|
|
3623
|
+
// no per-thread storage, nothing published by name — so every target gets it. The kill
|
|
3624
|
+
// switch still turns it off, so one flag restores the whole pre-#682 emission.
|
|
3625
|
+
let preamble_buffered = crate::module_statics_flag().unwrap_or(true);
|
|
3626
|
+
let saved_output = if preamble_buffered {
|
|
3627
|
+
Some(std::mem::take(&mut self.output))
|
|
3628
|
+
} else {
|
|
3629
|
+
None
|
|
3630
|
+
};
|
|
3631
|
+
|
|
3443
3632
|
// Before anything else: one namespace object per native module, shared by all of its imports.
|
|
3444
3633
|
self.emit_native_namespace_preamble();
|
|
3445
3634
|
|
|
@@ -3767,6 +3956,11 @@ impl Codegen {
|
|
|
3767
3956
|
);
|
|
3768
3957
|
}
|
|
3769
3958
|
|
|
3959
|
+
if let Some(saved) = saved_output {
|
|
3960
|
+
let preamble = std::mem::replace(&mut self.output, saved);
|
|
3961
|
+
self.emit_buffered_preamble(&preamble);
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3770
3964
|
// Polars, Egui etc. are emitted via VarDecl from import { X } from 'tish:...'
|
|
3771
3965
|
|
|
3772
3966
|
// Pre-scan for top-level function declarations and create cells (for mutual recursion)
|
|
@@ -3778,6 +3972,10 @@ impl Codegen {
|
|
|
3778
3972
|
if self.aggregate_alias.is_some() && self.aggregate_fns.contains_key(func_name) {
|
|
3779
3973
|
continue;
|
|
3780
3974
|
}
|
|
3975
|
+
// #682: promoted module fns live in `GF_NAME`, so they need no `run()` cell at all.
|
|
3976
|
+
if self.is_module_fn_static(func_name) {
|
|
3977
|
+
continue;
|
|
3978
|
+
}
|
|
3781
3979
|
let escaped = Self::escape_ident(func_name);
|
|
3782
3980
|
self.writeln(&format!(
|
|
3783
3981
|
"let {}_cell: VmRef<Value> = VmRef::new(Value::Null);",
|
|
@@ -3819,7 +4017,56 @@ impl Codegen {
|
|
|
3819
4017
|
// above it establish. A literal cannot observe or disturb anything, so moving it is not
|
|
3820
4018
|
// visible to the program.
|
|
3821
4019
|
let reordered = Self::hoist_forward_referenced_module_bindings(&program.statements);
|
|
3822
|
-
|
|
4020
|
+
let module_stmts = reordered.as_deref().unwrap_or(&program.statements);
|
|
4021
|
+
// #684 — the residue the hoist above cannot move: a module binding that is a capture cell
|
|
4022
|
+
// AND is captured by a closure emitted before its `let`. Declaring the cell here, ahead of
|
|
4023
|
+
// every statement, is what makes the capture resolvable in both directions; the `let` below
|
|
4024
|
+
// assigns into it (see the `predeclared_cell_bindings` arm in `Statement::VarDecl`).
|
|
4025
|
+
// #682 — module DATA bindings whose cell moves into a static. Computed here because it
|
|
4026
|
+
// needs `refcell_wrapped_vars`; the static ITEMS are emitted after `run()` (Rust items are
|
|
4027
|
+
// order-independent, and this is the first point where the set is known).
|
|
4028
|
+
if self.module_statics_on {
|
|
4029
|
+
self.module_var_statics =
|
|
4030
|
+
self.collect_module_var_statics(module_stmts, &self.refcell_wrapped_vars);
|
|
4031
|
+
for name in self.module_var_statics.clone() {
|
|
4032
|
+
// The handle is a `VmRef` local exactly as before, so every read/write site is
|
|
4033
|
+
// unchanged — it just comes out of the static instead of down a capture chain.
|
|
4034
|
+
self.writeln(&format!(
|
|
4035
|
+
"let {} = {};",
|
|
4036
|
+
Self::escape_ident(&name),
|
|
4037
|
+
Self::module_var_static_handle(&name)
|
|
4038
|
+
));
|
|
4039
|
+
self.rc_cell_storage_define(&name);
|
|
4040
|
+
if let Some(scope) = self.outer_vars_stack.last_mut() {
|
|
4041
|
+
scope.push(name.clone());
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
self.predeclared_cell_bindings =
|
|
4046
|
+
Self::collect_forward_referenced_cell_bindings(module_stmts, &self.refcell_wrapped_vars);
|
|
4047
|
+
// A promoted binding already has its cell up front, from the static.
|
|
4048
|
+
self.predeclared_cell_bindings
|
|
4049
|
+
.retain(|n| !self.module_var_statics.contains(n));
|
|
4050
|
+
self.predeclared_cell_bindings
|
|
4051
|
+
.retain(|n| !self.module_const_f64_arrays.contains_key(n));
|
|
4052
|
+
self.predeclared_cell_bindings
|
|
4053
|
+
.retain(|n| !self.module_const_f64_aliases.contains_key(n));
|
|
4054
|
+
let mut predeclared: Vec<String> = self.predeclared_cell_bindings.iter().cloned().collect();
|
|
4055
|
+
predeclared.sort();
|
|
4056
|
+
for name in &predeclared {
|
|
4057
|
+
self.writeln(&format!(
|
|
4058
|
+
"let {}: VmRef<Value> = VmRef::new(Value::Null);",
|
|
4059
|
+
Self::escape_ident(name)
|
|
4060
|
+
));
|
|
4061
|
+
// The cell holds a boxed `Value`: the binding's type is not known until its initialiser
|
|
4062
|
+
// is emitted, and a forward capture needs the storage to exist before then.
|
|
4063
|
+
self.type_context.define(name, RustType::Value);
|
|
4064
|
+
self.rc_cell_storage_define(name);
|
|
4065
|
+
if let Some(scope) = self.outer_vars_stack.last_mut() {
|
|
4066
|
+
scope.push(name.clone());
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
self.emit_statements_with_folds(module_stmts)?;
|
|
3823
4070
|
if self.is_async {
|
|
3824
4071
|
self.async_context_stack.pop();
|
|
3825
4072
|
}
|
|
@@ -3858,6 +4105,13 @@ impl Codegen {
|
|
|
3858
4105
|
self.writeln("Ok(())");
|
|
3859
4106
|
self.indent -= 1;
|
|
3860
4107
|
self.writeln("}");
|
|
4108
|
+
// #682 — the module-data statics. Emitted here, after `run()`, because the eligible set is
|
|
4109
|
+
// only known once `run()`'s capture prepass has run; Rust items are order-independent, so
|
|
4110
|
+
// the reads above resolve to them regardless.
|
|
4111
|
+
if self.module_statics_on {
|
|
4112
|
+
self.writeln("");
|
|
4113
|
+
self.emit_module_var_statics()?;
|
|
4114
|
+
}
|
|
3861
4115
|
if self.emit_mode == crate::NativeEmitMode::EmbeddedLib {
|
|
3862
4116
|
self.writeln("");
|
|
3863
4117
|
self.writeln("#[no_mangle]");
|
|
@@ -4234,6 +4488,10 @@ impl Codegen {
|
|
|
4234
4488
|
val_code
|
|
4235
4489
|
} else if val_ty == RustType::Value {
|
|
4236
4490
|
RustType::F64.from_value_expr(&val_code)
|
|
4491
|
+
} else if val_ty.is_integer_scalar() {
|
|
4492
|
+
// An i32-typed value into an f64-backed global. Widening is exact; emitted
|
|
4493
|
+
// raw it is E0308.
|
|
4494
|
+
format!("(({}) as f64)", val_code)
|
|
4237
4495
|
} else {
|
|
4238
4496
|
val_code
|
|
4239
4497
|
};
|
|
@@ -4350,6 +4608,17 @@ impl Codegen {
|
|
|
4350
4608
|
// `fixed` local must be lifted to `Fixed` — the RHS emitter reports numeric
|
|
4351
4609
|
// literals as F64, so without this a `fixed` reassignment mistypes (E0308).
|
|
4352
4610
|
format!("tishlang_runtime::Fixed::from_raw((({}) * 256.0) as i32)", val_code)
|
|
4611
|
+
} else if rust_type == RustType::F64 && val_ty.is_integer_scalar() {
|
|
4612
|
+
// ⚠️ AN i32 VALUE INTO AN f64 LOCAL, IN STATEMENT POSITION. The COMPARISON
|
|
4613
|
+
// emitter already widens (`n > ((MAX_UNITS) as f64)`), so
|
|
4614
|
+
//
|
|
4615
|
+
// if (n > MAX_UNITS) { n = MAX_UNITS } packages/clansave
|
|
4616
|
+
//
|
|
4617
|
+
// compiled its test and rejected its assignment: `n = MAX_UNITS;`, expected
|
|
4618
|
+
// f64 found i32. Needs a native local AND an `: i32`-annotated global on the
|
|
4619
|
+
// right, which is why it stayed hidden until a game imported clansave beside
|
|
4620
|
+
// the rest of its packages. Widening is exact.
|
|
4621
|
+
format!("(({}) as f64)", val_code)
|
|
4353
4622
|
} else {
|
|
4354
4623
|
val_code
|
|
4355
4624
|
};
|
|
@@ -6098,6 +6367,25 @@ impl Codegen {
|
|
|
6098
6367
|
rust_type = RustType::Vec(Box::new(RustType::I32));
|
|
6099
6368
|
}
|
|
6100
6369
|
|
|
6370
|
+
// #684: the cell for this module binding was declared at the top of `run()` because a
|
|
6371
|
+
// closure above the `let` captures it. Assign into that cell rather than shadowing it
|
|
6372
|
+
// with a fresh `let` — a second `let` would leave the earlier closures pointing at a
|
|
6373
|
+
// cell nothing ever writes.
|
|
6374
|
+
if self.predeclared_cell_bindings.contains(name.as_ref())
|
|
6375
|
+
&& self.outer_vars_stack.len() == 1
|
|
6376
|
+
{
|
|
6377
|
+
self.type_context.define(name.as_ref(), RustType::Value);
|
|
6378
|
+
if let Some(init_e) = init.as_ref() {
|
|
6379
|
+
let val = self.emit_expr(init_e)?;
|
|
6380
|
+
self.writeln(&format!(
|
|
6381
|
+
"*{}.borrow_mut() = ({}).clone();",
|
|
6382
|
+
Self::escape_ident(name.as_ref()),
|
|
6383
|
+
val
|
|
6384
|
+
));
|
|
6385
|
+
}
|
|
6386
|
+
return Ok(());
|
|
6387
|
+
}
|
|
6388
|
+
|
|
6101
6389
|
// #176: top-level numeric globals live in a thread_local `Cell<f64>` — no local slot.
|
|
6102
6390
|
if self.native_numeric_globals.contains_key(name.as_ref())
|
|
6103
6391
|
&& self.outer_vars_stack.len() == 1
|
|
@@ -6242,7 +6530,20 @@ impl Codegen {
|
|
|
6242
6530
|
Some(e) => self.emit_native_expr(e, &rust_type)?,
|
|
6243
6531
|
None => rust_type.default_value(),
|
|
6244
6532
|
};
|
|
6245
|
-
if self.
|
|
6533
|
+
if self.is_module_var_static(name.as_ref())
|
|
6534
|
+
&& self.outer_vars_stack.len() == 1
|
|
6535
|
+
{
|
|
6536
|
+
// #682: the cell is the static's; the `let` writes THROUGH the handle
|
|
6537
|
+
// already bound at the top of `run()`. Its type is recorded here — the
|
|
6538
|
+
// point where inference has finally settled it — for the accessor emitted
|
|
6539
|
+
// after `run()`.
|
|
6540
|
+
self.module_var_static_types.insert(
|
|
6541
|
+
name.to_string(),
|
|
6542
|
+
(rust_type.to_rust_type_str(), rust_type.default_value()),
|
|
6543
|
+
);
|
|
6544
|
+
self.writeln(&format!("*{}.borrow_mut() = {};", escaped_name, expr_str));
|
|
6545
|
+
self.rc_cell_storage_define(name.as_ref());
|
|
6546
|
+
} else if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
6246
6547
|
// Closure-mutated: same Rc<RefCell<T>> pattern as Value (assignments use borrow_mut)
|
|
6247
6548
|
self.writeln(&format!("let {} = VmRef::new({});", escaped_name, expr_str));
|
|
6248
6549
|
self.rc_cell_storage_define(name.as_ref());
|
|
@@ -6266,12 +6567,23 @@ impl Codegen {
|
|
|
6266
6567
|
None => ("Value::Null".to_string(), false),
|
|
6267
6568
|
};
|
|
6268
6569
|
// Vars that are mutated by nested closures must be RefCell from the start
|
|
6269
|
-
if
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6570
|
+
let init_val = if clone_needed {
|
|
6571
|
+
format!("({}).clone()", expr_str)
|
|
6572
|
+
} else {
|
|
6573
|
+
expr_str.to_string()
|
|
6574
|
+
};
|
|
6575
|
+
if self.is_module_var_static(name.as_ref())
|
|
6576
|
+
&& self.outer_vars_stack.len() == 1
|
|
6577
|
+
{
|
|
6578
|
+
// #682 — see the native branch above: the cell belongs to the static, and
|
|
6579
|
+
// the `let` writes through the handle bound at the top of `run()`.
|
|
6580
|
+
self.module_var_static_types.insert(
|
|
6581
|
+
name.to_string(),
|
|
6582
|
+
("Value".to_string(), "Value::Null".to_string()),
|
|
6583
|
+
);
|
|
6584
|
+
self.writeln(&format!("*{}.borrow_mut() = {};", escaped_name, init_val));
|
|
6585
|
+
self.rc_cell_storage_define(name.as_ref());
|
|
6586
|
+
} else if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
6275
6587
|
self.writeln(&format!("let {} = VmRef::new({});", escaped_name, init_val));
|
|
6276
6588
|
self.rc_cell_storage_define(name.as_ref());
|
|
6277
6589
|
} else if clone_needed {
|
|
@@ -7037,6 +7349,34 @@ impl Codegen {
|
|
|
7037
7349
|
{
|
|
7038
7350
|
return Ok(());
|
|
7039
7351
|
}
|
|
7352
|
+
// Same idea, wider: this fn already has a native lowering (`<name>_native`) that
|
|
7353
|
+
// every direct call routes to, and nothing in the program uses the NAME as a value
|
|
7354
|
+
// — so the boxed `Value::native` wrapper is dead weight. It is not free weight: the
|
|
7355
|
+
// wrapper and its cell are permanent slots in the enclosing frame, and for module
|
|
7356
|
+
// fns that frame is `run()`, which on GBA holds every one of them at once against a
|
|
7357
|
+
// 29 KB IWRAM stack. Skipping it is what makes "type your functions" actually pay.
|
|
7358
|
+
// ⚠️ AND ONLY WHEN EVERY CALL SITE CAN ACTUALLY BE DIRECT. A fn taking a STRUCT
|
|
7359
|
+
// still falls back to `value_call` when the argument is a plain boxed local --
|
|
7360
|
+
// `area(r)` in tests/core/struct_return does exactly that while `area(makeRect(..))`
|
|
7361
|
+
// lowers, so one call site needed the wrapper the other made look dead (E0425).
|
|
7362
|
+
// Restricting the elision to all-scalar parameter lists keeps the win for the
|
|
7363
|
+
// numeric fns it was written for and stops guessing about the rest.
|
|
7364
|
+
let all_scalar_params = self
|
|
7365
|
+
.native_fn_param_types
|
|
7366
|
+
.get(name.as_ref())
|
|
7367
|
+
.map(|ps| {
|
|
7368
|
+
ps.iter().all(|p| {
|
|
7369
|
+
matches!(p, RustType::F64 | RustType::I32 | RustType::Bool)
|
|
7370
|
+
|| p.is_narrow_int()
|
|
7371
|
+
})
|
|
7372
|
+
})
|
|
7373
|
+
.unwrap_or(false);
|
|
7374
|
+
if self.native_fns.contains(name.as_ref())
|
|
7375
|
+
&& !self.value_used_names.contains(name.as_ref())
|
|
7376
|
+
&& all_scalar_params
|
|
7377
|
+
{
|
|
7378
|
+
return Ok(());
|
|
7379
|
+
}
|
|
7040
7380
|
// Use Rc<RefCell<>> pattern to allow recursive function calls
|
|
7041
7381
|
// The function can reference itself through the cell
|
|
7042
7382
|
let name_raw = name.as_ref();
|
|
@@ -7083,6 +7423,10 @@ impl Codegen {
|
|
|
7083
7423
|
&& !param_names.contains(name)
|
|
7084
7424
|
&& !local_var_names.contains(name)
|
|
7085
7425
|
})
|
|
7426
|
+
// #682: a promoted module binding is reachable from its static at any depth,
|
|
7427
|
+
// so it is not captured — which is what removes the `_cell` clone this closure
|
|
7428
|
+
// would otherwise add to the ENCLOSING frame (for a module fn, `run()`'s).
|
|
7429
|
+
.filter(|name| !self.is_module_var_static(name))
|
|
7086
7430
|
.filter(|name| {
|
|
7087
7431
|
![
|
|
7088
7432
|
"Boolean",
|
|
@@ -7144,7 +7488,17 @@ impl Codegen {
|
|
|
7144
7488
|
}
|
|
7145
7489
|
}
|
|
7146
7490
|
|
|
7147
|
-
|
|
7491
|
+
// #682: a promoted module fn is BUILT INTO ITS STATIC, so neither the closure nor
|
|
7492
|
+
// a `let` for it occupies a slot in `run()`'s frame.
|
|
7493
|
+
let to_static = self.is_module_fn_static(name_raw);
|
|
7494
|
+
if to_static {
|
|
7495
|
+
self.writeln(&format!(
|
|
7496
|
+
"{}.with(|c| *c.borrow_mut() = {{",
|
|
7497
|
+
Self::module_fn_static_name(name_raw)
|
|
7498
|
+
));
|
|
7499
|
+
} else {
|
|
7500
|
+
self.writeln(&format!("let {} = {{", name_str));
|
|
7501
|
+
}
|
|
7148
7502
|
self.indent += 1;
|
|
7149
7503
|
// Clone RefCell for outer vars so closure can capture
|
|
7150
7504
|
for outer_var in &outer_vars {
|
|
@@ -7155,7 +7509,9 @@ impl Codegen {
|
|
|
7155
7509
|
));
|
|
7156
7510
|
}
|
|
7157
7511
|
// Clone the cell so the closure can reference the function recursively
|
|
7158
|
-
|
|
7512
|
+
// A promoted fn reaches itself through its static; no `_ref` clone, and so no
|
|
7513
|
+
// extra slot in the enclosing frame either.
|
|
7514
|
+
let needs_self_ref = referenced.contains(name_raw) && !to_static;
|
|
7159
7515
|
if needs_self_ref {
|
|
7160
7516
|
self.writeln(&format!(
|
|
7161
7517
|
"let {}_ref = {}_cell.clone();",
|
|
@@ -7169,7 +7525,12 @@ impl Codegen {
|
|
|
7169
7525
|
.map(|scope| {
|
|
7170
7526
|
scope
|
|
7171
7527
|
.iter()
|
|
7172
|
-
.filter(|s|
|
|
7528
|
+
.filter(|s| {
|
|
7529
|
+
s.as_str() != name_raw
|
|
7530
|
+
&& referenced.contains(s.as_str())
|
|
7531
|
+
// #682: a sibling in a static is read from the static.
|
|
7532
|
+
&& !self.is_module_fn_static(s.as_str())
|
|
7533
|
+
})
|
|
7173
7534
|
.cloned()
|
|
7174
7535
|
.collect()
|
|
7175
7536
|
})
|
|
@@ -7205,6 +7566,7 @@ impl Codegen {
|
|
|
7205
7566
|
for f in scope {
|
|
7206
7567
|
if f.as_str() != name_raw
|
|
7207
7568
|
&& referenced.contains(f.as_str())
|
|
7569
|
+
&& !self.is_module_fn_static(f.as_str())
|
|
7208
7570
|
&& !sibling_fns.iter().any(|s| s == f)
|
|
7209
7571
|
&& !outer_vars.iter().any(|v| v == f)
|
|
7210
7572
|
&& !outer_params.iter().any(|p| p == f)
|
|
@@ -7279,7 +7641,10 @@ impl Codegen {
|
|
|
7279
7641
|
"NaN",
|
|
7280
7642
|
"serve",
|
|
7281
7643
|
] {
|
|
7282
|
-
|
|
7644
|
+
// A user `function serve(...)` (etc.) SHADOWS the builtin: it lowers to a
|
|
7645
|
+
// `__TISH_GF_*` static, so there is no `serve` local to clone here and the
|
|
7646
|
+
// emitted prelude would be E0425. The call sites already read the static.
|
|
7647
|
+
if referenced.contains(*builtin) && !self.is_module_fn_static(builtin) {
|
|
7283
7648
|
self.writeln(&format!("let {} = {}.clone();", builtin, builtin));
|
|
7284
7649
|
}
|
|
7285
7650
|
}
|
|
@@ -7320,6 +7685,29 @@ impl Codegen {
|
|
|
7320
7685
|
if crate::native_recur_guard_enabled() {
|
|
7321
7686
|
self.writeln("let Some(_tish_depth) = tishlang_runtime::enter_call_guarded() else { return Value::Null; };");
|
|
7322
7687
|
}
|
|
7688
|
+
// #682: a promoted module binding is fetched straight from its static. One local
|
|
7689
|
+
// per referencing closure, and none at all in the frames in between — where the
|
|
7690
|
+
// `_cell` chain used to leave one at every level.
|
|
7691
|
+
if self.module_statics_on {
|
|
7692
|
+
let mut promoted: Vec<String> = self
|
|
7693
|
+
.module_var_statics
|
|
7694
|
+
.iter()
|
|
7695
|
+
.filter(|n| {
|
|
7696
|
+
referenced.contains(n.as_str())
|
|
7697
|
+
&& !param_names.contains(n.as_str())
|
|
7698
|
+
&& !local_var_names.contains(n.as_str())
|
|
7699
|
+
})
|
|
7700
|
+
.cloned()
|
|
7701
|
+
.collect();
|
|
7702
|
+
promoted.sort();
|
|
7703
|
+
for n in &promoted {
|
|
7704
|
+
self.writeln(&format!(
|
|
7705
|
+
"let {} = {};",
|
|
7706
|
+
Self::escape_ident(n),
|
|
7707
|
+
Self::module_var_static_handle(n)
|
|
7708
|
+
));
|
|
7709
|
+
}
|
|
7710
|
+
}
|
|
7323
7711
|
// Mutable outer vars: capture the RefCell so assignments use borrow_mut
|
|
7324
7712
|
for outer_var in &mutable_outer_vars {
|
|
7325
7713
|
let var_escaped = Self::escape_ident(outer_var);
|
|
@@ -7623,12 +8011,16 @@ impl Codegen {
|
|
|
7623
8011
|
self.value_fn_depth = self.value_fn_depth.saturating_sub(1);
|
|
7624
8012
|
self.try_closure_depth = saved_try_depth;
|
|
7625
8013
|
self.indent -= 1;
|
|
7626
|
-
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
8014
|
+
if to_static {
|
|
8015
|
+
self.writeln("});");
|
|
8016
|
+
} else {
|
|
8017
|
+
self.writeln("};");
|
|
8018
|
+
// Update the cell with the actual function value
|
|
8019
|
+
self.writeln(&format!(
|
|
8020
|
+
"*{}_cell.borrow_mut() = {}.clone();",
|
|
8021
|
+
name_str, name_str
|
|
8022
|
+
));
|
|
8023
|
+
}
|
|
7632
8024
|
}
|
|
7633
8025
|
}
|
|
7634
8026
|
Ok(())
|
|
@@ -8220,6 +8612,16 @@ impl Codegen {
|
|
|
8220
8612
|
if let Some(uv) = self.usize_var_subst.get(name.as_ref()) {
|
|
8221
8613
|
return Ok(format!("Value::Number(({} as f64))", uv));
|
|
8222
8614
|
}
|
|
8615
|
+
// #682: a module fn lives in `GF_NAME`, not in a local — and unlike a capture, the
|
|
8616
|
+
// static is in scope at every depth, so nothing has to be threaded in to reach it.
|
|
8617
|
+
// ⚠️ Unless a PARAM or LOCAL of the enclosing function shadows the name: earshot's
|
|
8618
|
+
// `let dist: i32` lost to packages/sfx.tish's internal `function dist(...)` here,
|
|
8619
|
+
// and the callee got a function cell where it expected a number.
|
|
8620
|
+
if self.is_module_fn_static(name.as_ref())
|
|
8621
|
+
&& !self.type_context.is_locally_shadowed(name.as_ref())
|
|
8622
|
+
{
|
|
8623
|
+
return Ok(Self::module_fn_static_read(name.as_ref()));
|
|
8624
|
+
}
|
|
8223
8625
|
let escaped = Self::escape_ident(name.as_ref());
|
|
8224
8626
|
if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
8225
8627
|
let var_type = self.type_context.get_type(name.as_ref());
|
|
@@ -9391,12 +9793,28 @@ impl Codegen {
|
|
|
9391
9793
|
let i32_abi = self.native_fns_i32.contains(fname.as_ref());
|
|
9392
9794
|
let want =
|
|
9393
9795
|
if i32_abi { RustType::I32 } else { RustType::F64 };
|
|
9796
|
+
let ptys = self.native_fn_param_types.get(fname.as_ref()).cloned();
|
|
9394
9797
|
let mut argc: Vec<String> = Vec::with_capacity(args.len());
|
|
9395
9798
|
let mut ok = true;
|
|
9396
|
-
for a in args {
|
|
9799
|
+
for (i, a) in args.iter().enumerate() {
|
|
9397
9800
|
if let CallArg::Expr(e) = a {
|
|
9801
|
+
// A struct slot takes a shared reference to the caller's struct;
|
|
9802
|
+
// coercing it to a number would be nonsense (and would not compile).
|
|
9803
|
+
let is_struct = matches!(
|
|
9804
|
+
ptys.as_ref().and_then(|v| v.get(i)),
|
|
9805
|
+
Some(RustType::Named { .. })
|
|
9806
|
+
);
|
|
9398
9807
|
let (ac, at) = self.emit_typed_expr(e)?;
|
|
9399
|
-
|
|
9808
|
+
if is_struct {
|
|
9809
|
+
if matches!(at, RustType::Named { .. }) {
|
|
9810
|
+
argc.push(format!("&{}", ac));
|
|
9811
|
+
} else {
|
|
9812
|
+
ok = false;
|
|
9813
|
+
break;
|
|
9814
|
+
}
|
|
9815
|
+
} else {
|
|
9816
|
+
argc.push(self.coerce_native_arg(&ac, at, want.clone()));
|
|
9817
|
+
}
|
|
9400
9818
|
} else {
|
|
9401
9819
|
ok = false;
|
|
9402
9820
|
break;
|
|
@@ -9835,6 +10253,10 @@ impl Codegen {
|
|
|
9835
10253
|
val_code
|
|
9836
10254
|
} else if val_ty == RustType::Value {
|
|
9837
10255
|
RustType::F64.from_value_expr(&val_code)
|
|
10256
|
+
} else if val_ty.is_integer_scalar() {
|
|
10257
|
+
// An i32-typed value into an f64-backed global. Widening is exact; emitted
|
|
10258
|
+
// raw it is E0308.
|
|
10259
|
+
format!("(({}) as f64)", val_code)
|
|
9838
10260
|
} else {
|
|
9839
10261
|
val_code
|
|
9840
10262
|
};
|
|
@@ -9857,6 +10279,12 @@ impl Codegen {
|
|
|
9857
10279
|
val_code
|
|
9858
10280
|
} else if val_ty == RustType::Value {
|
|
9859
10281
|
rust_type.from_value_expr(&val_code)
|
|
10282
|
+
} else if rust_type == RustType::F64 && val_ty.is_integer_scalar() {
|
|
10283
|
+
// ⚠️ An `: i32` global assigned into an f64-typed local. `let n = roster.len()`
|
|
10284
|
+
// infers f64, and `n = MAX_UNITS` (a `let MAX_UNITS: i32` in packages/clansave)
|
|
10285
|
+
// emitted `n = MAX_UNITS;` raw -- E0308, expected f64 found i32. Needs BOTH a
|
|
10286
|
+
// native local and an i32-annotated global, which is why so little hit it.
|
|
10287
|
+
format!("(({}) as f64)", val_code)
|
|
9860
10288
|
} else {
|
|
9861
10289
|
val_code
|
|
9862
10290
|
};
|
|
@@ -10367,6 +10795,126 @@ impl Codegen {
|
|
|
10367
10795
|
idents
|
|
10368
10796
|
}
|
|
10369
10797
|
|
|
10798
|
+
/// Names used AS A VALUE — i.e. everywhere except the callee slot of a direct `f(..)` call.
|
|
10799
|
+
/// A module fn that is only ever called directly needs no boxed `Value::native` wrapper: the
|
|
10800
|
+
/// native lowering (`f_native`) already serves every call site, and the wrapper is a permanent
|
|
10801
|
+
/// slot in `run()`'s stack frame. On GBA `run()` holds every module fn at once — 605 of them in
|
|
10802
|
+
/// `examples/ffta` — against a 29 KB IWRAM stack, so dropping the unused wrappers is the
|
|
10803
|
+
/// difference between a game fitting and overwriting the audio mixer.
|
|
10804
|
+
///
|
|
10805
|
+
/// ⚠️ CONSERVATIVE BY CONSTRUCTION. Anything this does not structurally recurse through falls
|
|
10806
|
+
/// back to [`Self::collect_expr_idents`], which marks every name inside as value-used — that
|
|
10807
|
+
/// only forfeits the optimisation, it never removes a wrapper something needs. And if it ever
|
|
10808
|
+
/// were wrong, the generated Rust names a binding that does not exist: a loud `E0425` at build
|
|
10809
|
+
/// time, not a silently wrong ROM.
|
|
10810
|
+
fn collect_value_used_expr(expr: &Expr, idents: &mut HashSet<String>) {
|
|
10811
|
+
match expr {
|
|
10812
|
+
Expr::Call { callee, args, .. } => {
|
|
10813
|
+
// The callee slot of a direct call is NOT a value use -- UNLESS the call cannot be
|
|
10814
|
+
// emitted as a direct one. A SPREAD call always goes through `value_call`, which
|
|
10815
|
+
// needs the boxed binding by name, so eliding the wrapper leaves `let _callee =
|
|
10816
|
+
// (sum3).clone()` referring to nothing (E0425, tests/core/spread).
|
|
10817
|
+
let spreads = args.iter().any(|a| matches!(a, CallArg::Spread(_)));
|
|
10818
|
+
if !matches!(&**callee, Expr::Ident { .. }) || spreads {
|
|
10819
|
+
Self::collect_value_used_expr(callee, idents);
|
|
10820
|
+
}
|
|
10821
|
+
for arg in args {
|
|
10822
|
+
match arg {
|
|
10823
|
+
CallArg::Expr(e) | CallArg::Spread(e) => {
|
|
10824
|
+
Self::collect_value_used_expr(e, idents)
|
|
10825
|
+
}
|
|
10826
|
+
}
|
|
10827
|
+
}
|
|
10828
|
+
}
|
|
10829
|
+
Expr::Binary { left, right, .. } => {
|
|
10830
|
+
Self::collect_value_used_expr(left, idents);
|
|
10831
|
+
Self::collect_value_used_expr(right, idents);
|
|
10832
|
+
}
|
|
10833
|
+
Expr::Unary { operand, .. } => Self::collect_value_used_expr(operand, idents),
|
|
10834
|
+
Expr::Conditional {
|
|
10835
|
+
cond,
|
|
10836
|
+
then_branch,
|
|
10837
|
+
else_branch,
|
|
10838
|
+
..
|
|
10839
|
+
} => {
|
|
10840
|
+
Self::collect_value_used_expr(cond, idents);
|
|
10841
|
+
Self::collect_value_used_expr(then_branch, idents);
|
|
10842
|
+
Self::collect_value_used_expr(else_branch, idents);
|
|
10843
|
+
}
|
|
10844
|
+
Expr::Member { object, prop, .. } => {
|
|
10845
|
+
Self::collect_value_used_expr(object, idents);
|
|
10846
|
+
if let MemberProp::Expr(e) = prop {
|
|
10847
|
+
Self::collect_value_used_expr(e, idents);
|
|
10848
|
+
}
|
|
10849
|
+
}
|
|
10850
|
+
Expr::Index { object, index, .. } => {
|
|
10851
|
+
Self::collect_value_used_expr(object, idents);
|
|
10852
|
+
Self::collect_value_used_expr(index, idents);
|
|
10853
|
+
}
|
|
10854
|
+
other => Self::collect_expr_idents(other, idents),
|
|
10855
|
+
}
|
|
10856
|
+
}
|
|
10857
|
+
|
|
10858
|
+
/// Statement-level driver for [`Self::collect_value_used_expr`]. Mirrors the shape of
|
|
10859
|
+
/// [`Self::collect_stmt_idents`]; unknown statement forms fall back to it (conservative).
|
|
10860
|
+
fn collect_value_used_stmt(stmt: &Statement, idents: &mut HashSet<String>) {
|
|
10861
|
+
match stmt {
|
|
10862
|
+
Statement::ExprStmt { expr, .. } => Self::collect_value_used_expr(expr, idents),
|
|
10863
|
+
Statement::VarDecl { init, .. } => {
|
|
10864
|
+
if let Some(e) = init {
|
|
10865
|
+
Self::collect_value_used_expr(e, idents);
|
|
10866
|
+
}
|
|
10867
|
+
}
|
|
10868
|
+
Statement::Return { value, .. } => {
|
|
10869
|
+
if let Some(e) = value {
|
|
10870
|
+
Self::collect_value_used_expr(e, idents);
|
|
10871
|
+
}
|
|
10872
|
+
}
|
|
10873
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
10874
|
+
for s in statements {
|
|
10875
|
+
Self::collect_value_used_stmt(s, idents);
|
|
10876
|
+
}
|
|
10877
|
+
}
|
|
10878
|
+
Statement::If {
|
|
10879
|
+
cond,
|
|
10880
|
+
then_branch,
|
|
10881
|
+
else_branch,
|
|
10882
|
+
..
|
|
10883
|
+
} => {
|
|
10884
|
+
Self::collect_value_used_expr(cond, idents);
|
|
10885
|
+
Self::collect_value_used_stmt(then_branch, idents);
|
|
10886
|
+
if let Some(s) = else_branch {
|
|
10887
|
+
Self::collect_value_used_stmt(s, idents);
|
|
10888
|
+
}
|
|
10889
|
+
}
|
|
10890
|
+
Statement::While { cond, body, .. } => {
|
|
10891
|
+
Self::collect_value_used_expr(cond, idents);
|
|
10892
|
+
Self::collect_value_used_stmt(body, idents);
|
|
10893
|
+
}
|
|
10894
|
+
Statement::FunDecl { body, .. } => Self::collect_value_used_stmt(body, idents),
|
|
10895
|
+
// ⚠️ An EXPORTED fn must keep its boxed wrapper: the module namespace hands it out as
|
|
10896
|
+
// a `Value`, and an importer may do anything with it. Mark the exported name itself as
|
|
10897
|
+
// value-used, then walk the declaration normally.
|
|
10898
|
+
Statement::Export { declaration, .. } => match &**declaration {
|
|
10899
|
+
tishlang_ast::ExportDeclaration::Named(inner) => {
|
|
10900
|
+
if let Statement::FunDecl { name, .. } = &**inner {
|
|
10901
|
+
idents.insert(name.to_string());
|
|
10902
|
+
}
|
|
10903
|
+
Self::collect_value_used_stmt(inner, idents);
|
|
10904
|
+
}
|
|
10905
|
+
tishlang_ast::ExportDeclaration::Default(e) => Self::collect_value_used_expr(e, idents),
|
|
10906
|
+
tishlang_ast::ExportDeclaration::ReExport { specifiers, .. } => {
|
|
10907
|
+
for sp in specifiers {
|
|
10908
|
+
if let tishlang_ast::ImportSpecifier::Named { name, .. } = sp {
|
|
10909
|
+
idents.insert(name.to_string());
|
|
10910
|
+
}
|
|
10911
|
+
}
|
|
10912
|
+
}
|
|
10913
|
+
},
|
|
10914
|
+
other => Self::collect_stmt_idents(other, idents),
|
|
10915
|
+
}
|
|
10916
|
+
}
|
|
10917
|
+
|
|
10370
10918
|
fn collect_expr_idents(expr: &Expr, idents: &mut HashSet<String>) {
|
|
10371
10919
|
match expr {
|
|
10372
10920
|
Expr::Ident { name, .. } => {
|
|
@@ -12024,6 +12572,8 @@ impl Codegen {
|
|
|
12024
12572
|
]
|
|
12025
12573
|
.contains(&name.as_str())
|
|
12026
12574
|
&& !self.native_numeric_globals.contains_key(name.as_str())
|
|
12575
|
+
// #682: reachable from its static at any depth — nothing to capture.
|
|
12576
|
+
&& !self.is_module_var_static(name.as_str())
|
|
12027
12577
|
})
|
|
12028
12578
|
.collect();
|
|
12029
12579
|
|
|
@@ -12114,7 +12664,9 @@ impl Codegen {
|
|
|
12114
12664
|
"RegExp",
|
|
12115
12665
|
"Polars",
|
|
12116
12666
|
] {
|
|
12117
|
-
|
|
12667
|
+
// Same shadowing rule as the value-fn prelude: a user module function of this name
|
|
12668
|
+
// lowers to a `__TISH_GF_*` static, so no builtin local exists to clone.
|
|
12669
|
+
if referenced.contains(*builtin) && !self.is_module_fn_static(builtin) {
|
|
12118
12670
|
code.push_str(&format!(" let {} = {}.clone();\n", builtin, builtin));
|
|
12119
12671
|
}
|
|
12120
12672
|
}
|
|
@@ -12126,7 +12678,15 @@ impl Codegen {
|
|
|
12126
12678
|
.map(|scope| {
|
|
12127
12679
|
scope
|
|
12128
12680
|
.iter()
|
|
12129
|
-
.filter(|f|
|
|
12681
|
+
.filter(|f| {
|
|
12682
|
+
referenced.contains(f.as_str())
|
|
12683
|
+
&& !param_names.contains(*f)
|
|
12684
|
+
// #682: a promoted module fn has no `_cell` anywhere — it lives in
|
|
12685
|
+
// `__TISH_GF_<name>` and the body reads it from there. Cloning a cell
|
|
12686
|
+
// that was never emitted is `error[E0425]: cannot find value
|
|
12687
|
+
// bagList_cell`, and it takes the whole ROM down.
|
|
12688
|
+
&& !self.is_module_fn_static(f.as_str())
|
|
12689
|
+
})
|
|
12130
12690
|
.cloned()
|
|
12131
12691
|
.collect()
|
|
12132
12692
|
})
|
|
@@ -12181,6 +12741,12 @@ impl Codegen {
|
|
|
12181
12741
|
// arrow whose grandparent-scope var was TLS-lowered). Same exclusion the
|
|
12182
12742
|
// FunDecl closure path and this fn's own outer_vars filter already apply.
|
|
12183
12743
|
&& !self.native_numeric_globals.contains_key(name.as_str())
|
|
12744
|
+
// #682: same reason — a promoted module binding has no boxed local to
|
|
12745
|
+
// capture; the body reads it out of its static. Module FNS as well as data:
|
|
12746
|
+
// `let present_ref = VmRef::new(present.clone())` against a fn that now lives
|
|
12747
|
+
// in `__TISH_GF_present` is `error[E0425]: cannot find value present`.
|
|
12748
|
+
&& !self.is_module_var_static(name.as_str())
|
|
12749
|
+
&& !self.is_module_fn_static(name.as_str())
|
|
12184
12750
|
})
|
|
12185
12751
|
.cloned()
|
|
12186
12752
|
.collect()
|
|
@@ -12202,6 +12768,26 @@ impl Codegen {
|
|
|
12202
12768
|
let saved_try_depth = self.try_closure_depth;
|
|
12203
12769
|
self.try_closure_depth = 0;
|
|
12204
12770
|
|
|
12771
|
+
// #682: same handle fetch as the FunDecl path — see the comment there.
|
|
12772
|
+
if self.module_statics_on {
|
|
12773
|
+
let mut promoted: Vec<&String> = self
|
|
12774
|
+
.module_var_statics
|
|
12775
|
+
.iter()
|
|
12776
|
+
.filter(|n| {
|
|
12777
|
+
referenced.contains(n.as_str())
|
|
12778
|
+
&& !param_names.contains(n.as_str())
|
|
12779
|
+
&& !local_var_names.contains(n.as_str())
|
|
12780
|
+
})
|
|
12781
|
+
.collect();
|
|
12782
|
+
promoted.sort();
|
|
12783
|
+
for n in promoted {
|
|
12784
|
+
code.push_str(&format!(
|
|
12785
|
+
" let {} = {};\n",
|
|
12786
|
+
Self::escape_ident(n),
|
|
12787
|
+
Self::module_var_static_handle(n)
|
|
12788
|
+
));
|
|
12789
|
+
}
|
|
12790
|
+
}
|
|
12205
12791
|
// Make captured outer params available as plain Values (from _ref RefCells)
|
|
12206
12792
|
for outer_param in &outer_params {
|
|
12207
12793
|
let param_escaped = Self::escape_ident(outer_param);
|
|
@@ -16828,6 +17414,295 @@ impl Codegen {
|
|
|
16828
17414
|
format!("G_{}", safe.to_uppercase())
|
|
16829
17415
|
}
|
|
16830
17416
|
|
|
17417
|
+
/// #682 — does this program hand a tish closure to something that will call it on ANOTHER
|
|
17418
|
+
/// thread? Those are the surfaces the host's `thread_local!` storage cannot serve: the worker
|
|
17419
|
+
/// would read its own uninitialised copy of every module fn.
|
|
17420
|
+
///
|
|
17421
|
+
/// Keyed on what the program NAMES, not on which capabilities are linked — `tish build` links
|
|
17422
|
+
/// the whole capability set by default, so a feature test would switch this off for every
|
|
17423
|
+
/// desktop program including the ones that never open a socket. Conservative in the safe
|
|
17424
|
+
/// direction: a program that merely mentions one of these names keeps the old emission.
|
|
17425
|
+
fn program_may_call_tish_off_thread(&self, program: &Program) -> bool {
|
|
17426
|
+
let mut idents = HashSet::new();
|
|
17427
|
+
for s in &program.statements {
|
|
17428
|
+
Self::collect_stmt_idents(s, &mut idents);
|
|
17429
|
+
}
|
|
17430
|
+
// `serve(port, handler)` dispatches the handler on tokio workers; `serve(port, {onWorker})`
|
|
17431
|
+
// builds one per accept thread. `Server`/`WebSocket`/`wsAccept` are the ws equivalents.
|
|
17432
|
+
for n in ["serve", "Server", "WebSocket", "wsAccept"] {
|
|
17433
|
+
if idents.contains(n) {
|
|
17434
|
+
return true;
|
|
17435
|
+
}
|
|
17436
|
+
}
|
|
17437
|
+
// `Promise.spawn` is an OS-thread spawn, and only exists in ws-linked programs. The member
|
|
17438
|
+
// name is not an ident, so the receiver is what we can see.
|
|
17439
|
+
if self.has_feature("ws") && idents.contains("Promise") {
|
|
17440
|
+
return true;
|
|
17441
|
+
}
|
|
17442
|
+
false
|
|
17443
|
+
}
|
|
17444
|
+
|
|
17445
|
+
/// #682 — re-emit a buffered builtin preamble inside the `#[inline(never)]` shim, handing its
|
|
17446
|
+
/// bindings back as a tuple. See the call site for why.
|
|
17447
|
+
///
|
|
17448
|
+
/// The names come from the buffer itself rather than a hand-kept list: half the preamble is
|
|
17449
|
+
/// feature-gated (`fs`, `http`, `timers`, JSX…), so what was actually emitted is the only
|
|
17450
|
+
/// reliable answer. Only declarations at the preamble's own indent are taken — the `let`s
|
|
17451
|
+
/// nested inside a `Value::object({ … })` block are that block's business.
|
|
17452
|
+
/// Re-emit the buffered builtin preamble inside the `#[inline(never)]` shim, handing its
|
|
17453
|
+
/// base-indent bindings back as a tuple. The names are read out of the emitted text rather than
|
|
17454
|
+
/// kept in a list beside it: half the preamble is feature-gated (`fs`, `http`, `timers`, JSX…),
|
|
17455
|
+
/// so what was actually emitted is the only reliable answer. The preamble is a flat run of
|
|
17456
|
+
/// `writeln`s, so an indent test identifies its own declarations exactly — a chunk of user code
|
|
17457
|
+
/// is not, which is why `chunk_escaping_names` works off the AST instead.
|
|
17458
|
+
fn emit_buffered_preamble(&mut self, preamble: &str) {
|
|
17459
|
+
let base = " ".repeat(self.indent);
|
|
17460
|
+
let mut names: Vec<(bool, String)> = Vec::new();
|
|
17461
|
+
for line in preamble.lines() {
|
|
17462
|
+
let Some(rest) = line.strip_prefix(&base) else {
|
|
17463
|
+
continue;
|
|
17464
|
+
};
|
|
17465
|
+
if rest.starts_with(' ') {
|
|
17466
|
+
continue;
|
|
17467
|
+
}
|
|
17468
|
+
// `emit_native_namespace_preamble` writes `#[allow(unused_variables)] let __tish_native_ns_0 = …`.
|
|
17469
|
+
// Without stripping the attribute the scan does not see a `let` at all, the binding stays
|
|
17470
|
+
// inside the closure, and every chunk that imports from a native module fails with
|
|
17471
|
+
// `error[E0425]: cannot find value __tish_native_ns_0`.
|
|
17472
|
+
let rest = match rest.find("] ") {
|
|
17473
|
+
Some(i) if rest.starts_with("#[") => &rest[i + 2..],
|
|
17474
|
+
_ => rest,
|
|
17475
|
+
};
|
|
17476
|
+
let Some(rest) = rest.strip_prefix("let ") else {
|
|
17477
|
+
continue;
|
|
17478
|
+
};
|
|
17479
|
+
let (is_mut, rest) = match rest.strip_prefix("mut ") {
|
|
17480
|
+
Some(r) => (true, r),
|
|
17481
|
+
None => (false, rest),
|
|
17482
|
+
};
|
|
17483
|
+
let name: String = rest
|
|
17484
|
+
.chars()
|
|
17485
|
+
.take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
|
|
17486
|
+
.collect();
|
|
17487
|
+
// `let x: String = …` counts — the annotation is dropped on the way through the
|
|
17488
|
+
// tuple and inference puts it back. A destructuring `let (a, b) = …` does not: its name
|
|
17489
|
+
// parse comes back empty, and anything else unexpected is skipped rather than guessed
|
|
17490
|
+
// at. (Missing one is not silent: the binding stays inside the closure and the next
|
|
17491
|
+
// reference to it fails to compile.)
|
|
17492
|
+
let after = rest[name.len()..].trim_start();
|
|
17493
|
+
if name.is_empty() || !(after.starts_with('=') || after.starts_with(':')) {
|
|
17494
|
+
continue;
|
|
17495
|
+
}
|
|
17496
|
+
if names.iter().any(|(_, n)| *n == name) {
|
|
17497
|
+
continue;
|
|
17498
|
+
}
|
|
17499
|
+
names.push((is_mut, name));
|
|
17500
|
+
}
|
|
17501
|
+
let binds = names
|
|
17502
|
+
.iter()
|
|
17503
|
+
.map(|(m, n)| if *m { format!("mut {n}") } else { n.clone() })
|
|
17504
|
+
.collect::<Vec<_>>()
|
|
17505
|
+
.join(", ");
|
|
17506
|
+
let vals = names
|
|
17507
|
+
.iter()
|
|
17508
|
+
.map(|(_, n)| n.clone())
|
|
17509
|
+
.collect::<Vec<_>>()
|
|
17510
|
+
.join(", ");
|
|
17511
|
+
self.writeln(&format!("let ({binds}) = __tish_no_inline(|| {{"));
|
|
17512
|
+
self.output.push_str(preamble);
|
|
17513
|
+
self.indent += 1;
|
|
17514
|
+
self.writeln(&format!("({vals})"));
|
|
17515
|
+
self.indent -= 1;
|
|
17516
|
+
self.writeln("});");
|
|
17517
|
+
}
|
|
17518
|
+
|
|
17519
|
+
/// #682 — static name for a module-scope FUNCTION binding (`heroAnim` → `__TISH_GF_heroAnim`).
|
|
17520
|
+
///
|
|
17521
|
+
/// CASE-PRESERVING, unlike `global_static_name`'s `G_*`: tish identifiers are case-sensitive, so
|
|
17522
|
+
/// upper-casing them collides two distinct bindings onto one static. `fl` and `Fl` in
|
|
17523
|
+
/// tests/core did exactly that — `error[E0428]: the name GV_FL is defined multiple times`. The
|
|
17524
|
+
/// `__TISH_` prefix is reserved, so a user binding cannot shadow one of these either.
|
|
17525
|
+
fn module_fn_static_name(name: &str) -> String {
|
|
17526
|
+
format!("__TISH_GF_{}", Self::static_ident_body(name))
|
|
17527
|
+
}
|
|
17528
|
+
|
|
17529
|
+
/// The token-safe body of a generated static's name: `escape_ident` may hand back a raw
|
|
17530
|
+
/// identifier (`r#fn` for the keyword `fn`), and `#` does not tokenize inside a longer name.
|
|
17531
|
+
fn static_ident_body(name: &str) -> String {
|
|
17532
|
+
let esc = Self::escape_ident(name);
|
|
17533
|
+
let raw = esc.strip_prefix("r#").unwrap_or(&esc);
|
|
17534
|
+
raw.chars()
|
|
17535
|
+
.map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' })
|
|
17536
|
+
.collect()
|
|
17537
|
+
}
|
|
17538
|
+
|
|
17539
|
+
/// Read a module fn out of its static. Emitted INSIDE closure bodies as well as at top level:
|
|
17540
|
+
/// the read happens when the expression runs, by which time every module fn is installed.
|
|
17541
|
+
fn module_fn_static_read(name: &str) -> String {
|
|
17542
|
+
format!(
|
|
17543
|
+
"{}.with(|c| (*c.borrow()).clone())",
|
|
17544
|
+
Self::module_fn_static_name(name)
|
|
17545
|
+
)
|
|
17546
|
+
}
|
|
17547
|
+
|
|
17548
|
+
/// #682 — which module DATA bindings can move their cell into a static.
|
|
17549
|
+
///
|
|
17550
|
+
/// Restricted to bindings that are ALREADY a `VmRef` cell (`wrapped`), because those are the
|
|
17551
|
+
/// ones a closure capture threads down through every enclosing frame — a plain module local
|
|
17552
|
+
/// costs one slot in `run()` and nothing further.
|
|
17553
|
+
///
|
|
17554
|
+
/// Excluded, in order: a name declared more than once at module level (the second `let` is a
|
|
17555
|
+
/// different binding); a name bound anywhere else in the program — a parameter, a local `let`,
|
|
17556
|
+
/// a `for…of` or `catch` binding — since the static read site has no scope information to tell
|
|
17557
|
+
/// the two apart; and any name another emission path already claims, either with its own static
|
|
17558
|
+
/// (#176 numeric globals, module const arrays) or with a declaration that never reaches the
|
|
17559
|
+
/// write-through branch below (an i32 loop var).
|
|
17560
|
+
///
|
|
17561
|
+
/// Note what is NOT a criterion: the binding's TYPE. The static and its accessor are emitted
|
|
17562
|
+
/// after `run()`, from the type recorded when the `let` was emitted, so nothing here has to
|
|
17563
|
+
/// predict what inference will decide.
|
|
17564
|
+
fn collect_module_var_statics(
|
|
17565
|
+
&self,
|
|
17566
|
+
stmts: &[Statement],
|
|
17567
|
+
wrapped: &HashSet<String>,
|
|
17568
|
+
) -> HashSet<String> {
|
|
17569
|
+
let mut decl_counts: HashMap<String, usize> = HashMap::new();
|
|
17570
|
+
let mut candidates: Vec<String> = Vec::new();
|
|
17571
|
+
for st in stmts {
|
|
17572
|
+
if let Statement::VarDecl { name, .. } = st {
|
|
17573
|
+
*decl_counts.entry(name.to_string()).or_insert(0) += 1;
|
|
17574
|
+
candidates.push(name.to_string());
|
|
17575
|
+
}
|
|
17576
|
+
}
|
|
17577
|
+
let mut shadow: HashSet<String> = HashSet::new();
|
|
17578
|
+
for st in stmts {
|
|
17579
|
+
Self::collect_binding_names(st, &mut shadow);
|
|
17580
|
+
// A module-level `let x` IS the declaration, not a shadow of it — every other `let`
|
|
17581
|
+
// in the program is. (`collect_binding_names` above already covers the parameter,
|
|
17582
|
+
// `for…of` and `catch` bindings an initialiser's arrow can introduce.)
|
|
17583
|
+
if !matches!(st, Statement::VarDecl { .. }) {
|
|
17584
|
+
Self::collect_local_var_names(st, &mut shadow);
|
|
17585
|
+
}
|
|
17586
|
+
}
|
|
17587
|
+
let mut out = HashSet::new();
|
|
17588
|
+
for name in candidates {
|
|
17589
|
+
if !wrapped.contains(&name)
|
|
17590
|
+
|| decl_counts.get(&name).copied().unwrap_or(0) > 1
|
|
17591
|
+
|| shadow.contains(&name)
|
|
17592
|
+
|| self.native_numeric_globals.contains_key(&name)
|
|
17593
|
+
|| self.module_const_f64_arrays.contains_key(&name)
|
|
17594
|
+
|| self.module_const_f64_aliases.contains_key(&name)
|
|
17595
|
+
|| self.i32_loop_vars.contains(&name)
|
|
17596
|
+
|| self.module_fn_statics.contains(&name)
|
|
17597
|
+
{
|
|
17598
|
+
continue;
|
|
17599
|
+
}
|
|
17600
|
+
out.insert(name);
|
|
17601
|
+
}
|
|
17602
|
+
out
|
|
17603
|
+
}
|
|
17604
|
+
|
|
17605
|
+
/// #682 — static name for a module-scope DATA binding (`deadCol` → `__TISH_GV_deadCol`).
|
|
17606
|
+
/// Case-preserving for the reason `module_fn_static_name` documents.
|
|
17607
|
+
fn module_var_static_name(name: &str) -> String {
|
|
17608
|
+
format!("__TISH_GV_{}", Self::static_ident_body(name))
|
|
17609
|
+
}
|
|
17610
|
+
|
|
17611
|
+
/// Clone the binding's `VmRef` handle out of its static.
|
|
17612
|
+
///
|
|
17613
|
+
/// The static holds the CELL, not the value, so every existing read/write site keeps working
|
|
17614
|
+
/// verbatim — `(*x.borrow())`, `*x.borrow_mut() = …`, `vm_read(&x)` all still see a `VmRef`
|
|
17615
|
+
/// local. What changes is where that local comes from: a closure now takes it straight from the
|
|
17616
|
+
/// static instead of having it threaded in through one `_cell` clone per enclosing scope, and
|
|
17617
|
+
/// each of those clones was a slot in `run()`'s frame.
|
|
17618
|
+
///
|
|
17619
|
+
/// `OnceCell` rather than a plain value because `VmRef::new` is not const, and the handle must
|
|
17620
|
+
/// exist before the binding's `let` runs — a closure created above it captures the same cell.
|
|
17621
|
+
fn module_var_static_handle(name: &str) -> String {
|
|
17622
|
+
format!("{}()", Self::module_var_accessor_name(name))
|
|
17623
|
+
}
|
|
17624
|
+
|
|
17625
|
+
/// The accessor that hands out the cell. Every use site calls this and never names the cell's
|
|
17626
|
+
/// type, so the type is free to be recorded when the binding's `let` is emitted rather than
|
|
17627
|
+
/// predicted before inference has run.
|
|
17628
|
+
fn module_var_accessor_name(name: &str) -> String {
|
|
17629
|
+
format!("__tish_gv_{}", Self::static_ident_body(name))
|
|
17630
|
+
}
|
|
17631
|
+
|
|
17632
|
+
fn is_module_var_static(&self, name: &str) -> bool {
|
|
17633
|
+
self.module_statics_on && self.module_var_statics.contains(name)
|
|
17634
|
+
}
|
|
17635
|
+
|
|
17636
|
+
fn emit_module_var_statics(&mut self) -> Result<(), CompileError> {
|
|
17637
|
+
if self.module_var_statics.is_empty() {
|
|
17638
|
+
return Ok(());
|
|
17639
|
+
}
|
|
17640
|
+
let mut names: Vec<String> = self.module_var_statics.iter().cloned().collect();
|
|
17641
|
+
names.sort();
|
|
17642
|
+
let gba = self.emit_mode == crate::NativeEmitMode::Gba;
|
|
17643
|
+
for n in &names {
|
|
17644
|
+
let (ty, default) = self
|
|
17645
|
+
.module_var_static_types
|
|
17646
|
+
.get(n)
|
|
17647
|
+
.cloned()
|
|
17648
|
+
.unwrap_or_else(|| ("Value".to_string(), "Value::Null".to_string()));
|
|
17649
|
+
let stat = Self::module_var_static_name(n);
|
|
17650
|
+
if gba {
|
|
17651
|
+
self.writeln(&format!(
|
|
17652
|
+
"static {stat}: tishlang_runtime::SingleCore<core::cell::OnceCell<VmRef<{ty}>>> = \
|
|
17653
|
+
tishlang_runtime::SingleCore::new(core::cell::OnceCell::new());"
|
|
17654
|
+
));
|
|
17655
|
+
} else {
|
|
17656
|
+
self.writeln(&format!(
|
|
17657
|
+
"thread_local! {{ static {stat}: std::cell::OnceCell<VmRef<{ty}>> = const {{ std::cell::OnceCell::new() }}; }}"
|
|
17658
|
+
));
|
|
17659
|
+
}
|
|
17660
|
+
self.writeln(&format!(
|
|
17661
|
+
"#[inline] fn {}() -> VmRef<{ty}> {{ {stat}.with(|c| c.get_or_init(|| VmRef::new({default})).clone()) }}",
|
|
17662
|
+
Self::module_var_accessor_name(n)
|
|
17663
|
+
));
|
|
17664
|
+
}
|
|
17665
|
+
self.writeln("");
|
|
17666
|
+
Ok(())
|
|
17667
|
+
}
|
|
17668
|
+
|
|
17669
|
+
fn is_module_fn_static(&self, name: &str) -> bool {
|
|
17670
|
+
self.module_statics_on && self.module_fn_statics.contains(name)
|
|
17671
|
+
}
|
|
17672
|
+
|
|
17673
|
+
/// Declare one static per promoted module fn, before `run()`. `SingleCore` on GBA (no_std has
|
|
17674
|
+
/// no `thread_local!`), `thread_local!` on the host — the same split #594 made for the numeric
|
|
17675
|
+
/// globals, and for the same reason: `with()` mirrors `LocalKey::with`, so every read site is
|
|
17676
|
+
/// written once and works on both.
|
|
17677
|
+
fn emit_module_fn_statics(&mut self) -> Result<(), CompileError> {
|
|
17678
|
+
if self.module_fn_statics.is_empty() {
|
|
17679
|
+
return Ok(());
|
|
17680
|
+
}
|
|
17681
|
+
let mut names: Vec<String> = self.module_fn_statics.iter().cloned().collect();
|
|
17682
|
+
names.sort();
|
|
17683
|
+
if self.emit_mode == crate::NativeEmitMode::Gba {
|
|
17684
|
+
for n in &names {
|
|
17685
|
+
self.writeln(&format!(
|
|
17686
|
+
"static {}: tishlang_runtime::SingleCore<core::cell::RefCell<Value>> = \
|
|
17687
|
+
tishlang_runtime::SingleCore::new(core::cell::RefCell::new(Value::Null));",
|
|
17688
|
+
Self::module_fn_static_name(n)
|
|
17689
|
+
));
|
|
17690
|
+
}
|
|
17691
|
+
} else {
|
|
17692
|
+
// ONE `thread_local!` PER STATIC, not one block listing them all: the macro recurses
|
|
17693
|
+
// once per item, and a real program has hundreds of module fns — a single block blows
|
|
17694
|
+
// rustc's default `recursion_limit = 128` before it ever type-checks.
|
|
17695
|
+
for n in &names {
|
|
17696
|
+
self.writeln(&format!(
|
|
17697
|
+
"thread_local! {{ static {}: std::cell::RefCell<Value> = const {{ std::cell::RefCell::new(Value::Null) }}; }}",
|
|
17698
|
+
Self::module_fn_static_name(n)
|
|
17699
|
+
));
|
|
17700
|
+
}
|
|
17701
|
+
}
|
|
17702
|
+
self.writeln("");
|
|
17703
|
+
Ok(())
|
|
17704
|
+
}
|
|
17705
|
+
|
|
16831
17706
|
fn native_global_static(name: &str) -> String {
|
|
16832
17707
|
Self::global_static_name(name)
|
|
16833
17708
|
}
|
|
@@ -17451,6 +18326,76 @@ impl Codegen {
|
|
|
17451
18326
|
Some(out)
|
|
17452
18327
|
}
|
|
17453
18328
|
|
|
18329
|
+
/// #684 — MODULE BINDINGS A CLOSURE ABOVE THEM TOUCHES, THAT THE HOIST CANNOT REACH.
|
|
18330
|
+
///
|
|
18331
|
+
/// `hoist_forward_referenced_module_bindings` moves the declaration up, but only for a LITERAL
|
|
18332
|
+
/// initialiser referenced from a top-level `FunDecl` — deliberately, since moving a call would
|
|
18333
|
+
/// move its side effects. Everything else it leaves in place, and for a binding that is also a
|
|
18334
|
+
/// capture cell (`refcell_wrapped_vars`) the result does not compile:
|
|
18335
|
+
///
|
|
18336
|
+
/// ```text
|
|
18337
|
+
/// let mk = [() => { function noteDeath(c) { deadCol = c } … }] // *deadCol.borrow_mut() = …
|
|
18338
|
+
/// export let deadCol: i32 = -1 // let deadCol = VmRef::new(..)
|
|
18339
|
+
/// ```
|
|
18340
|
+
///
|
|
18341
|
+
/// The write emits `*deadCol.borrow_mut()` because `refcell_wrapped_vars` — a whole-program
|
|
18342
|
+
/// prepass — says the binding is a cell, while the capture list comes from `outer_vars_stack`,
|
|
18343
|
+
/// which is built as statements are emitted and does not have the name yet. Nothing is captured,
|
|
18344
|
+
/// nothing is in scope: `error[E0425]: cannot find value 'deadCol' in this scope`.
|
|
18345
|
+
///
|
|
18346
|
+
/// The fix is not to move the declaration but to split it: declare the CELL up front (holding
|
|
18347
|
+
/// `Value::Null`, which is what the binding's value is before its `let` runs anyway) and let the
|
|
18348
|
+
/// `let` assign into it. Every closure — above or below — then captures the same cell, and the
|
|
18349
|
+
/// initialiser stays exactly where the program put it.
|
|
18350
|
+
///
|
|
18351
|
+
/// Only names declared EXACTLY ONCE at module level qualify: with two declarations a mention
|
|
18352
|
+
/// above the second one refers to the first, and merging them into one cell would change which
|
|
18353
|
+
/// value it sees (the same hazard the hoist documents).
|
|
18354
|
+
fn collect_forward_referenced_cell_bindings(
|
|
18355
|
+
stmts: &[Statement],
|
|
18356
|
+
wrapped: &HashSet<String>,
|
|
18357
|
+
) -> HashSet<String> {
|
|
18358
|
+
let mut decl_index: HashMap<String, usize> = HashMap::new();
|
|
18359
|
+
let mut decl_counts: HashMap<String, usize> = HashMap::new();
|
|
18360
|
+
for (i, s) in stmts.iter().enumerate() {
|
|
18361
|
+
if let Statement::VarDecl { name, .. } = s {
|
|
18362
|
+
*decl_counts.entry(name.to_string()).or_insert(0) += 1;
|
|
18363
|
+
decl_index.entry(name.to_string()).or_insert(i);
|
|
18364
|
+
}
|
|
18365
|
+
}
|
|
18366
|
+
let mut out = HashSet::new();
|
|
18367
|
+
for (name, di) in &decl_index {
|
|
18368
|
+
if !wrapped.contains(name.as_str()) {
|
|
18369
|
+
continue;
|
|
18370
|
+
}
|
|
18371
|
+
if decl_counts.get(name.as_str()).copied().unwrap_or(0) > 1 {
|
|
18372
|
+
continue;
|
|
18373
|
+
}
|
|
18374
|
+
if stmts
|
|
18375
|
+
.iter()
|
|
18376
|
+
.take(*di)
|
|
18377
|
+
.any(|s| Self::stmt_closure_captures_ident(s, name))
|
|
18378
|
+
{
|
|
18379
|
+
out.insert(name.clone());
|
|
18380
|
+
}
|
|
18381
|
+
}
|
|
18382
|
+
out
|
|
18383
|
+
}
|
|
18384
|
+
|
|
18385
|
+
/// Does a CLOSURE inside this statement capture `name`? Straight-line code above a declaration
|
|
18386
|
+
/// is deliberately not counted — there a mention is a genuine use-before-init, not a legal
|
|
18387
|
+
/// forward reference, and must keep failing rather than silently reading `Value::Null`.
|
|
18388
|
+
///
|
|
18389
|
+
/// Reuses the same walker `collect_vars_needing_capture_cell` uses to decide cell-ness in the
|
|
18390
|
+
/// first place, so the two answers cannot drift apart.
|
|
18391
|
+
fn stmt_closure_captures_ident(stmt: &Statement, name: &str) -> bool {
|
|
18392
|
+
let mut block_vars = HashSet::new();
|
|
18393
|
+
block_vars.insert(name.to_string());
|
|
18394
|
+
let mut captured = HashSet::new();
|
|
18395
|
+
Self::collect_captured_block_vars_from_statements(stmt, &block_vars, &mut captured);
|
|
18396
|
+
captured.contains(name)
|
|
18397
|
+
}
|
|
18398
|
+
|
|
17454
18399
|
/// An initialiser that cannot observe or disturb program state, so moving it is invisible.
|
|
17455
18400
|
/// Deliberately narrow — array/object literals are included only when every element is itself
|
|
17456
18401
|
/// literal, so no call or property read comes along for the ride.
|
|
@@ -19222,6 +20167,7 @@ impl Codegen {
|
|
|
19222
20167
|
statements: &[Statement],
|
|
19223
20168
|
globals: &std::collections::HashSet<String>,
|
|
19224
20169
|
native_vec_names: &std::collections::HashSet<String>,
|
|
20170
|
+
aliases: &std::collections::HashMap<String, RustType>,
|
|
19225
20171
|
) -> std::collections::HashSet<String> {
|
|
19226
20172
|
use std::collections::HashSet;
|
|
19227
20173
|
let mut cand: HashSet<String> = HashSet::new();
|
|
@@ -19240,7 +20186,14 @@ impl Codegen {
|
|
|
19240
20186
|
let params_ok = params.iter().all(|p| {
|
|
19241
20187
|
matches!(p, FunParam::Simple(tp)
|
|
19242
20188
|
if tp.default.is_none()
|
|
19243
|
-
&& tp.type_ann.as_ref().map_or(true,
|
|
20189
|
+
&& tp.type_ann.as_ref().map_or(true, |a| {
|
|
20190
|
+
Self::ann_is_number(a)
|
|
20191
|
+
// At least one numeric field is enough to be a CANDIDATE. The
|
|
20192
|
+
// fixpoint below still has to prove the body, and a read of a
|
|
20193
|
+
// non-numeric field fails that proof — so admitting a mixed
|
|
20194
|
+
// record here cannot lower anything that was not already sound.
|
|
20195
|
+
|| !Self::struct_numeric_fields(a, aliases).is_empty()
|
|
20196
|
+
}))
|
|
19244
20197
|
});
|
|
19245
20198
|
// Return: an annotated `: number`, OR unannotated with all-numeric returns
|
|
19246
20199
|
// (verified in the fixpoint via `returns_numeric`), so the native `-> f64` holds.
|
|
@@ -19253,7 +20206,19 @@ impl Codegen {
|
|
|
19253
20206
|
// still has to prove the body native-safe and every return numeric — the annotation
|
|
19254
20207
|
// never decides that on its own (see `liar` in the issue).
|
|
19255
20208
|
let i32_ok = Self::fn_sig_all_i32(params, return_type);
|
|
19256
|
-
|
|
20209
|
+
// A struct-param fn can never satisfy `fn_sig_all_i32` (its struct slot is not an
|
|
20210
|
+
// `i32`), so an `: i32` return would reject it even though the return is perfectly
|
|
20211
|
+
// native. Accept an i32 return for those; the f64 ABI carries it, and the existing
|
|
20212
|
+
// all-i32 fast path is untouched for everyone else.
|
|
20213
|
+
let has_struct_param = params.iter().any(|p| {
|
|
20214
|
+
matches!(p, FunParam::Simple(tp)
|
|
20215
|
+
if tp.type_ann.as_ref()
|
|
20216
|
+
.is_some_and(|a| !Self::struct_numeric_fields(a, aliases).is_empty()))
|
|
20217
|
+
});
|
|
20218
|
+
let ret_ok = ret_ok
|
|
20219
|
+
|| i32_ok
|
|
20220
|
+
|| (has_struct_param && return_type.as_ref().is_some_and(Self::ann_is_i32));
|
|
20221
|
+
let params_ok = params_ok || i32_ok;
|
|
19257
20222
|
// #320: 0-param numeric fns (e.g. k_nucleotide's `nextBase()` — mutates a numeric
|
|
19258
20223
|
// global, returns number) are eligible too; the fixpoint below still proves the body
|
|
19259
20224
|
// native-safe + all-numeric-returns, so `fn name_native() -> f64` is sound.
|
|
@@ -19307,6 +20272,25 @@ impl Codegen {
|
|
|
19307
20272
|
}
|
|
19308
20273
|
let pnames: HashSet<String> =
|
|
19309
20274
|
params.iter().flat_map(|p| p.bound_names()).map(|n| n.to_string()).collect();
|
|
20275
|
+
// Each struct param maps to the fields that can lower — its NUMERIC ones. A param
|
|
20276
|
+
// with no numeric field is left out entirely, so it reads exactly as "not a struct
|
|
20277
|
+
// param" and nothing about it is native.
|
|
20278
|
+
let sparams: std::collections::HashMap<String, HashSet<String>> = params
|
|
20279
|
+
.iter()
|
|
20280
|
+
.filter_map(|p| match p {
|
|
20281
|
+
FunParam::Simple(tp) => tp.type_ann.as_ref().and_then(|a| {
|
|
20282
|
+
let fs = Self::struct_numeric_fields(a, aliases);
|
|
20283
|
+
(!fs.is_empty()).then(|| (tp.name.to_string(), fs))
|
|
20284
|
+
}),
|
|
20285
|
+
_ => None,
|
|
20286
|
+
})
|
|
20287
|
+
.collect();
|
|
20288
|
+
Self::struct_params_with(|m| {
|
|
20289
|
+
m.clear();
|
|
20290
|
+
for (k, v) in sparams.iter() {
|
|
20291
|
+
m.insert(k.clone(), v.clone());
|
|
20292
|
+
}
|
|
20293
|
+
});
|
|
19310
20294
|
let nums = Self::native_fn_numeric_locals(
|
|
19311
20295
|
body,
|
|
19312
20296
|
&pnames,
|
|
@@ -19314,9 +20298,20 @@ impl Codegen {
|
|
|
19314
20298
|
&cand,
|
|
19315
20299
|
native_vec_names,
|
|
19316
20300
|
);
|
|
19317
|
-
|
|
19318
|
-
|
|
19319
|
-
|
|
20301
|
+
let safe = Self::native_safe_stmt(body, &pnames, &cand, globals, &nums, native_vec_names);
|
|
20302
|
+
let rets = Self::returns_numeric(body, &pnames, &cand, globals, &nums);
|
|
20303
|
+
let ok = safe && rets;
|
|
20304
|
+
if !ok && std::env::var("TISH_PROBE_LOWERING").as_deref() == Ok("1") {
|
|
20305
|
+
// Which HALF rejected it. "body not native-safe" and "a return is not numeric"
|
|
20306
|
+
// are entirely different fixes, and the fixpoint silently collapsed both into a
|
|
20307
|
+
// candidate quietly disappearing.
|
|
20308
|
+
eprintln!(
|
|
20309
|
+
"[probe] {} rejected: body_safe={} returns_numeric={}",
|
|
20310
|
+
name, safe, rets
|
|
20311
|
+
);
|
|
20312
|
+
}
|
|
20313
|
+
Self::struct_params_with(|s| s.clear());
|
|
20314
|
+
if !ok {
|
|
19320
20315
|
remove.push(name.to_string());
|
|
19321
20316
|
}
|
|
19322
20317
|
}
|
|
@@ -19512,6 +20507,114 @@ impl Codegen {
|
|
|
19512
20507
|
}
|
|
19513
20508
|
|
|
19514
20509
|
/// Numeric locals in an M5-eligible fn body (params + fixpoint literal/copy seeds).
|
|
20510
|
+
/// The current function's params that are annotated with a type alias whose fields are ALL
|
|
20511
|
+
/// numeric, so `p.field` is a machine-word load. Set for the duration of one function's
|
|
20512
|
+
/// native-safety proof (`collect_native_fns`) and read by the proof's leaf predicates.
|
|
20513
|
+
///
|
|
20514
|
+
/// A side-channel rather than a threaded argument on purpose: the proof is five mutually
|
|
20515
|
+
/// recursive static helpers, and widening all of their signatures to carry a set that only two
|
|
20516
|
+
/// leaves consult is a large diff for no added safety. Codegen is single-threaded; the set is
|
|
20517
|
+
/// cleared after each function.
|
|
20518
|
+
///
|
|
20519
|
+
/// ⚠️ THE FIELD SET IS WHAT MAKES THIS SOUND, AND IT REPLACED A BARE NAME SET FOR THAT REASON.
|
|
20520
|
+
/// While the map held only names, admitting a struct with a `string` field would have made
|
|
20521
|
+
/// `p.name` "numeric" here and emitted Rust that does not compile — so the gate demanded that
|
|
20522
|
+
/// EVERY field be numeric, and one string disqualified the whole function. Carrying the numeric
|
|
20523
|
+
/// fields per param removes that ceiling: `p.x` proves native and `p.name` does not, so a mixed
|
|
20524
|
+
/// record lowers its arithmetic and a function that touches the string simply fails the proof
|
|
20525
|
+
/// and stays boxed. Never widen this to a bare name test again.
|
|
20526
|
+
fn struct_params_with<R>(
|
|
20527
|
+
f: impl FnOnce(&mut std::collections::HashMap<String, HashSet<String>>) -> R,
|
|
20528
|
+
) -> R {
|
|
20529
|
+
thread_local! {
|
|
20530
|
+
static STRUCT_PARAMS:
|
|
20531
|
+
std::cell::RefCell<std::collections::HashMap<String, HashSet<String>>> =
|
|
20532
|
+
std::cell::RefCell::new(std::collections::HashMap::new());
|
|
20533
|
+
}
|
|
20534
|
+
STRUCT_PARAMS.with(|c| f(&mut c.borrow_mut()))
|
|
20535
|
+
}
|
|
20536
|
+
|
|
20537
|
+
/// Declared externs whose signature is numbers in, a number out.
|
|
20538
|
+
///
|
|
20539
|
+
/// A `declare fn` already resolves to a native Rust fn — the emitter calls
|
|
20540
|
+
/// `<crate>::<symbol>_typed(..)` directly (see [`Self::extern_sig_for_arity`]). But the proof
|
|
20541
|
+
/// only accepted calls to `cand` (the tish fns being lowered), so ANY function calling into an
|
|
20542
|
+
/// extension crate was rejected and stayed boxed. On a game that is nearly everything: measured
|
|
20543
|
+
/// on `examples/ffta-hud`, a fn doing `u.col * 100 + u.row` lowered while the one beside it
|
|
20544
|
+
/// calling `tac_add_unit(u.col, u.row, …)` did not, purely because of the call.
|
|
20545
|
+
///
|
|
20546
|
+
/// Only ALL-NUMERIC signatures qualify. An extern taking or returning a string, an array or a
|
|
20547
|
+
/// struct is not callable from a lowered body without a boxing boundary, which is the thing
|
|
20548
|
+
/// being removed.
|
|
20549
|
+
///
|
|
20550
|
+
/// A thread-local for the same reason [`Self::struct_params_with`] is one: the proof is a set of
|
|
20551
|
+
/// associated fns with no `&self`, and threading a new parameter through every one of them would
|
|
20552
|
+
/// touch far more code than the fact being communicated.
|
|
20553
|
+
fn numeric_externs_with<R>(f: impl FnOnce(&mut HashSet<String>) -> R) -> R {
|
|
20554
|
+
thread_local! {
|
|
20555
|
+
static NUMERIC_EXTERNS: std::cell::RefCell<HashSet<String>> =
|
|
20556
|
+
std::cell::RefCell::new(HashSet::new());
|
|
20557
|
+
}
|
|
20558
|
+
NUMERIC_EXTERNS.with(|c| f(&mut c.borrow_mut()))
|
|
20559
|
+
}
|
|
20560
|
+
|
|
20561
|
+
/// `p.field` where `p` is one of [`Self::struct_params_with`]'s params AND `field` is one of its
|
|
20562
|
+
/// NUMERIC fields. Both halves matter: the name alone would admit `p.name` on a mixed record.
|
|
20563
|
+
fn is_struct_param_field(e: &Expr) -> bool {
|
|
20564
|
+
let Expr::Member { object, prop, .. } = e else {
|
|
20565
|
+
return false;
|
|
20566
|
+
};
|
|
20567
|
+
let MemberProp::Name { name: fname, .. } = prop else {
|
|
20568
|
+
return false;
|
|
20569
|
+
};
|
|
20570
|
+
let Expr::Ident { name, .. } = object.as_ref() else {
|
|
20571
|
+
return false;
|
|
20572
|
+
};
|
|
20573
|
+
Self::struct_params_with(|m| {
|
|
20574
|
+
m.get(name.as_ref()).is_some_and(|fs| fs.contains(fname.as_ref()))
|
|
20575
|
+
})
|
|
20576
|
+
}
|
|
20577
|
+
|
|
20578
|
+
/// Every field of this annotation's aliased struct is a native NUMBER (the invariant above).
|
|
20579
|
+
fn ann_is_all_numeric_struct(
|
|
20580
|
+
ann: &TypeAnnotation,
|
|
20581
|
+
aliases: &std::collections::HashMap<String, RustType>,
|
|
20582
|
+
) -> bool {
|
|
20583
|
+
matches!(
|
|
20584
|
+
RustType::from_annotation_with_aliases(ann, aliases),
|
|
20585
|
+
RustType::Named { ref fields, .. }
|
|
20586
|
+
if !fields.is_empty()
|
|
20587
|
+
&& fields.iter().all(|(_, t)| {
|
|
20588
|
+
matches!(t, RustType::F64 | RustType::I32) || t.is_narrow_int()
|
|
20589
|
+
})
|
|
20590
|
+
)
|
|
20591
|
+
}
|
|
20592
|
+
|
|
20593
|
+
/// The NUMERIC fields of this annotation's aliased struct, by name.
|
|
20594
|
+
///
|
|
20595
|
+
/// [`Self::ann_is_all_numeric_struct`] asks a whole-struct question, and that is the ceiling on
|
|
20596
|
+
/// what can lower: half of a game's record shapes carry a string, a null or an array beside
|
|
20597
|
+
/// their numbers, and one such field disqualifies the whole function even when it only ever
|
|
20598
|
+
/// touches the numeric ones. Answering per FIELD lets `m.x` lower while `m.name` does not — a
|
|
20599
|
+
/// function that reads the string fails the proof exactly as it does today, so nothing that
|
|
20600
|
+
/// lowers now stops lowering.
|
|
20601
|
+
///
|
|
20602
|
+
/// An empty set means nothing here can lower: not a struct, or a struct with no numeric field.
|
|
20603
|
+
/// Callers treat empty as "not a struct param", which keeps the conservative default.
|
|
20604
|
+
fn struct_numeric_fields(
|
|
20605
|
+
ann: &TypeAnnotation,
|
|
20606
|
+
aliases: &std::collections::HashMap<String, RustType>,
|
|
20607
|
+
) -> HashSet<String> {
|
|
20608
|
+
match RustType::from_annotation_with_aliases(ann, aliases) {
|
|
20609
|
+
RustType::Named { ref fields, .. } => fields
|
|
20610
|
+
.iter()
|
|
20611
|
+
.filter(|(_, t)| matches!(t, RustType::F64 | RustType::I32) || t.is_narrow_int())
|
|
20612
|
+
.map(|(n, _)| n.to_string())
|
|
20613
|
+
.collect(),
|
|
20614
|
+
_ => HashSet::new(),
|
|
20615
|
+
}
|
|
20616
|
+
}
|
|
20617
|
+
|
|
19515
20618
|
fn native_fn_numeric_locals(
|
|
19516
20619
|
body: &Statement,
|
|
19517
20620
|
params: &HashSet<String>,
|
|
@@ -19545,6 +20648,10 @@ impl Codegen {
|
|
|
19545
20648
|
..
|
|
19546
20649
|
} => {
|
|
19547
20650
|
let numeric = type_ann.as_ref().is_some_and(Self::ann_is_number)
|
|
20651
|
+
// `: i32` is a number too. Without this, `let j: i32 = …` was never seeded as a
|
|
20652
|
+
// numeric local, so every later use of `j` failed the safety proof and the whole
|
|
20653
|
+
// function stayed boxed — for an annotation the author added to make it FASTER.
|
|
20654
|
+
|| type_ann.as_ref().is_some_and(Self::ann_is_i32)
|
|
19548
20655
|
|| matches!(
|
|
19549
20656
|
init,
|
|
19550
20657
|
Some(Expr::Literal {
|
|
@@ -19564,7 +20671,13 @@ impl Codegen {
|
|
|
19564
20671
|
}) if matches!(
|
|
19565
20672
|
callee.as_ref(),
|
|
19566
20673
|
Expr::Ident { name: fnname, .. }
|
|
19567
|
-
if cand.contains(fnname.as_ref())
|
|
20674
|
+
if cand.contains(fnname.as_ref())
|
|
20675
|
+
|| native_vec_names.contains(fnname.as_ref())
|
|
20676
|
+
// A numbers-in-number-out extern yields a number, same as the
|
|
20677
|
+
// safety proof and `numeric_shaped` already accept. Every
|
|
20678
|
+
// analysis has to agree or a local initialised from an extern is
|
|
20679
|
+
// numeric in one and unknown in another.
|
|
20680
|
+
|| Self::numeric_externs_with(|s| s.contains(fnname.as_ref()))
|
|
19568
20681
|
)
|
|
19569
20682
|
);
|
|
19570
20683
|
if numeric {
|
|
@@ -19676,6 +20789,13 @@ impl Codegen {
|
|
|
19676
20789
|
nums: &HashSet<String>,
|
|
19677
20790
|
native_vec_names: &std::collections::HashSet<String>,
|
|
19678
20791
|
) -> bool {
|
|
20792
|
+
// A field read off an all-numeric struct param is a plain numeric load — `p.x` on a
|
|
20793
|
+
// `&TishStruct_P` is a machine word, not a boxed property lookup. Without this the proof
|
|
20794
|
+
// rejects every object-taking fn, which is why typing a game's functions never produced a
|
|
20795
|
+
// native lowering. See `STRUCT_PARAMS`.
|
|
20796
|
+
if Self::is_struct_param_field(expr) {
|
|
20797
|
+
return true;
|
|
20798
|
+
}
|
|
19679
20799
|
match expr {
|
|
19680
20800
|
Expr::Literal { value, .. } => matches!(value, Literal::Number(_) | Literal::Bool(_)),
|
|
19681
20801
|
Expr::Ident { name, .. } => {
|
|
@@ -19750,7 +20870,13 @@ impl Codegen {
|
|
|
19750
20870
|
}
|
|
19751
20871
|
match callee.as_ref() {
|
|
19752
20872
|
Expr::Ident { name, .. } => {
|
|
19753
|
-
cand.contains(name.as_ref())
|
|
20873
|
+
cand.contains(name.as_ref())
|
|
20874
|
+
|| native_vec_names.contains(name.as_ref())
|
|
20875
|
+
// A numbers-in-number-out extern lowers to a direct
|
|
20876
|
+
// `<crate>::<symbol>_typed(..)` call, so it is native-safe. Without this
|
|
20877
|
+
// any fn touching an extension crate stayed boxed — which in a game is
|
|
20878
|
+
// most of them.
|
|
20879
|
+
|| Self::numeric_externs_with(|s| s.contains(name.as_ref()))
|
|
19754
20880
|
}
|
|
19755
20881
|
Expr::Member { object, prop: MemberProp::Name { name: m, .. }, .. } => {
|
|
19756
20882
|
matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
|
|
@@ -19810,6 +20936,10 @@ impl Codegen {
|
|
|
19810
20936
|
globals: &std::collections::HashSet<String>,
|
|
19811
20937
|
nums: &HashSet<String>,
|
|
19812
20938
|
) -> bool {
|
|
20939
|
+
// `p.field` on an all-numeric struct param IS a number — see `struct_params_with`.
|
|
20940
|
+
if Self::is_struct_param_field(e) {
|
|
20941
|
+
return true;
|
|
20942
|
+
}
|
|
19813
20943
|
match e {
|
|
19814
20944
|
Expr::Literal { value: Literal::Number(_), .. } => true,
|
|
19815
20945
|
Expr::Ident { name, .. } => {
|
|
@@ -19855,7 +20985,12 @@ impl Codegen {
|
|
|
19855
20985
|
&& Self::numeric_shaped(else_branch, params, cand, globals, nums)
|
|
19856
20986
|
}
|
|
19857
20987
|
Expr::Call { callee, .. } => match callee.as_ref() {
|
|
19858
|
-
|
|
20988
|
+
// Same set as the safety proof: a numbers-in-number-out extern RETURNS a number, so
|
|
20989
|
+
// `return tac_unit_hp(id)` is a numeric return and the fn keeps its native ABI.
|
|
20990
|
+
Expr::Ident { name, .. } => {
|
|
20991
|
+
cand.contains(name.as_ref())
|
|
20992
|
+
|| Self::numeric_externs_with(|s| s.contains(name.as_ref()))
|
|
20993
|
+
}
|
|
19859
20994
|
Expr::Member { object, prop: MemberProp::Name { name: m, .. }, .. } => {
|
|
19860
20995
|
matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
|
|
19861
20996
|
&& matches!(
|
|
@@ -20055,12 +21190,28 @@ impl Codegen {
|
|
|
20055
21190
|
}
|
|
20056
21191
|
let i32_abi = self.native_fns_i32.contains(name.as_ref());
|
|
20057
21192
|
let abi = if i32_abi { "i32" } else { "f64" };
|
|
21193
|
+
// Per-parameter type. A struct param lowers to `&TishStruct_<alias>` — one pointer,
|
|
21194
|
+
// no boxing, and `p.field` becomes a machine-word load. Numeric params keep the
|
|
21195
|
+
// single f64/i32 ABI. `mut` only on the numerics: a shared reference is not rebound.
|
|
20058
21196
|
let plist: Vec<String> = params
|
|
20059
21197
|
.iter()
|
|
20060
21198
|
.filter_map(|p| match p {
|
|
20061
|
-
FunParam::Simple(tp) =>
|
|
20062
|
-
|
|
20063
|
-
|
|
21199
|
+
FunParam::Simple(tp) => Some(
|
|
21200
|
+
match tp.type_ann.as_ref().map(|a| {
|
|
21201
|
+
RustType::from_annotation_with_aliases(a, &self.type_aliases)
|
|
21202
|
+
}) {
|
|
21203
|
+
Some(RustType::Named { name: sname, .. }) => format!(
|
|
21204
|
+
"{}: &{}",
|
|
21205
|
+
Self::escape_ident(tp.name.as_ref()),
|
|
21206
|
+
crate::types::named_struct_ident(&sname)
|
|
21207
|
+
),
|
|
21208
|
+
_ => format!(
|
|
21209
|
+
"mut {}: {}",
|
|
21210
|
+
Self::escape_ident(tp.name.as_ref()),
|
|
21211
|
+
abi
|
|
21212
|
+
),
|
|
21213
|
+
},
|
|
21214
|
+
),
|
|
20064
21215
|
_ => None,
|
|
20065
21216
|
})
|
|
20066
21217
|
.collect();
|
|
@@ -20079,10 +21230,13 @@ impl Codegen {
|
|
|
20079
21230
|
self.type_context.push_scope();
|
|
20080
21231
|
for p in params {
|
|
20081
21232
|
if let FunParam::Simple(tp) = p {
|
|
20082
|
-
|
|
20083
|
-
|
|
20084
|
-
|
|
20085
|
-
|
|
21233
|
+
let ty = match tp.type_ann.as_ref().map(|a| {
|
|
21234
|
+
RustType::from_annotation_with_aliases(a, &self.type_aliases)
|
|
21235
|
+
}) {
|
|
21236
|
+
Some(named @ RustType::Named { .. }) => named,
|
|
21237
|
+
_ => if i32_abi { RustType::I32 } else { RustType::F64 },
|
|
21238
|
+
};
|
|
21239
|
+
self.type_context.define(tp.name.as_ref(), ty);
|
|
20086
21240
|
}
|
|
20087
21241
|
}
|
|
20088
21242
|
let copy_suffix = if copy_idx == 0 { String::new() } else { format!("_r{}", copy_idx) };
|
|
@@ -20281,6 +21435,20 @@ impl Codegen {
|
|
|
20281
21435
|
// An f64-typed expression returned from an `i32` fn takes the same ToInt32 the
|
|
20282
21436
|
// boxed path would have applied at the consumer.
|
|
20283
21437
|
format!("tishlang_runtime::to_int32({})", code)
|
|
21438
|
+
} else if want == RustType::F64 && ty.is_integer_scalar() {
|
|
21439
|
+
// An INTEGER-typed expression returned from an `f64`-ABI fn. Widening is exact
|
|
21440
|
+
// (i32 and every narrow width fit in f64), and without it the body computes an
|
|
21441
|
+
// integer while the signature declares f64:
|
|
21442
|
+
//
|
|
21443
|
+
// fn uiKeys_native() -> f64 { return tish_agb::keys_edge_typed(); }
|
|
21444
|
+
// ^ i32, expected f64 (E0308)
|
|
21445
|
+
//
|
|
21446
|
+
// This was unreachable while the only integer-returning callees were fns the
|
|
21447
|
+
// proof had already typed; it became reachable when calls to i32 externs were
|
|
21448
|
+
// admitted as native-safe, because an extern reports its own declared return
|
|
21449
|
+
// type. `Fixed` is deliberately NOT covered — it is fixed-point, so `as f64`
|
|
21450
|
+
// would reinterpret the scale rather than convert it.
|
|
21451
|
+
format!("(({}) as f64)", code)
|
|
20284
21452
|
} else {
|
|
20285
21453
|
code
|
|
20286
21454
|
};
|
|
@@ -26125,10 +27293,27 @@ impl Codegen {
|
|
|
26125
27293
|
};
|
|
26126
27294
|
let mut argc: Vec<String> = Vec::with_capacity(args.len());
|
|
26127
27295
|
let mut ok = true;
|
|
26128
|
-
for a in args {
|
|
27296
|
+
for (idx, a) in args.iter().enumerate() {
|
|
26129
27297
|
if let CallArg::Expr(e) = a {
|
|
27298
|
+
// A struct slot takes `&caller_struct`; coercing it to a number
|
|
27299
|
+
// would not compile. Mirrors the boxed-context call site.
|
|
27300
|
+
let is_struct = matches!(
|
|
27301
|
+
self.native_fn_param_types
|
|
27302
|
+
.get(fname.as_ref())
|
|
27303
|
+
.and_then(|v| v.get(idx)),
|
|
27304
|
+
Some(RustType::Named { .. })
|
|
27305
|
+
);
|
|
26130
27306
|
let (ac, at) = self.emit_typed_expr(e)?;
|
|
26131
|
-
|
|
27307
|
+
if is_struct {
|
|
27308
|
+
if matches!(at, RustType::Named { .. }) {
|
|
27309
|
+
argc.push(format!("&{}", ac));
|
|
27310
|
+
} else {
|
|
27311
|
+
ok = false;
|
|
27312
|
+
break;
|
|
27313
|
+
}
|
|
27314
|
+
} else {
|
|
27315
|
+
argc.push(self.coerce_native_arg(&ac, at, want.clone()));
|
|
27316
|
+
}
|
|
26132
27317
|
} else {
|
|
26133
27318
|
ok = false;
|
|
26134
27319
|
break;
|