@tishlang/tish-lsp 3.8.6 → 3.9.2

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.
@@ -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
- self.emit_statements_with_folds(reordered.as_deref().unwrap_or(&program.statements))?;
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.refcell_wrapped_vars.contains(name.as_ref()) {
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 self.refcell_wrapped_vars.contains(name.as_ref()) {
6388
- let init_val = if clone_needed {
6389
- format!("({}).clone()", expr_str)
6390
- } else {
6391
- expr_str.to_string()
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
- self.writeln(&format!("let {} = {{", name_str));
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
- let needs_self_ref = referenced.contains(name_raw);
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| s.as_str() != name_raw && referenced.contains(s.as_str()))
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
- if referenced.contains(*builtin) {
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
- self.writeln("};");
7773
- // Update the cell with the actual function value
7774
- self.writeln(&format!(
7775
- "*{}_cell.borrow_mut() = {}.clone();",
7776
- name_str, name_str
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
- if referenced.contains(*builtin) {
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| referenced.contains(f.as_str()) && !param_names.contains(*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.