@tishlang/tish-format 3.8.6 → 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-format +0 -0
- package/crates/tish_compile/src/codegen.rs +675 -22
- package/crates/tish_compile/src/lib.rs +20 -0
- package/crates/tish_compile/src/types.rs +7 -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_fmt/Cargo.toml +1 -1
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
|
@@ -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>,
|
|
@@ -1443,6 +1461,11 @@ impl Codegen {
|
|
|
1443
1461
|
outer_params_stack: Vec::new(),
|
|
1444
1462
|
outer_vars_stack: vec![Vec::new()], // Start with module-level scope
|
|
1445
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(),
|
|
1446
1469
|
native_numeric_globals: std::collections::HashMap::new(),
|
|
1447
1470
|
module_const_f64_arrays: std::collections::HashMap::new(),
|
|
1448
1471
|
module_const_int_statics: std::collections::HashSet::new(),
|
|
@@ -3105,7 +3128,7 @@ impl Codegen {
|
|
|
3105
3128
|
// no_std GBA ROM: alloc-based, Arc aliases to Rc in the facade. The
|
|
3106
3129
|
// prelude `use tishlang_runtime::{…}` below is unchanged — those names
|
|
3107
3130
|
// are re-exported by the tishlang_runtime_gba facade.
|
|
3108
|
-
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");
|
|
3109
3132
|
self.write("extern crate alloc;\n");
|
|
3110
3133
|
self.write("use core::cell::RefCell;\n");
|
|
3111
3134
|
self.write("use alloc::rc::Rc;\n");
|
|
@@ -3117,7 +3140,7 @@ impl Codegen {
|
|
|
3117
3140
|
// f64 transcendentals for inline math (`Math.sqrt(x)` → `x.sqrt()`).
|
|
3118
3141
|
self.write("use tishlang_runtime::FloatExt;\n");
|
|
3119
3142
|
} else {
|
|
3120
|
-
self.write("#![allow(unused, non_snake_case)]\n\n");
|
|
3143
|
+
self.write("#![allow(unused, non_snake_case, non_upper_case_globals)]\n\n");
|
|
3121
3144
|
self.write("use std::cell::RefCell;\n");
|
|
3122
3145
|
self.write("use std::rc::Rc;\n");
|
|
3123
3146
|
self.write("use std::sync::Arc;\n");
|
|
@@ -3532,6 +3555,46 @@ impl Codegen {
|
|
|
3532
3555
|
}
|
|
3533
3556
|
self.nonneg_locals = self.collect_nonneg_locals(&program.statements);
|
|
3534
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
|
+
|
|
3535
3598
|
// Gba mode is sync in P2 (real async lowering is P5); `std::error::Error` in
|
|
3536
3599
|
// the signature is rewritten to `core::error::Error` by the no_std post-pass.
|
|
3537
3600
|
if self.is_async && self.emit_mode != crate::NativeEmitMode::Gba {
|
|
@@ -3543,6 +3606,29 @@ impl Codegen {
|
|
|
3543
3606
|
}
|
|
3544
3607
|
self.indent += 1;
|
|
3545
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
|
+
|
|
3546
3632
|
// Before anything else: one namespace object per native module, shared by all of its imports.
|
|
3547
3633
|
self.emit_native_namespace_preamble();
|
|
3548
3634
|
|
|
@@ -3870,6 +3956,11 @@ impl Codegen {
|
|
|
3870
3956
|
);
|
|
3871
3957
|
}
|
|
3872
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
|
+
|
|
3873
3964
|
// Polars, Egui etc. are emitted via VarDecl from import { X } from 'tish:...'
|
|
3874
3965
|
|
|
3875
3966
|
// Pre-scan for top-level function declarations and create cells (for mutual recursion)
|
|
@@ -3881,6 +3972,10 @@ impl Codegen {
|
|
|
3881
3972
|
if self.aggregate_alias.is_some() && self.aggregate_fns.contains_key(func_name) {
|
|
3882
3973
|
continue;
|
|
3883
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
|
+
}
|
|
3884
3979
|
let escaped = Self::escape_ident(func_name);
|
|
3885
3980
|
self.writeln(&format!(
|
|
3886
3981
|
"let {}_cell: VmRef<Value> = VmRef::new(Value::Null);",
|
|
@@ -3922,7 +4017,56 @@ impl Codegen {
|
|
|
3922
4017
|
// above it establish. A literal cannot observe or disturb anything, so moving it is not
|
|
3923
4018
|
// visible to the program.
|
|
3924
4019
|
let reordered = Self::hoist_forward_referenced_module_bindings(&program.statements);
|
|
3925
|
-
|
|
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)?;
|
|
3926
4070
|
if self.is_async {
|
|
3927
4071
|
self.async_context_stack.pop();
|
|
3928
4072
|
}
|
|
@@ -3961,6 +4105,13 @@ impl Codegen {
|
|
|
3961
4105
|
self.writeln("Ok(())");
|
|
3962
4106
|
self.indent -= 1;
|
|
3963
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
|
+
}
|
|
3964
4115
|
if self.emit_mode == crate::NativeEmitMode::EmbeddedLib {
|
|
3965
4116
|
self.writeln("");
|
|
3966
4117
|
self.writeln("#[no_mangle]");
|
|
@@ -6216,6 +6367,25 @@ impl Codegen {
|
|
|
6216
6367
|
rust_type = RustType::Vec(Box::new(RustType::I32));
|
|
6217
6368
|
}
|
|
6218
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
|
+
|
|
6219
6389
|
// #176: top-level numeric globals live in a thread_local `Cell<f64>` — no local slot.
|
|
6220
6390
|
if self.native_numeric_globals.contains_key(name.as_ref())
|
|
6221
6391
|
&& self.outer_vars_stack.len() == 1
|
|
@@ -6360,7 +6530,20 @@ impl Codegen {
|
|
|
6360
6530
|
Some(e) => self.emit_native_expr(e, &rust_type)?,
|
|
6361
6531
|
None => rust_type.default_value(),
|
|
6362
6532
|
};
|
|
6363
|
-
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()) {
|
|
6364
6547
|
// Closure-mutated: same Rc<RefCell<T>> pattern as Value (assignments use borrow_mut)
|
|
6365
6548
|
self.writeln(&format!("let {} = VmRef::new({});", escaped_name, expr_str));
|
|
6366
6549
|
self.rc_cell_storage_define(name.as_ref());
|
|
@@ -6384,12 +6567,23 @@ impl Codegen {
|
|
|
6384
6567
|
None => ("Value::Null".to_string(), false),
|
|
6385
6568
|
};
|
|
6386
6569
|
// Vars that are mutated by nested closures must be RefCell from the start
|
|
6387
|
-
if
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
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()) {
|
|
6393
6587
|
self.writeln(&format!("let {} = VmRef::new({});", escaped_name, init_val));
|
|
6394
6588
|
self.rc_cell_storage_define(name.as_ref());
|
|
6395
6589
|
} else if clone_needed {
|
|
@@ -7229,6 +7423,10 @@ impl Codegen {
|
|
|
7229
7423
|
&& !param_names.contains(name)
|
|
7230
7424
|
&& !local_var_names.contains(name)
|
|
7231
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))
|
|
7232
7430
|
.filter(|name| {
|
|
7233
7431
|
![
|
|
7234
7432
|
"Boolean",
|
|
@@ -7290,7 +7488,17 @@ impl Codegen {
|
|
|
7290
7488
|
}
|
|
7291
7489
|
}
|
|
7292
7490
|
|
|
7293
|
-
|
|
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
|
+
}
|
|
7294
7502
|
self.indent += 1;
|
|
7295
7503
|
// Clone RefCell for outer vars so closure can capture
|
|
7296
7504
|
for outer_var in &outer_vars {
|
|
@@ -7301,7 +7509,9 @@ impl Codegen {
|
|
|
7301
7509
|
));
|
|
7302
7510
|
}
|
|
7303
7511
|
// Clone the cell so the closure can reference the function recursively
|
|
7304
|
-
|
|
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;
|
|
7305
7515
|
if needs_self_ref {
|
|
7306
7516
|
self.writeln(&format!(
|
|
7307
7517
|
"let {}_ref = {}_cell.clone();",
|
|
@@ -7315,7 +7525,12 @@ impl Codegen {
|
|
|
7315
7525
|
.map(|scope| {
|
|
7316
7526
|
scope
|
|
7317
7527
|
.iter()
|
|
7318
|
-
.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
|
+
})
|
|
7319
7534
|
.cloned()
|
|
7320
7535
|
.collect()
|
|
7321
7536
|
})
|
|
@@ -7351,6 +7566,7 @@ impl Codegen {
|
|
|
7351
7566
|
for f in scope {
|
|
7352
7567
|
if f.as_str() != name_raw
|
|
7353
7568
|
&& referenced.contains(f.as_str())
|
|
7569
|
+
&& !self.is_module_fn_static(f.as_str())
|
|
7354
7570
|
&& !sibling_fns.iter().any(|s| s == f)
|
|
7355
7571
|
&& !outer_vars.iter().any(|v| v == f)
|
|
7356
7572
|
&& !outer_params.iter().any(|p| p == f)
|
|
@@ -7425,7 +7641,10 @@ impl Codegen {
|
|
|
7425
7641
|
"NaN",
|
|
7426
7642
|
"serve",
|
|
7427
7643
|
] {
|
|
7428
|
-
|
|
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) {
|
|
7429
7648
|
self.writeln(&format!("let {} = {}.clone();", builtin, builtin));
|
|
7430
7649
|
}
|
|
7431
7650
|
}
|
|
@@ -7466,6 +7685,29 @@ impl Codegen {
|
|
|
7466
7685
|
if crate::native_recur_guard_enabled() {
|
|
7467
7686
|
self.writeln("let Some(_tish_depth) = tishlang_runtime::enter_call_guarded() else { return Value::Null; };");
|
|
7468
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
|
+
}
|
|
7469
7711
|
// Mutable outer vars: capture the RefCell so assignments use borrow_mut
|
|
7470
7712
|
for outer_var in &mutable_outer_vars {
|
|
7471
7713
|
let var_escaped = Self::escape_ident(outer_var);
|
|
@@ -7769,12 +8011,16 @@ impl Codegen {
|
|
|
7769
8011
|
self.value_fn_depth = self.value_fn_depth.saturating_sub(1);
|
|
7770
8012
|
self.try_closure_depth = saved_try_depth;
|
|
7771
8013
|
self.indent -= 1;
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
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
|
+
}
|
|
7778
8024
|
}
|
|
7779
8025
|
}
|
|
7780
8026
|
Ok(())
|
|
@@ -8366,6 +8612,16 @@ impl Codegen {
|
|
|
8366
8612
|
if let Some(uv) = self.usize_var_subst.get(name.as_ref()) {
|
|
8367
8613
|
return Ok(format!("Value::Number(({} as f64))", uv));
|
|
8368
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
|
+
}
|
|
8369
8625
|
let escaped = Self::escape_ident(name.as_ref());
|
|
8370
8626
|
if self.refcell_wrapped_vars.contains(name.as_ref()) {
|
|
8371
8627
|
let var_type = self.type_context.get_type(name.as_ref());
|
|
@@ -12316,6 +12572,8 @@ impl Codegen {
|
|
|
12316
12572
|
]
|
|
12317
12573
|
.contains(&name.as_str())
|
|
12318
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())
|
|
12319
12577
|
})
|
|
12320
12578
|
.collect();
|
|
12321
12579
|
|
|
@@ -12406,7 +12664,9 @@ impl Codegen {
|
|
|
12406
12664
|
"RegExp",
|
|
12407
12665
|
"Polars",
|
|
12408
12666
|
] {
|
|
12409
|
-
|
|
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) {
|
|
12410
12670
|
code.push_str(&format!(" let {} = {}.clone();\n", builtin, builtin));
|
|
12411
12671
|
}
|
|
12412
12672
|
}
|
|
@@ -12418,7 +12678,15 @@ impl Codegen {
|
|
|
12418
12678
|
.map(|scope| {
|
|
12419
12679
|
scope
|
|
12420
12680
|
.iter()
|
|
12421
|
-
.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
|
+
})
|
|
12422
12690
|
.cloned()
|
|
12423
12691
|
.collect()
|
|
12424
12692
|
})
|
|
@@ -12473,6 +12741,12 @@ impl Codegen {
|
|
|
12473
12741
|
// arrow whose grandparent-scope var was TLS-lowered). Same exclusion the
|
|
12474
12742
|
// FunDecl closure path and this fn's own outer_vars filter already apply.
|
|
12475
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())
|
|
12476
12750
|
})
|
|
12477
12751
|
.cloned()
|
|
12478
12752
|
.collect()
|
|
@@ -12494,6 +12768,26 @@ impl Codegen {
|
|
|
12494
12768
|
let saved_try_depth = self.try_closure_depth;
|
|
12495
12769
|
self.try_closure_depth = 0;
|
|
12496
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
|
+
}
|
|
12497
12791
|
// Make captured outer params available as plain Values (from _ref RefCells)
|
|
12498
12792
|
for outer_param in &outer_params {
|
|
12499
12793
|
let param_escaped = Self::escape_ident(outer_param);
|
|
@@ -17120,6 +17414,295 @@ impl Codegen {
|
|
|
17120
17414
|
format!("G_{}", safe.to_uppercase())
|
|
17121
17415
|
}
|
|
17122
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
|
+
|
|
17123
17706
|
fn native_global_static(name: &str) -> String {
|
|
17124
17707
|
Self::global_static_name(name)
|
|
17125
17708
|
}
|
|
@@ -17743,6 +18326,76 @@ impl Codegen {
|
|
|
17743
18326
|
Some(out)
|
|
17744
18327
|
}
|
|
17745
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
|
+
|
|
17746
18399
|
/// An initialiser that cannot observe or disturb program state, so moving it is invisible.
|
|
17747
18400
|
/// Deliberately narrow — array/object literals are included only when every element is itself
|
|
17748
18401
|
/// literal, so no call or property read comes along for the ride.
|