Skip to main content

lua_vm/
state.rs

1//! Global State — port of `lstate.c` (445 lines, 25 functions) + `lstate.h` (merged).
2//!
3//! Manages per-thread ([`LuaState`]) and process-wide ([`GlobalState`]) Lua state:
4//! creation, initialization, teardown, and coroutine lifecycle helpers.
5//!
6//! The `lstate.h` header is merged into this module per PORTING.md §1.
7//!
8//! # C source files
9//! - `reference/lua-5.4.7/src/lstate.c`  (445 lines, 25 functions)
10//! - `reference/lua-5.4.7/src/lstate.h`  (408 lines; struct + macro definitions merged)
11
12// PORT NOTE: The C `LX` (thread + extra space) and `LG` (LX + global state) layout
13// wrappers are C-only pointer-arithmetic helpers for allocating the main thread and
14// GlobalState as one contiguous block. In Rust, `GlobalState` and `LuaState` are
15// separate heap-allocated values linked via `Rc<RefCell<GlobalState>>`. No LX/LG
16// equivalents are needed.
17
18// PORT NOTE: C macro `fromstate(L)` (cast LX* from lua_State*) is C-only pointer
19// arithmetic and is not translated. Rust owns the allocations via Rc/Box.
20
21use std::cell::RefCell;
22use std::hash::{BuildHasherDefault, Hasher};
23use std::rc::Rc;
24
25use crate::string::StringPool;
26pub use lua_types::error::LuaError;
27pub use lua_types::{CallInfoIdx, StackIdx};
28
29/// Internal: a thin wrapper used so stubbed methods can accept either
30/// `StackIdx` or `u32` (Phase A code mixes both). Phase B will normalise.
31pub struct StackIdxConv(pub StackIdx);
32
33/// Phase-A code casts `StackIdx as i32`; provide a `From` so it compiles.
34/// TODO(phase-b): expressions like `state.top_idx().0 as i32` should become
35/// `state.top_idx().raw() as i32`. The non-primitive-cast error is silenced
36/// here by promoting the StackIdx through a free-function conversion.
37#[inline(always)]
38pub fn stack_idx_to_i32(i: StackIdx) -> i32 {
39    i.0 as i32
40}
41
42impl From<u32> for StackIdxConv {
43    #[inline(always)]
44    fn from(v: u32) -> Self {
45        StackIdxConv(StackIdx(v))
46    }
47}
48impl From<i32> for StackIdxConv {
49    #[inline(always)]
50    fn from(v: i32) -> Self {
51        StackIdxConv(StackIdx(v.max(0) as u32))
52    }
53}
54impl From<usize> for StackIdxConv {
55    #[inline(always)]
56    fn from(v: usize) -> Self {
57        StackIdxConv(StackIdx(v as u32))
58    }
59}
60impl From<StackIdx> for StackIdxConv {
61    #[inline(always)]
62    fn from(v: StackIdx) -> Self {
63        StackIdxConv(v)
64    }
65}
66pub use lua_types::closure::{
67    LuaCClosure as LuaClosureC, LuaCFnPtr, LuaClosure, LuaLClosure as LuaClosureLua,
68};
69pub use lua_types::gc::GcRef;
70pub use lua_types::proto::LuaProto;
71pub use lua_types::string::LuaString;
72pub use lua_types::upval::UpVal;
73pub use lua_types::userdata::LuaUserData;
74pub use lua_types::value::{F2Imod, LuaTable, LuaValue};
75
76pub struct LuaByteHasher {
77    hash: u64,
78}
79
80impl Default for LuaByteHasher {
81    fn default() -> Self {
82        Self {
83            hash: 0xcbf2_9ce4_8422_2325,
84        }
85    }
86}
87
88impl Hasher for LuaByteHasher {
89    #[inline]
90    fn write(&mut self, bytes: &[u8]) {
91        const PRIME: u64 = 0x0000_0100_0000_01b3;
92        for &byte in bytes {
93            self.hash ^= u64::from(byte);
94            self.hash = self.hash.wrapping_mul(PRIME);
95        }
96    }
97
98    #[inline]
99    fn write_u8(&mut self, i: u8) {
100        self.write(&[i]);
101    }
102
103    #[inline]
104    fn write_usize(&mut self, i: usize) {
105        self.write(&i.to_ne_bytes());
106    }
107
108    #[inline]
109    fn finish(&self) -> u64 {
110        self.hash
111    }
112}
113
114pub type LuaByteBuildHasher = BuildHasherDefault<LuaByteHasher>;
115
116/// The short-string intern table, shaped like C-Lua's `stringtable`
117/// (lstring.c): power-of-two hash buckets of `GcRef<LuaString>` chained by
118/// Vec instead of `u.hnext`. Compared to the previous
119/// `HashMap<Box<[u8]>, GcRef<LuaString>>`:
120///
121/// - lookup hashes the input bytes ONCE and never allocates (the entry-API
122///   shape boxed the key on every call — rejected experiment
123///   `intern-hitpath-borrowed-lookup` documents why a partial fix loses);
124/// - insert reuses the same hash, no second probe;
125/// - bytes are stored once (in the `LuaString`), not duplicated in a map key;
126/// - dead strings are removed O(dead) by `(hash, identity)` pairs collected
127///   during the GC mark phase, replacing the O(table·log live)
128///   sort + binary-search retain that dominated churn-heavy profiles
129///   (concat_chain 20260609T2201Z: intern machinery ~25% of wall).
130pub struct InternedStringMap {
131    buckets: Vec<Vec<GcRef<LuaString>>>,
132    count: usize,
133}
134
135impl Default for InternedStringMap {
136    fn default() -> Self {
137        InternedStringMap {
138            buckets: (0..64).map(|_| Vec::new()).collect(),
139            count: 0,
140        }
141    }
142}
143
144impl InternedStringMap {
145    #[inline]
146    fn mask(&self) -> usize {
147        self.buckets.len() - 1
148    }
149
150    pub fn len(&self) -> usize {
151        self.count
152    }
153
154    pub fn is_empty(&self) -> bool {
155        self.count == 0
156    }
157
158    /// Number of hash buckets currently allocated (the power-of-two table
159    /// size, C's `strt.size`). Exposed for the shrink-policy test, which
160    /// asserts the array grows under a flood and shrinks back toward 64 once
161    /// the interned strings are collected.
162    pub fn bucket_count(&self) -> usize {
163        self.buckets.len()
164    }
165
166    #[inline]
167    pub fn find(&self, bytes: &[u8], hash: u32) -> Option<GcRef<LuaString>> {
168        let bucket = &self.buckets[hash as usize & self.mask()];
169        bucket
170            .iter()
171            .find(|s| s.hash() == hash && s.as_bytes() == bytes)
172            .cloned()
173    }
174
175    /// C's `luaS_resize` growth rule: keep average chain length ~1.
176    pub fn insert(&mut self, s: GcRef<LuaString>) {
177        if self.count >= self.buckets.len() {
178            self.resize(self.buckets.len() * 2);
179        }
180        let m = self.mask();
181        self.buckets[s.hash() as usize & m].push(s);
182        self.count += 1;
183    }
184
185    fn resize(&mut self, new_len: usize) {
186        let old = std::mem::replace(
187            &mut self.buckets,
188            (0..new_len).map(|_| Vec::new()).collect(),
189        );
190        let m = self.mask();
191        for bucket in old {
192            for s in bucket {
193                self.buckets[s.hash() as usize & m].push(s);
194            }
195        }
196    }
197
198    /// C's `luaS_resize` shrink path, driven by `lgc.c:checkSizes`
199    /// (`if (g->strt.nuse < g->strt.size / 4) luaS_resize(L, size/2)`).
200    /// Shrinks the bucket array when the live load factor falls below 25%
201    /// (`count * 4 < buckets.len()`), down to `next_power_of_two(count)`
202    /// floored at the initial 64 (C's `MINSTRTABSIZE`). The 4× gap is
203    /// hysteresis: a table at load factor just under 1.0 will not thrash,
204    /// since the next grow-then-shrink cycle needs the population to drop
205    /// fourfold first. Rehashing only relocates the surviving
206    /// `GcRef<LuaString>` entries by their cached `hash()`; it never derefs a
207    /// string, so it is safe to call from the post-mark/sweep GC hook AFTER
208    /// all dead entries have been removed (only live refs remain).
209    pub fn shrink_if_sparse(&mut self) {
210        if self.count * 4 >= self.buckets.len() {
211            return;
212        }
213        let target = self.count.next_power_of_two().max(64);
214        if target < self.buckets.len() {
215            self.resize(target);
216        }
217    }
218
219    /// O(dead): removes one entry located by its cached hash + GC identity.
220    pub fn remove(&mut self, hash: u32, identity: usize) {
221        let m = self.mask();
222        let bucket = &mut self.buckets[hash as usize & m];
223        if let Some(pos) = bucket.iter().position(|s| s.identity() == identity) {
224            bucket.swap_remove(pos);
225            self.count -= 1;
226        }
227    }
228
229    pub fn iter(&self) -> impl Iterator<Item = &GcRef<LuaString>> {
230        self.buckets.iter().flatten()
231    }
232
233    pub fn contains_key(&self, bytes: &[u8]) -> bool {
234        self.find(bytes, LuaString::hash_bytes(bytes, 0)).is_some()
235    }
236}
237
238/// A Lua-callable function pointer. C: `lua_CFunction`.
239///
240/// TODO(phase-b): the lua-types crate uses a placeholder
241/// `LuaCFnPtr = fn() -> i32` since it can't reference `LuaState` without a
242/// circular dep. The real signature is `fn(&mut LuaState) -> Result<usize, LuaError>`,
243/// kept here as the lua-vm-facing type alias.
244pub type LuaCFunction = fn(&mut LuaState) -> Result<usize, LuaError>;
245
246pub type LuaRustFunction = Rc<dyn Fn(&mut LuaState) -> Result<usize, LuaError>>;
247
248#[derive(Clone)]
249pub enum LuaCallable {
250    Bare(LuaCFunction),
251    Rust(LuaRustFunction),
252}
253
254impl std::fmt::Debug for LuaCallable {
255    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256        match self {
257            LuaCallable::Bare(_) => f.write_str("LuaCallable::Bare(..)"),
258            LuaCallable::Rust(_) => f.write_str("LuaCallable::Rust(..)"),
259        }
260    }
261}
262
263impl LuaCallable {
264    pub fn bare(f: LuaCFunction) -> Self {
265        LuaCallable::Bare(f)
266    }
267
268    pub fn rust(f: LuaRustFunction) -> Self {
269        LuaCallable::Rust(f)
270    }
271
272    pub fn as_bare(&self) -> Option<LuaCFunction> {
273        match self {
274            LuaCallable::Bare(f) => Some(*f),
275            LuaCallable::Rust(_) => None,
276        }
277    }
278
279    pub fn call(&self, state: &mut LuaState) -> Result<usize, LuaError> {
280        match self {
281            LuaCallable::Bare(f) => f(state),
282            LuaCallable::Rust(f) => f(state),
283        }
284    }
285}
286
287#[derive(Clone, Debug)]
288pub enum FinalizerObject {
289    Table(GcRef<LuaTable>),
290    UserData(GcRef<LuaUserData>),
291}
292
293impl FinalizerObject {
294    pub fn identity(&self) -> usize {
295        match self {
296            FinalizerObject::Table(t) => t.identity(),
297            FinalizerObject::UserData(u) => u.identity(),
298        }
299    }
300
301    pub fn metatable(&self) -> Option<GcRef<LuaTable>> {
302        match self {
303            FinalizerObject::Table(t) => t.metatable(),
304            FinalizerObject::UserData(u) => u.metatable(),
305        }
306    }
307
308    pub fn as_lua_value(&self) -> LuaValue {
309        match self {
310            FinalizerObject::Table(t) => LuaValue::Table(t.clone()),
311            FinalizerObject::UserData(u) => LuaValue::UserData(u.clone()),
312        }
313    }
314
315    pub fn mark(&self, marker: &mut lua_gc::Marker) {
316        match self {
317            FinalizerObject::Table(t) => marker.mark(t.0),
318            FinalizerObject::UserData(u) => marker.mark(u.0),
319        }
320    }
321
322    pub fn heap_ptr(&self) -> Option<std::ptr::NonNull<lua_gc::GcBox<dyn lua_gc::Trace>>> {
323        Some(match self {
324            FinalizerObject::Table(t) => t.0.as_trace_ptr(),
325            FinalizerObject::UserData(u) => u.0.as_trace_ptr(),
326        })
327    }
328
329    pub fn age(&self) -> lua_gc::GcAge {
330        match self {
331            FinalizerObject::Table(t) => t.0.age(),
332            FinalizerObject::UserData(u) => u.0.age(),
333        }
334    }
335
336    pub fn is_finalized(&self) -> bool {
337        match self {
338            FinalizerObject::Table(t) => t.0.is_finalized(),
339            FinalizerObject::UserData(u) => u.0.is_finalized(),
340        }
341    }
342
343    pub fn set_finalized(&self, finalized: bool) {
344        match self {
345            FinalizerObject::Table(t) => t.0.set_finalized(finalized),
346            FinalizerObject::UserData(u) => u.0.set_finalized(finalized),
347        }
348    }
349}
350
351impl lua_gc::FinalizerEntry for FinalizerObject {
352    fn identity(&self) -> usize {
353        FinalizerObject::identity(self)
354    }
355
356    fn heap_ptr(&self) -> Option<std::ptr::NonNull<lua_gc::GcBox<dyn lua_gc::Trace>>> {
357        FinalizerObject::heap_ptr(self)
358    }
359
360    fn age(&self) -> lua_gc::GcAge {
361        FinalizerObject::age(self)
362    }
363
364    fn is_finalized(&self) -> bool {
365        FinalizerObject::is_finalized(self)
366    }
367
368    fn set_finalized(&self, finalized: bool) {
369        FinalizerObject::set_finalized(self, finalized);
370    }
371}
372
373#[derive(Clone, Debug)]
374pub struct WeakTableEntry {
375    table: lua_types::gc::GcWeak<LuaTable>,
376    kind: lua_gc::WeakListKind,
377}
378
379impl WeakTableEntry {
380    pub fn new(table: &GcRef<LuaTable>) -> Self {
381        let mode = table.weak_mode();
382        let weak_keys = (mode & (1 << 0)) != 0;
383        let weak_values = (mode & (1 << 1)) != 0;
384        let kind = match (weak_keys, weak_values) {
385            (true, true) => lua_gc::WeakListKind::AllWeak,
386            (true, false) => lua_gc::WeakListKind::Ephemeron,
387            (false, true) => lua_gc::WeakListKind::WeakValues,
388            (false, false) => lua_gc::WeakListKind::WeakValues,
389        };
390        Self {
391            table: table.downgrade(),
392            kind,
393        }
394    }
395}
396
397impl lua_gc::WeakEntry for WeakTableEntry {
398    type Strong = GcRef<LuaTable>;
399
400    fn identity(&self) -> usize {
401        self.table.identity()
402    }
403
404    fn list_kind(&self) -> lua_gc::WeakListKind {
405        self.kind
406    }
407
408    fn upgrade(&self) -> Option<Self::Strong> {
409        self.table.upgrade()
410    }
411}
412
413// ─── Constants (from macros.tsv) ──────────────────────────────────────────────
414
415// macros.tsv: EXTRA_STACK → const EXTRA_STACK: u32 = 5
416pub(crate) const EXTRA_STACK: usize = 5;
417
418// macros.tsv: LUA_MINSTACK → const LUA_MINSTACK: u32 = 20
419pub(crate) const LUA_MINSTACK: usize = 20;
420
421// macros.tsv: BASIC_STACK_SIZE → const BASIC_STACK_SIZE: u32 = 2 * LUA_MINSTACK
422pub(crate) const BASIC_STACK_SIZE: usize = 2 * LUA_MINSTACK;
423
424/// Maximum nested non-yielding C-call recursion depth — the single source of
425/// truth for the call-depth guard (also used by `do_::ccall_inner` and
426/// `do_::lua_resume`).
427///
428/// This is the structural defense that keeps a recursive interpreter sound for
429/// untrusted code: a recursive Rust interpreter consumes host (Rust) stack per
430/// nested Lua→Lua call, so unbounded Lua recursion would otherwise overflow the
431/// OS thread stack and crash the process. Tripping this limit instead raises a
432/// catchable `"stack overflow"` / `"C stack overflow"` Lua error.
433///
434/// Safe margin: each nested call frame consumes a bounded amount of Rust stack,
435/// so `MAXCCALLS` frames fit within the default ~8 MiB thread stack with room to
436/// spare — verified on macOS/Linux release builds against deep non-tail
437/// recursion, infinite `__index`/`__concat`/`__tostring` metamethod chains, and
438/// nested-coroutine `__close` cascades, all of which error cleanly rather than
439/// SIGSEGV (see the `recursion_*` sandbox tests). Embedders that run the VM on a
440/// smaller thread stack should lower this constant proportionally (roughly
441/// `stack_bytes / 40_000`).
442
443pub(crate) const LUAI_MAXCCALLS: u32 = 200;
444
445// macros.tsv: CIST_C → const CIST_C: u16 = 1 << 1
446pub(crate) const CIST_C: u16 = 1 << 1;
447
448// Remaining CIST_* bits from macros.tsv
449pub(crate) const CIST_OAH: u16 = 1 << 0;
450pub(crate) const CIST_FRESH: u16 = 1 << 2;
451pub(crate) const CIST_HOOKED: u16 = 1 << 3;
452pub(crate) const CIST_YPCALL: u16 = 1 << 4;
453pub(crate) const CIST_TAIL: u16 = 1 << 5;
454pub(crate) const CIST_HOOKYIELD: u16 = 1 << 6;
455pub(crate) const CIST_FIN: u16 = 1 << 7;
456pub(crate) const CIST_TRAN: u16 = 1 << 8;
457pub(crate) const CIST_RECST: u32 = 10;
458// macros.tsv: CIST_LEQ → const CIST_LEQ: u16 = 1 << 13 (LUA_COMPAT_LT_LE).
459// Marks a CallInfo whose `__lt` call is standing in for a missing `__le`, so
460// that if the `__lt` metamethod yields, the comparison-resume path
461// (`vm::finish_op`) knows to negate the result — the synchronous derive in
462// `tagmethods::call_order_tm` cannot, since the negation happens after the
463// yield unwinds the stack. Bits 10-12 are CIST_RECST, so this is bit 13 (as C).
464pub(crate) const CIST_LEQ: u16 = 1 << 13;
465
466/// Per-frame hook trap flag, folded out of `CallInfoFrame` into a free
467/// callstatus bit by T2-C2. C 5.4 stores trap in `ci->u.l.trap` (a Lua-only
468/// union field); we instead store it in callstatus bit 14 so reading it costs
469/// one mask with no enum-discriminant branch. Bit 14 is free (LEQ=13 is the
470/// top used bit; RECST occupies 10-12). Only Lua frames are ever trapped
471/// (`set_traps` guards on `is_lua()`), and the bit is cleared whenever a
472/// CallInfo slot is repurposed for a new call (see `prep_call_info`).
473pub(crate) const CIST_TRAP: u16 = 1 << 14;
474
475// macros.tsv: LUA_NUMTYPES → const LUA_NUMTYPES: usize = 9
476const LUA_NUMTYPES: usize = 9;
477
478// TODO(port): import from crate::gc (lgc.c → gc.rs) once it exists in Phase D
479const GCSTPUSR: u8 = 1;
480const GCSTPGC: u8 = 2;
481
482// TODO(port): import from crate::gc in Phase D
483const GCS_PAUSE: u8 = 0;
484
485const LUAI_GCPAUSE: u32 = 200;
486const LUAI_GCMUL: u32 = 100;
487const LUAI_GCSTEPSIZE: u8 = 13;
488const LUAI_GENMAJORMUL: u32 = 100;
489const LUAI_GENMINORMUL: u8 = 20;
490
491const WHITE0BIT: u8 = 0;
492
493const STRCACHE_N: usize = 53;
494const STRCACHE_M: usize = 2;
495
496// ─── GcKind enum ─────────────────────────────────────────────────────────────
497
498/// Garbage collector operating mode.
499///
500/// macros.tsv: `KGC_INC → GcKind::Incremental`, `KGC_GEN → GcKind::Generational`
501#[derive(Debug, Clone, Copy, PartialEq, Eq)]
502pub enum GcKind {
503    Incremental = 0,
504    Generational = 1,
505}
506
507/// State of the built-in warning handler, mirroring the `warnfoff` /
508/// `warnfon` / `warnfcont` static functions in upstream `lauxlib.c`.
509///
510/// `Off` is the install-time default (warnings disabled until `warn("@on")`).
511/// `On` is ready to begin a fresh message. `Cont` means the previous `warn`
512/// call had `tocont` set, so the next message part continues the current line
513/// without re-printing the `Lua warning: ` prefix.
514#[derive(Debug, Clone, Copy, PartialEq, Eq)]
515pub enum WarnMode {
516    Off,
517    On,
518    Cont,
519}
520
521/// Output mode for the testC/ltests warning sink.
522#[derive(Debug, Clone, Copy, PartialEq, Eq)]
523pub enum TestWarnMode {
524    Normal,
525    Allow,
526    Store,
527}
528
529// ─── LuaStatus enum ──────────────────────────────────────────────────────────
530
531/// Thread / call status codes.
532///
533pub use lua_types::status::LuaStatus;
534
535// ─── StackValue ───────────────────────────────────────────────────────────────
536
537/// One slot on the Lua value stack.
538///
539/// C's `StackValue` overlays a to-be-closed delta (`tbclist.delta: u16`) in a
540/// union with the value, because C threads its tbclist through the stack
541/// itself. The Rust port keeps that list as a side structure
542/// (`LuaState.tbclist: Vec<StackIdx>`), so the slot is just the value and
543/// stays 16 bytes — matching C's slot size without the union. A vestigial
544/// `tbc_delta: u16` field (written, never read) padded every slot to 24 bytes
545/// until it was removed on 2026-06-09 (PERF_PUSH_SPEC.md P3).
546///
547/// types.tsv: `StackValue → StackValue { val: LuaValue }` (tbclist.delta →
548/// `LuaState.tbclist: Vec<StackIdx>`)
549#[derive(Clone)]
550pub struct StackValue {
551    pub val: LuaValue,
552}
553
554impl Default for StackValue {
555    fn default() -> Self {
556        StackValue {
557            val: LuaValue::Nil,
558        }
559    }
560}
561
562// ─── CallInfo ────────────────────────────────────────────────────────────────
563
564/// Saved state for a Lua or C call frame.
565///
566/// types.tsv: CallInfo → CallInfo (several fields renamed / adapted).
567///
568/// The C intrusive doubly-linked list (`previous`, `next` as raw pointers) is
569/// replaced by `Option<CallInfoIdx>` indices into `LuaState::call_info`.
570#[derive(Clone)]
571pub struct CallInfo {
572    // types.tsv: CallInfo.func → StackIdx
573    pub func: StackIdx,
574
575    // types.tsv: CallInfo.top → StackIdx
576    pub top: StackIdx,
577
578    // types.tsv: CallInfo.previous → CallInfoIdx (Option at boundary)
579    pub previous: Option<CallInfoIdx>,
580
581    // types.tsv: CallInfo.next → CallInfoIdx (Option at tail)
582    pub next: Option<CallInfoIdx>,
583
584    pub u: CallInfoFrame,
585
586    pub u2: CallInfoExtra,
587
588    // types.tsv: CallInfo.nresults → i16
589    pub nresults: i16,
590
591    // types.tsv: CallInfo.callstatus → u16 (bit-packed CIST_* flags)
592    pub callstatus: u16,
593
594    /// Lua 5.5: number of `__call` metamethods traversed before entering this
595    /// frame. Upstream stores this in the repacked 5.5 `callstatus` bits; keep
596    /// it separate here so older transfer/recover-status bits stay unchanged.
597    pub call_metamethods: u8,
598
599    /// Lua 5.1: count of tail calls "lost" into this reused frame. Mirrors C
600    /// 5.1's `ci->tailcalls`. Each `OP_TAILCALL` reuses the current frame and
601    /// increments this; a normal call gets a fresh slot (via `next_ci`) where
602    /// it is reset to 0. Used only by the 5.1 `debug.getstack` level walk to
603    /// synthesize the `(tail call)` frames the 5.1 debug model exposes; 5.2+
604    /// dropped that model in favour of the `istailcall` flag and never read it.
605    /// Lives in the 3 spare bytes after `call_metamethods`, so `CallInfo` stays
606    /// 72 B (the size guard above still holds).
607    pub tailcalls: u16,
608}
609
610/// Compile-time guard that the T2-C2 `CallInfoFrame` flatten kept the per-frame
611/// footprint unchanged: the payload stays 32 B and `CallInfo` stays 72 B. The
612/// approved design (Option A) is conditioned on not growing `CallInfo`; if a
613/// future field change breaks this, the build fails here rather than silently
614/// regressing per-frame RSS. The byte counts are 64-bit-specific (the payload
615/// holds a fn pointer and two `isize`s), so the guard is gated off 32-bit
616/// targets — on wasm32 the struct is naturally smaller and there is no C
617/// layout-parity claim to enforce.
618#[cfg(target_pointer_width = "64")]
619const _: () = {
620    assert!(core::mem::size_of::<CallInfoFrame>() == 32);
621    assert!(core::mem::size_of::<CallInfo>() == 72);
622};
623
624/// Payload of `CallInfo.u`.
625///
626/// C 5.4 declares this as an anonymous union with a Lua-frame member (`l`)
627/// and a C-frame member (`c`); which member is live is implied by the
628/// `CIST_C` callstatus bit. T2-C2 flattened the previous 2-variant Rust enum
629/// into this branch-free struct: all fields are always present, so the hot
630/// accessors (`saved_pc`, `set_saved_pc`, `nextra_args`) are plain field
631/// reads with no discriminant test. The C-frame fields (`k`, `old_errfunc`,
632/// `ctx`) are only meaningful on C frames; the Lua-frame fields (`savedpc`,
633/// `nextraargs`) only on Lua frames. Accessors carry `debug_assert!`s on the
634/// frame kind that replace the old enum's wrong-variant panic tripwire at
635/// zero release cost. The `trap` flag that used to live in the Lua variant
636/// now lives in callstatus bit `CIST_TRAP`.
637///
638/// Layout: `u32 + i32 + Option<fn> + isize + isize` = 32 B (8-byte aligned),
639/// identical to the previous enum payload, so `CallInfo` stays 72 B.
640#[derive(Clone, Copy)]
641pub struct CallInfoFrame {
642    /// Lua-frame: index of the next bytecode instruction. C: `u.l.savedpc`.
643    /// types.tsv: CallInfo.u.l.savedpc → u32
644    pub savedpc: u32,
645    /// Lua-frame: count of extra varargs collected at entry. C: `u.l.nextraargs`.
646    /// types.tsv: CallInfo.u.l.nextraargs → i32
647    pub nextraargs: i32,
648    /// C-frame: continuation function for a yieldable C call. C: `u.c.k`.
649    /// types.tsv: CallInfo.u.c.k → Option<lua_KFunction>
650    pub k: Option<LuaKFunction>,
651    /// C-frame: saved `errfunc` to restore on pcall recovery. C: `u.c.old_errfunc`.
652    /// types.tsv: CallInfo.u.c.old_errfunc → isize
653    pub old_errfunc: isize,
654    /// C-frame: continuation context. C: `u.c.ctx`.
655    /// types.tsv: CallInfo.u.c.ctx → isize
656    pub ctx: isize,
657}
658
659/// Continuation function for yieldable C calls.  C: `lua_KFunction`.
660pub type LuaKFunction = fn(&mut LuaState, status: i32, ctx: isize) -> Result<usize, LuaError>;
661
662/// Payload of `CallInfo.u2`.
663///
664/// types.tsv: CallInfo.u2 → CallInfoExtra (Rust: struct with all fields, interpretation by context)
665#[derive(Default, Clone, Copy)]
666pub struct CallInfoExtra {
667    pub value: i32,
668    pub ftransfer: u16,
669    pub ntransfer: u16,
670}
671
672impl CallInfoFrame {
673    /// Default C-call frame: no continuation, zero context. The Lua-frame
674    /// fields are benignly zeroed so any latent read on a misclassified frame
675    /// reads the same value the old C variant produced.
676    pub fn c_default() -> Self {
677        CallInfoFrame {
678            savedpc: 0,
679            nextraargs: 0,
680            k: None,
681            old_errfunc: 0,
682            ctx: 0,
683        }
684    }
685
686    /// Default Lua-call frame: pc=0, no extra args. The C-frame fields are
687    /// benignly zeroed to exactly the values `c_default` produced, so a Lua
688    /// frame that is later reclassified or read as a C frame sees identical
689    /// defaults to the pre-flatten enum (which carried no C fields). The
690    /// `trap` flag now lives in callstatus bit `CIST_TRAP`, cleared by the
691    /// `callstatus` write in `prep_call_info`, not here.
692    pub fn lua_default() -> Self {
693        CallInfoFrame {
694            savedpc: 0,
695            nextraargs: 0,
696            k: None,
697            old_errfunc: 0,
698            ctx: 0,
699        }
700    }
701}
702
703impl Default for CallInfo {
704    fn default() -> Self {
705        CallInfo {
706            func: StackIdx(0),
707            top: StackIdx(0),
708            previous: None,
709            next: None,
710            u: CallInfoFrame::c_default(),
711            u2: CallInfoExtra::default(),
712            nresults: 0,
713            callstatus: 0,
714            call_metamethods: 0,
715            tailcalls: 0,
716        }
717    }
718}
719
720impl CallInfo {
721    pub fn is_lua(&self) -> bool {
722        (self.callstatus & CIST_C) == 0
723    }
724    pub fn is_lua_code(&self) -> bool {
725        self.is_lua()
726    }
727    /// Whether the active function is a vararg function.
728    ///
729    /// Currently returns `false` unconditionally — vararg introspection via
730    /// `debug.getinfo` reports no vararg info instead of panicking.
731    ///
732    /// TODO(port): wire when CallInfo carries proto access for vararg detection.
733    pub fn is_vararg_func(&self) -> bool {
734        false
735    }
736    /// Index of the next bytecode instruction on this Lua frame.
737    ///
738    /// `debug_assert!` guards that the frame is a Lua frame, restoring the
739    /// wrong-variant tripwire the pre-flatten enum gave for free.
740    pub fn saved_pc(&self) -> u32 {
741        debug_assert!(self.is_lua(), "saved_pc on a C frame");
742        self.u.savedpc
743    }
744    /// Write the next-instruction index on this Lua frame.
745    pub fn set_saved_pc(&mut self, pc: u32) {
746        debug_assert!(self.is_lua(), "set_saved_pc on a C frame");
747        self.u.savedpc = pc;
748    }
749    /// Count of extra varargs collected at entry on this Lua frame.
750    pub fn nextra_args(&self) -> i32 {
751        debug_assert!(self.is_lua(), "nextra_args on a C frame");
752        self.u.nextraargs
753    }
754    /// Write the extra-vararg count on this Lua frame.
755    pub fn set_nextra_args(&mut self, n: i32) {
756        debug_assert!(self.is_lua(), "set_nextra_args on a C frame");
757        self.u.nextraargs = n;
758    }
759    pub fn transfer_ftransfer(&self) -> u16 {
760        self.u2.ftransfer
761    }
762    pub fn transfer_ntransfer(&self) -> u16 {
763        self.u2.ntransfer
764    }
765    /// Set or clear the per-frame hook trap, stored in callstatus bit
766    /// `CIST_TRAP` (T2-C2 moved it out of the `CallInfoFrame` payload). Only
767    /// Lua frames are ever trapped: `set_traps` guards on `is_lua()` before
768    /// calling here, and `trace_call`/`trace_exec` operate on the current Lua
769    /// frame. The `debug_assert!` preserves that invariant. Reusing a slot for
770    /// a new call clears the bit via the `callstatus = mask` write in
771    /// `prep_call_info`, exactly as the old full-variant store reset `trap`.
772    pub fn set_trap(&mut self, t: bool) {
773        debug_assert!(self.is_lua(), "set_trap on a C frame");
774        if t {
775            self.callstatus |= CIST_TRAP;
776        } else {
777            self.callstatus &= !CIST_TRAP;
778        }
779    }
780    /// Read the per-frame hook trap from callstatus bit `CIST_TRAP`. One mask,
781    /// no enum-discriminant branch — this is the hot read in the OP_CALL /
782    /// OP_RETURN `updatetrap` paths.
783    #[inline(always)]
784    pub fn trap(&self) -> bool {
785        (self.callstatus & CIST_TRAP) != 0
786    }
787    /// Read the 3-bit recover-status field packed into bits 10-12 of callstatus.
788    ///
789    pub fn recover_status(&self) -> i32 {
790        ((self.callstatus >> CIST_RECST) & 7) as i32
791    }
792    /// Write the 3-bit recover-status field. `status` must fit in three bits.
793    ///
794    pub fn set_recover_status<T: Into<i32>>(&mut self, status: T) {
795        let st = (status.into() & 7) as u16;
796        self.callstatus = (self.callstatus & !(7u16 << CIST_RECST)) | (st << CIST_RECST);
797    }
798    pub fn get_oah(&self) -> bool {
799        (self.callstatus & CIST_OAH) != 0
800    }
801    /// Store the current `allowhook` value into callstatus bit 0 (CIST_OAH).
802    ///
803    pub fn set_oah(&mut self, allow: bool) {
804        self.callstatus = (self.callstatus & !CIST_OAH) | (if allow { CIST_OAH } else { 0 });
805    }
806    /// Saved `errfunc` to restore on pcall recovery (C-call frame).
807    ///
808    /// `debug_assert!` guards that the frame is a C frame, restoring the
809    /// wrong-variant tripwire the pre-flatten enum gave for free.
810    pub fn u_c_old_errfunc(&self) -> isize {
811        debug_assert!(!self.is_lua(), "u_c_old_errfunc on a Lua frame");
812        self.u.old_errfunc
813    }
814    /// Continuation context on a C-call frame.
815    pub fn u_c_ctx(&self) -> isize {
816        debug_assert!(!self.is_lua(), "u_c_ctx on a Lua frame");
817        self.u.ctx
818    }
819    /// Continuation function on a C-call frame.
820    pub fn u_c_k(&self) -> Option<LuaKFunction> {
821        debug_assert!(!self.is_lua(), "u_c_k on a Lua frame");
822        self.u.k
823    }
824    /// Set continuation function on a C-call frame.
825    ///
826    /// `debug_assert!` guards that the frame is a C frame (callers reach here
827    /// from `lua_callk`/`lua_pcallk`, where `L->ci` is the calling C builtin's
828    /// frame).
829    pub fn set_u_c_k(&mut self, k: Option<LuaKFunction>) {
830        debug_assert!(!self.is_lua(), "set_u_c_k on a Lua frame");
831        self.u.k = k;
832    }
833    /// Set continuation context on a C-call frame.
834    pub fn set_u_c_ctx(&mut self, ctx: isize) {
835        debug_assert!(!self.is_lua(), "set_u_c_ctx on a Lua frame");
836        self.u.ctx = ctx;
837    }
838    /// Set saved old_errfunc on a C-call frame.
839    pub fn set_u_c_old_errfunc(&mut self, old_errfunc: isize) {
840        debug_assert!(!self.is_lua(), "set_u_c_old_errfunc on a Lua frame");
841        self.u.old_errfunc = old_errfunc;
842    }
843    /// Set the `u2.funcidx` field, used by yieldable pcall for error recovery.
844    ///
845    pub fn set_u2_funcidx(&mut self, idx: i32) {
846        self.u2.value = idx;
847    }
848}
849
850// ─── Phase-B value/proto/instruction helpers ──────────────────────────────────
851
852/// Extension methods on `LuaValue`. TODO(phase-b): move these to
853/// `lua_types::value` (or wherever the canonical impl lives) once the type
854/// helpers stabilise.
855pub trait LuaValueExt {
856    fn base_type(&self) -> lua_types::LuaType;
857    fn to_number_no_strconv(&self) -> Option<f64>;
858    fn to_number_with_strconv(&self) -> Option<f64>;
859    fn to_integer_no_strconv(&self) -> Option<i64>;
860    fn to_integer_with_strconv(&self) -> Option<i64>;
861    fn full_type_tag(&self) -> u8;
862}
863
864impl LuaValueExt for LuaValue {
865    fn base_type(&self) -> lua_types::LuaType {
866        self.type_tag()
867    }
868    fn to_number_no_strconv(&self) -> Option<f64> {
869        match self {
870            LuaValue::Float(f) => Some(*f),
871            LuaValue::Int(i) => Some(*i as f64),
872            _ => None,
873        }
874    }
875    fn to_number_with_strconv(&self) -> Option<f64> {
876        if let Some(n) = self.to_number_no_strconv() {
877            return Some(n);
878        }
879        if let LuaValue::Str(s) = self {
880            let mut tmp = LuaValue::Nil;
881            let sz = crate::object::str2num(s.as_bytes(), &mut tmp);
882            if sz == 0 {
883                return None;
884            }
885            return match tmp {
886                LuaValue::Int(i) => Some(i as f64),
887                LuaValue::Float(f) => Some(f),
888                _ => None,
889            };
890        }
891        None
892    }
893    fn to_integer_no_strconv(&self) -> Option<i64> {
894        match self {
895            LuaValue::Int(i) => Some(*i),
896            LuaValue::Float(f) if f.fract() == 0.0 && f.is_finite() => {
897                //   d >= LUA_MININTEGER && d < -(lua_Number)LUA_MININTEGER.
898                // Without this, Rust's `as i64` saturates and silently
899                // produces i64::MAX / i64::MIN for out-of-range floats.
900                let min_f = i64::MIN as f64;
901                let max_plus1_f = -(i64::MIN as f64);
902                if *f >= min_f && *f < max_plus1_f {
903                    Some(*f as i64)
904                } else {
905                    None
906                }
907            }
908            _ => None,
909        }
910    }
911    fn to_integer_with_strconv(&self) -> Option<i64> {
912        if let Some(i) = self.to_integer_no_strconv() {
913            return Some(i);
914        }
915        if let LuaValue::Str(s) = self {
916            let mut tmp = LuaValue::Nil;
917            let sz = crate::object::str2num(s.as_bytes(), &mut tmp);
918            if sz == 0 {
919                return None;
920            }
921            return tmp.to_integer_no_strconv();
922        }
923        None
924    }
925    fn full_type_tag(&self) -> u8 {
926        match self {
927            LuaValue::Nil => 0x00,
928            LuaValue::Bool(false) => 0x01,
929            LuaValue::Bool(true) => 0x11,
930            LuaValue::Int(_) => 0x03,
931            LuaValue::Float(_) => 0x13,
932            LuaValue::Str(s) if s.is_short() => 0x04,
933            LuaValue::Str(_) => 0x14,
934            LuaValue::LightUserData(_) => 0x02,
935            LuaValue::Table(_) => 0x05,
936            LuaValue::Function(LuaClosure::Lua(_)) => 0x06,
937            LuaValue::Function(LuaClosure::LightC(_)) => 0x16,
938            LuaValue::Function(LuaClosure::C(_)) => 0x26,
939            LuaValue::UserData(_) => 0x07,
940            LuaValue::Thread(_) => 0x08,
941        }
942    }
943}
944
945/// Extension methods on `lua_types::LuaType`.
946pub trait LuaTypeExt {
947    fn type_name(&self) -> &'static [u8];
948}
949
950impl LuaTypeExt for lua_types::LuaType {
951    fn type_name(&self) -> &'static [u8] {
952        use lua_types::LuaType::*;
953        match self {
954            None => b"no value",
955            Nil => b"nil",
956            Boolean => b"boolean",
957            LightUserData => b"userdata",
958            Number => b"number",
959            String => b"string",
960            Table => b"table",
961            Function => b"function",
962            UserData => b"userdata",
963            Thread => b"thread",
964        }
965    }
966}
967
968/// StackIdx checked-arithmetic helpers. Returns the raw `u32` because Phase A
969/// callers use the result in arithmetic comparisons against other `u32`
970/// quantities (stack-distance offsets).
971pub trait StackIdxExt {
972    fn saturating_sub(self, n: impl Into<StackIdxConv>) -> u32;
973    fn wrapping_sub(self, n: impl Into<StackIdxConv>) -> u32;
974    fn raw(self) -> u32;
975}
976impl StackIdxExt for StackIdx {
977    #[inline(always)]
978    fn saturating_sub(self, n: impl Into<StackIdxConv>) -> u32 {
979        self.0.saturating_sub(n.into().0 .0)
980    }
981    #[inline(always)]
982    fn wrapping_sub(self, n: impl Into<StackIdxConv>) -> u32 {
983        self.0.wrapping_sub(n.into().0 .0)
984    }
985    #[inline(always)]
986    fn raw(self) -> u32 {
987        self.0
988    }
989}
990
991/// `GcRef<LuaTable>` / `GcRef<LuaUserData>` field-access helpers. These
992/// methods are needed by api.rs and tagmethods.rs but the lua-types
993/// placeholders don't yet expose them. TODO(phase-b): replace with real
994/// accessor methods on the canonical types in lua-types.
995///
996/// PORT NOTE: the historical `reject_invalid_table_key` precheck used to
997/// guard nil/NaN keys at this layer; it has moved inside
998/// [`LuaTable::try_raw_set`] (alongside the integer-fast-path match) so
999/// the lua-vm wrapper does not double-check.
1000pub trait LuaTableRefExt {
1001    fn metatable(&self) -> Option<GcRef<LuaTable>>;
1002    fn has_metatable(&self) -> bool;
1003    fn as_ptr(&self) -> *const ();
1004    fn get(&self, _k: &LuaValue) -> LuaValue;
1005    fn get_int(&self, _k: i64) -> LuaValue;
1006    fn get_short_str(&self, _k: &GcRef<LuaString>) -> LuaValue;
1007    fn raw_set(&self, _state: &mut LuaState, _k: LuaValue, _v: LuaValue) -> Result<(), LuaError>;
1008    fn raw_set_int(&self, _state: &mut LuaState, _k: i64, _v: LuaValue) -> Result<(), LuaError>;
1009    fn raw_set_short_str(
1010        &self,
1011        _state: &mut LuaState,
1012        _k: GcRef<LuaString>,
1013        _v: LuaValue,
1014    ) -> Result<(), LuaError>;
1015    fn invalidate_tm_cache(&self);
1016    fn resize(&self, _state: &mut LuaState, _na: usize, _nh: usize) -> Result<(), LuaError>;
1017    fn next(&self, _k: LuaValue) -> Result<Option<(LuaValue, LuaValue)>, LuaError>;
1018}
1019impl LuaTableRefExt for GcRef<LuaTable> {
1020    #[inline]
1021    fn metatable(&self) -> Option<GcRef<LuaTable>> {
1022        (**self).metatable()
1023    }
1024    #[inline]
1025    fn has_metatable(&self) -> bool {
1026        (**self).has_metatable()
1027    }
1028    #[inline]
1029    fn as_ptr(&self) -> *const () {
1030        GcRef::identity(self) as *const ()
1031    }
1032    #[inline]
1033    fn get(&self, k: &LuaValue) -> LuaValue {
1034        (**self).get(k)
1035    }
1036    #[inline]
1037    fn get_int(&self, k: i64) -> LuaValue {
1038        (**self).get_int(k)
1039    }
1040    #[inline]
1041    fn get_short_str(&self, k: &GcRef<LuaString>) -> LuaValue {
1042        (**self).get_short_str(k)
1043    }
1044    /// Forwards to [`LuaTable::try_raw_set`], which performs the nil/NaN
1045    /// key validation internally as part of its integer-fast-path match.
1046    ///
1047    /// A collectable KEY stored into the table needs its own backward
1048    /// barrier, mirroring C-Lua's `luaC_barrierback(L, obj2gco(t), key)`
1049    /// inside `luaH_newkey` (ltable.c:717). Without it a young key inserted
1050    /// into an old table is invisible to the generational collector and is
1051    /// freed while still referenced — the 2026-06-09 table livelock.
1052    #[inline(always)]
1053    fn raw_set(&self, state: &mut LuaState, k: LuaValue, v: LuaValue) -> Result<(), LuaError> {
1054        match k {
1055            LuaValue::Int(i) => return self.raw_set_int(state, i, v),
1056            LuaValue::Str(s) if s.is_short() => return self.raw_set_short_str(state, s, v),
1057            k => {
1058                if k.is_collectable() {
1059                    state.gc_table_barrier_back(self, &k);
1060                }
1061                let before = (**self).buffer_bytes();
1062                let result = (**self).try_raw_set(k, v);
1063                if result.is_ok() {
1064                    account_table_buffer_delta(self, before);
1065                }
1066                result
1067            }
1068        }
1069    }
1070    #[inline(always)]
1071    fn raw_set_int(&self, _state: &mut LuaState, k: i64, v: LuaValue) -> Result<(), LuaError> {
1072        match (**self).try_update_int(k, v) {
1073            Ok(()) => Ok(()),
1074            Err(v) => {
1075                let before = (**self).buffer_bytes();
1076                let result = (**self).try_raw_set_int(k, v);
1077                if result.is_ok() {
1078                    account_table_buffer_delta(self, before);
1079                }
1080                result
1081            }
1082        }
1083    }
1084    /// The miss arm inserts a NEW short-string key, so the key itself is
1085    /// barriered (C-Lua does this inside `luaH_newkey`, ltable.c:717); the
1086    /// hit arm only overwrites a value whose key is already traced.
1087    #[inline(always)]
1088    fn raw_set_short_str(
1089        &self,
1090        state: &mut LuaState,
1091        k: GcRef<LuaString>,
1092        v: LuaValue,
1093    ) -> Result<(), LuaError> {
1094        match (**self).try_update_short_str(&k, v) {
1095            Ok(()) => Ok(()),
1096            Err(v) => {
1097                state.gc_table_barrier_back(self, &LuaValue::Str(k));
1098                let before = (**self).buffer_bytes();
1099                let result = (**self).try_raw_set(LuaValue::Str(k), v);
1100                if result.is_ok() {
1101                    account_table_buffer_delta(self, before);
1102                }
1103                result
1104            }
1105        }
1106    }
1107    fn invalidate_tm_cache(&self) {}
1108    fn resize(&self, _state: &mut LuaState, na: usize, nh: usize) -> Result<(), LuaError> {
1109        let before = (**self).buffer_bytes();
1110        let na32 = na.min(u32::MAX as usize) as u32;
1111        let nh32 = nh.min(u32::MAX as usize) as u32;
1112        let result = (**self).resize(na32, nh32);
1113        if result.is_ok() {
1114            account_table_buffer_delta(self, before);
1115        }
1116        result
1117    }
1118    fn next(&self, k: LuaValue) -> Result<Option<(LuaValue, LuaValue)>, LuaError> {
1119        (**self).try_next_pair(&k)
1120    }
1121}
1122
1123#[inline]
1124fn account_table_buffer_delta(t: &GcRef<LuaTable>, before: usize) {
1125    let after = (**t).buffer_bytes();
1126    if after > before {
1127        t.account_buffer((after - before) as isize);
1128    } else if before > after {
1129        t.account_buffer(-((before - after) as isize));
1130    }
1131}
1132
1133pub trait LuaUserDataRefExt {
1134    fn metatable(&self) -> Option<GcRef<LuaTable>>;
1135    fn set_metatable(&self, mt: Option<GcRef<LuaTable>>);
1136    fn as_ptr(&self) -> *const ();
1137    fn len(&self) -> usize;
1138}
1139impl LuaUserDataRefExt for GcRef<LuaUserData> {
1140    fn metatable(&self) -> Option<GcRef<LuaTable>> {
1141        (**self).metatable()
1142    }
1143    fn set_metatable(&self, mt: Option<GcRef<LuaTable>>) {
1144        (**self).set_metatable(mt);
1145    }
1146    fn as_ptr(&self) -> *const () {
1147        GcRef::identity(self) as *const ()
1148    }
1149    fn len(&self) -> usize {
1150        self.0.data.len()
1151    }
1152}
1153
1154pub trait LuaStringRefExt {
1155    fn is_white(&self) -> bool;
1156    fn hash(&self) -> u32;
1157    fn as_gc_ref(&self) -> GcRef<LuaString>;
1158}
1159impl LuaStringRefExt for GcRef<LuaString> {
1160    fn is_white(&self) -> bool {
1161        false
1162    }
1163    fn hash(&self) -> u32 {
1164        self.0.hash()
1165    }
1166    fn as_gc_ref(&self) -> GcRef<LuaString> {
1167        self.clone()
1168    }
1169}
1170
1171pub trait LuaLClosureRefExt {
1172    fn proto(&self) -> &GcRef<LuaProto>;
1173    fn nupvalues(&self) -> usize;
1174}
1175impl LuaLClosureRefExt for GcRef<lua_types::closure::LuaLClosure> {
1176    fn proto(&self) -> &GcRef<LuaProto> {
1177        &self.0.proto
1178    }
1179    fn nupvalues(&self) -> usize {
1180        self.0.upvals.len()
1181    }
1182}
1183
1184/// `LuaClosure` accessor — `nupvalues()` reports the upvalue count uniformly.
1185pub trait LuaClosureExt {
1186    fn nupvalues(&self) -> usize;
1187}
1188impl LuaClosureExt for LuaClosure {
1189    fn nupvalues(&self) -> usize {
1190        match self {
1191            LuaClosure::Lua(l) => l.0.upvals.len(),
1192            LuaClosure::C(c) => c.0.upvalues.borrow().len(),
1193            LuaClosure::LightC(_) => 0,
1194        }
1195    }
1196}
1197
1198/// `LuaProto` source bytes accessor.
1199pub trait LuaProtoExt {
1200    fn source_bytes(&self) -> &[u8];
1201    fn source_string(&self) -> Option<&GcRef<LuaString>>;
1202}
1203impl LuaProtoExt for LuaProto {
1204    fn source_bytes(&self) -> &[u8] {
1205        match &self.source {
1206            Some(s) => s.0.as_bytes(),
1207            None => &[],
1208        }
1209    }
1210    fn source_string(&self) -> Option<&GcRef<LuaString>> {
1211        self.source.as_ref()
1212    }
1213}
1214
1215// ─── Collectable trait (GC interface) ────────────────────────────────────────
1216
1217/// Marker trait for GC-managed objects.
1218///
1219/// Phase D: real tracing GC.
1220/// types.tsv: `GCObject → (trait Collectable; concrete = GcRef<T>)`
1221pub trait Collectable: std::fmt::Debug {}
1222
1223impl std::fmt::Debug for LuaState {
1224    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1225        write!(f, "LuaState")
1226    }
1227}
1228impl Collectable for LuaState {}
1229
1230// ─── GlobalState ─────────────────────────────────────────────────────────────
1231
1232/// Function-pointer signature for the text-source parser, installed on
1233/// [`GlobalState::parser_hook`] by the embedder.
1234///
1235/// The implementation lives in `lua-parse`; `lua-vm` cannot depend on it
1236/// directly (that would form a cycle), so the parser is reached via this
1237/// function pointer registered at startup.
1238///
1239/// The parser receives the live [`ZIO`](crate::zio::ZIO) (already positioned
1240/// past `firstchar`) rather than a fully-materialised buffer, so it can pull
1241/// reader chunks on demand and stop at the first syntax error — matching C's
1242/// `lua_load`, where the lexer drives the `lua_Reader` byte by byte.
1243pub type ParserHook = fn(
1244    state: &mut LuaState,
1245    z: &mut crate::zio::ZIO,
1246    name: &[u8],
1247    firstchar: i32,
1248) -> Result<GcRef<lua_types::closure::LuaLClosure>, LuaError>;
1249
1250/// Function-pointer signature for reading a file's full contents into memory,
1251/// installed on [`GlobalState::file_loader_hook`] by the embedder.
1252///
1253/// `std::fs` is banned outside `lua-cli`, so `lua-stdlib`'s `loadfile` and
1254/// `searcher_lua` reach the filesystem via this hook. `None` keeps the file
1255/// system unreachable, which is appropriate for embeddings where modules are
1256/// served exclusively from `package.preload`.
1257pub type FileLoaderHook = fn(filename: &[u8]) -> Result<Vec<u8>, LuaError>;
1258
1259/// Function-pointer signature for opening a file handle, installed on
1260/// [`GlobalState::file_open_hook`] by the embedder.
1261///
1262/// `std::fs` is banned outside `lua-cli`, so `lua-stdlib`'s io library reaches
1263/// the filesystem via this hook. `None` causes `io.open` and `io.output(name)`
1264/// to return a "file system not available" error, which is appropriate for
1265/// sandboxed embeddings.
1266///
1267/// `mode` is a Lua fopen-style mode string (e.g. `b"r"`, `b"w"`, `b"a"`,
1268/// `b"r+"`, etc.). The hook must honour at least `r`, `w`, and `a`.
1269pub type FileOpenHook =
1270    fn(filename: &[u8], mode: &[u8]) -> Result<Box<dyn lua_types::LuaFileHandle>, LuaError>;
1271
1272/// Function-pointer signature for writing bytes to a host-provided output
1273/// stream, installed on [`GlobalState::stdout_hook`] or
1274/// [`GlobalState::stderr_hook`] by the embedder.
1275///
1276/// Bare `wasm32-unknown-unknown` has no ambient stdout/stderr. Keeping output
1277/// behind explicit hooks lets sandboxed and WASM hosts decide whether output is
1278/// unavailable, buffered, or bridged to something like a browser console.
1279pub type OutputHook = fn(bytes: &[u8]) -> std::io::Result<()>;
1280
1281/// Function-pointer signature for reading bytes from a host-provided input
1282/// stream, installed on [`GlobalState::stdin_hook`] by the embedder.
1283pub type InputHook = fn(buf: &mut [u8]) -> std::io::Result<usize>;
1284
1285/// Function-pointer signature for reading a host environment variable.
1286///
1287/// Returning `None` maps naturally to Lua's `os.getenv` result for a missing
1288/// variable and is also the sandbox/bare-WASM default when no environment is
1289/// exposed.
1290pub type EnvHook = fn(name: &[u8]) -> Option<Vec<u8>>;
1291
1292/// Function-pointer signature for retrieving the current Unix time in seconds.
1293pub type UnixTimeHook = fn() -> i64;
1294
1295/// Function-pointer signature for retrieving program CPU time in seconds.
1296///
1297/// Backs `os.clock`. C's `clock()` reads `CLOCK_PROCESS_CPUTIME_ID`, which has no
1298/// `std` equivalent and is unavailable on bare WASM; the stdlib falls back to a
1299/// monotonic wall-clock baseline (matching wasi-libc/Emscripten's emulation) when
1300/// this hook is unset. A host wanting true CPU time can install one (e.g. via the
1301/// `cpu-time` crate) without changing the sandboxed crates.
1302pub type CpuClockHook = fn() -> f64;
1303
1304/// Function-pointer signature for the host's local timezone offset.
1305///
1306/// Given a Unix timestamp (seconds, UTC), returns the offset in seconds that the
1307/// host's local timezone applies at that instant, such that
1308/// `local_broken_down = gmtime(timestamp + offset)`. Positive east of UTC (e.g.
1309/// `+3600` for CET), negative west (e.g. `-14400` for US EDT). This backs the
1310/// local-time semantics of `os.date` (non-`!` formats) and `os.time`, which C
1311/// implements with `localtime_r`/`mktime`. Reading the host timezone database
1312/// requires `libc` FFI (`unsafe`), banned in `lua-stdlib`, so the host installs
1313/// this hook. When unset the stdlib uses UTC (offset 0), keeping the
1314/// `os.date`/`os.time` round-trip exact on hosts without a timezone.
1315pub type LocalOffsetHook = fn(timestamp: i64) -> i64;
1316
1317/// Function-pointer signature for host entropy used by default PRNG seeds and
1318/// table-sort pivot randomisation. Hosts without entropy may leave it unset; the
1319/// stdlib then uses deterministic fallback values instead of touching OS stubs.
1320pub type EntropyHook = fn() -> u64;
1321
1322/// Function-pointer signature for generating a host temporary filename.
1323///
1324/// Used by `os.tmpname` and `io.tmpfile`. The hook should return a path-like byte
1325/// string that the host's `file_open_hook` can understand.
1326pub type TempNameHook = fn() -> Result<Vec<u8>, LuaError>;
1327
1328/// Function-pointer signature for spawning a child process with a connected
1329/// pipe, installed on [`GlobalState::popen_hook`] by the embedder.
1330///
1331/// `std::process::Command` is banned outside `lua-cli`, so `lua-stdlib`'s
1332/// `io.popen` reaches the OS through this hook. `None` causes `io.popen` to
1333/// raise a clean Lua error ("popen not enabled in this build"), which is
1334/// appropriate for sandboxed embeddings.
1335///
1336/// `mode` is the Lua popen mode string — `b"r"` for reading the child's
1337/// stdout, `b"w"` for writing to the child's stdin.
1338pub type PopenHook =
1339    fn(cmd: &[u8], mode: &[u8]) -> Result<Box<dyn lua_types::LuaFileHandle>, LuaError>;
1340
1341/// Function-pointer signature for removing a file, installed on
1342/// [`GlobalState::file_remove_hook`] by the embedder.
1343///
1344/// `std::fs` is banned outside `lua-cli`, so `lua-stdlib`'s `os.remove`
1345/// reaches the filesystem via this hook. Returns `Ok(())` on success.
1346pub type FileRemoveHook = fn(filename: &[u8]) -> Result<(), LuaError>;
1347
1348/// Function-pointer signature for renaming a file, installed on
1349/// [`GlobalState::file_rename_hook`] by the embedder.
1350///
1351/// `std::fs` is banned outside `lua-cli`, so `lua-stdlib`'s `os.rename`
1352/// reaches the filesystem via this hook. Returns `Ok(())` on success.
1353pub type FileRenameHook = fn(from: &[u8], to: &[u8]) -> Result<(), LuaError>;
1354
1355/// Reason a shell command terminated, returned by [`OsExecuteHook`].
1356///
1357/// Mirrors the two string literals that C-Lua's `l_inspectstat` / `luaL_execresult`
1358/// can produce: `"exit"` for normal process exit, `"signal"` for signal termination
1359/// (POSIX only).
1360#[derive(Clone, Copy, Debug)]
1361pub enum OsExecuteReason {
1362    /// Process exited with an exit code (`WIFEXITED` / `ExitStatus::code()` is `Some`).
1363    Exit,
1364    /// Process was terminated by a signal (`WIFSIGNALED` / `ExitStatus::signal()` is `Some`).
1365    Signal,
1366}
1367
1368/// Result returned by [`OsExecuteHook`], carrying the three values that
1369/// C-Lua's `luaL_execresult` pushes: `(boolean|nil, "exit"|"signal", int)`.
1370#[derive(Debug)]
1371pub struct OsExecuteResult {
1372    /// `true` when the command exited successfully (exit code 0).
1373    pub success: bool,
1374    /// How the process terminated.
1375    pub reason: OsExecuteReason,
1376    /// Exit code (for `Exit`) or signal number (for `Signal`).
1377    pub code: i32,
1378}
1379
1380/// Function-pointer signature for executing a shell command, installed on
1381/// [`GlobalState::os_execute_hook`] by the embedder.
1382///
1383/// `std::process` is banned outside `lua-cli`, so `lua-stdlib`'s `os.execute`
1384/// reaches the shell via this hook. Returns an [`OsExecuteResult`] on success,
1385/// or a [`LuaError`] when the spawn itself fails.
1386pub type OsExecuteHook = fn(cmd: &[u8]) -> Result<OsExecuteResult, LuaError>;
1387
1388/// Opaque handle to a dynamically loaded library, allocated by a
1389/// [`DynLibLoadHook`] backend and stored in `package._CLIBS`.
1390///
1391/// The handle is a backend-owned `u64`; the embedder is free to use it as an
1392/// index into a `Vec<libloading::Library>` or a `HashMap` key. `lua-stdlib`
1393/// stores the value verbatim and never inspects it.
1394#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1395pub struct DynLibId(pub u64);
1396
1397/// Resolved dynamic-library symbol.
1398///
1399/// Only `RustNative` is callable by this build of the VM. `LuaCAbi` resolves
1400/// to a real C function pointer compiled against stock Lua 5.4's `lua_State *`
1401/// ABI but cannot be safely invoked here — it is reported as an `"init"`
1402/// failure with a clear message. `Unsupported` carries an embedder-provided
1403/// reason byte-string.
1404pub enum DynamicSymbol {
1405    /// Function pointer that follows this build's Rust-native module ABI:
1406    /// `fn(&mut LuaState) -> Result<usize, LuaError>`.
1407    RustNative(LuaCFunction),
1408    /// Symbol exported against stock Lua 5.4's C ABI. The function pointer is
1409    /// resolved but never called from this build, since `lua_State *` is not
1410    /// our `LuaState`. Kept as a payload so a future C-ABI facade can pick it
1411    /// up; the embedder is responsible for ensuring the underlying library
1412    /// outlives this value.
1413    LuaCAbi(*const ()),
1414    /// Embedder-provided refusal reason, e.g. "symbol resolved but ABI version
1415    /// mismatch". Reported verbatim as an `"init"` failure.
1416    Unsupported { reason: Vec<u8> },
1417}
1418
1419/// Function-pointer signature for loading a dynamic library, installed on
1420/// [`GlobalState::dynlib_load_hook`] by the embedder.
1421///
1422/// `libloading`/`dlopen`/`LoadLibraryEx` are FFI calls and require `unsafe`,
1423/// which is banned in `lua-stdlib`. `lua-cli` installs a `libloading`-backed
1424/// implementation. `None` causes `package.loadlib` to return the C-Lua
1425/// `"absent"` failure shape, matching the fallback platform stub.
1426///
1427/// `see_global` mirrors C-Lua's `seeglb` (POSIX `RTLD_GLOBAL`): set when the
1428/// caller invokes `package.loadlib(path, "*")`.
1429pub type DynLibLoadHook =
1430    fn(state: &mut LuaState, path: &[u8], see_global: bool) -> Result<DynLibId, LuaError>;
1431
1432/// Function-pointer signature for resolving a symbol in a previously loaded
1433/// dynamic library, installed on [`GlobalState::dynlib_symbol_hook`].
1434///
1435/// The hook receives the [`DynLibId`] returned by [`DynLibLoadHook`] and the
1436/// requested symbol name. Returning `DynamicSymbol::RustNative` makes the
1437/// symbol callable; `LuaCAbi`/`Unsupported` propagate to `package.loadlib`
1438/// as an `"init"` failure with a clear message.
1439pub type DynLibSymbolHook =
1440    fn(state: &mut LuaState, handle: DynLibId, symbol: &[u8]) -> Result<DynamicSymbol, LuaError>;
1441
1442/// Function-pointer signature for unloading a dynamic library, installed on
1443/// [`GlobalState::dynlib_unload_hook`].
1444///
1445/// Called from the `_CLIBS` `__gc` metamethod when the Lua state closes.
1446/// `libloading`'s safety model requires every loaded library to outlive the
1447/// last symbol it exports; the CLI backend is therefore free to ignore this
1448/// hook and keep libraries alive until process exit.
1449pub type DynLibUnloadHook = fn(handle: DynLibId);
1450
1451/// One row of [`GlobalState::threads`]. Pairs the per-thread `LuaState`
1452/// with the canonical `GcRef<LuaThread>` so every `push_thread` for the
1453/// same id shares pointer-identity. Phase E-1 adds this; Phase E-2
1454/// extends it with interior-mutability bookkeeping when `resume`/`yield`
1455/// need to mutate the child thread while the parent holds a borrow.
1456pub struct ThreadRegistryEntry {
1457    /// The owned coroutine `LuaState`. Wrapped in `Rc<RefCell<...>>` so
1458    /// that `coroutine.resume` can borrow the child mutably while the
1459    /// parent is still in scope. Single-threaded — borrows never overlap
1460    /// in practice because only one resume path is live at a time.
1461    pub state: Rc<RefCell<LuaState>>,
1462    /// Canonical thread-value handle. Reused on every push so
1463    /// `GcRef::ptr_eq` is true across pushes.
1464    pub value: GcRef<lua_types::value::LuaThread>,
1465}
1466
1467/// Stable key for a value pinned in [`ExternalRootSet`].
1468///
1469/// The generation is part of the key so a handle that has already unrooted its
1470/// slot cannot accidentally observe a later handle's value after slot reuse.
1471#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1472pub struct ExternalRootKey {
1473    index: usize,
1474    generation: u64,
1475}
1476
1477#[derive(Debug)]
1478struct ExternalRootSlot {
1479    value: Option<LuaValue>,
1480    generation: u64,
1481}
1482
1483/// Values held alive by external Rust handles.
1484///
1485/// This is the embedding API's GC anchor. It intentionally lives directly on
1486/// `GlobalState` instead of inside the Lua registry table: handle drop/unroot
1487/// must be cheap, infallible, and independent of the Lua stack protocol.
1488#[derive(Debug, Default)]
1489pub struct ExternalRootSet {
1490    slots: Vec<ExternalRootSlot>,
1491    free: Vec<usize>,
1492    live: usize,
1493}
1494
1495impl ExternalRootSet {
1496    pub fn insert(&mut self, value: LuaValue) -> ExternalRootKey {
1497        if let Some(index) = self.free.pop() {
1498            let slot = &mut self.slots[index];
1499            debug_assert!(slot.value.is_none(), "free external-root slot is occupied");
1500            slot.generation = slot.generation.wrapping_add(1).max(1);
1501            slot.value = Some(value);
1502            self.live += 1;
1503            ExternalRootKey {
1504                index,
1505                generation: slot.generation,
1506            }
1507        } else {
1508            let index = self.slots.len();
1509            self.slots.push(ExternalRootSlot {
1510                value: Some(value),
1511                generation: 1,
1512            });
1513            self.live += 1;
1514            ExternalRootKey {
1515                index,
1516                generation: 1,
1517            }
1518        }
1519    }
1520
1521    pub fn get(&self, key: ExternalRootKey) -> Option<&LuaValue> {
1522        let slot = self.slots.get(key.index)?;
1523        if slot.generation == key.generation {
1524            slot.value.as_ref()
1525        } else {
1526            None
1527        }
1528    }
1529
1530    pub fn replace(&mut self, key: ExternalRootKey, value: LuaValue) -> Option<LuaValue> {
1531        let slot = self.slots.get_mut(key.index)?;
1532        if slot.generation != key.generation || slot.value.is_none() {
1533            return None;
1534        }
1535        slot.value.replace(value)
1536    }
1537
1538    pub fn remove(&mut self, key: ExternalRootKey) -> Option<LuaValue> {
1539        let slot = self.slots.get_mut(key.index)?;
1540        if slot.generation != key.generation {
1541            return None;
1542        }
1543        let old = slot.value.take()?;
1544        self.free.push(key.index);
1545        self.live -= 1;
1546        Some(old)
1547    }
1548
1549    pub fn iter_values(&self) -> impl Iterator<Item = &LuaValue> {
1550        self.slots.iter().filter_map(|slot| slot.value.as_ref())
1551    }
1552
1553    pub fn len(&self) -> usize {
1554        self.live
1555    }
1556
1557    pub fn is_empty(&self) -> bool {
1558        self.live == 0
1559    }
1560
1561    pub fn vacant_len(&self) -> usize {
1562        self.free.len()
1563    }
1564}
1565
1566/// Process-wide state shared by all Lua threads.
1567///
1568/// types.tsv: `global_State → GlobalState`
1569///
1570/// Not exposed directly at the API; accessed via `state.global()` / `state.global_mut()`.
1571pub struct GlobalState {
1572    /// Phase-B hook for the Lua text parser. Set by the embedder (`lua-cli`
1573    /// or stdlib host) to bridge the cyclic crate split between `lua-vm` and
1574    /// `lua-parse`: when `f_parser` decides the chunk is text, it invokes
1575    /// this hook instead of the parser stub. `None` leaves the stub in place
1576    /// so unit tests that never load text still work.
1577    pub parser_hook: Option<ParserHook>,
1578
1579    /// Transient slot carrying the CLI's `argv` into the `pmain` C closure.
1580    /// Mirrors `lua.c`'s `lua_pushinteger(argc)/lua_pushlightuserdata(argv)`
1581    /// arguments to `pmain`: a lua-rs C closure cannot capture Rust values, so
1582    /// `lua-cli`'s `run` parks `argv` here, pushes a zero-arg `pmain` closure,
1583    /// and `pcall_k`s it; `pmain` `take()`s it back out. Lives on `GlobalState`
1584    /// to keep `lua-cli` free of `unsafe` light-userdata round-tripping.
1585    pub cli_argv: Option<Vec<Vec<u8>>>,
1586
1587    /// Transient slot carrying the CLI's native-module `preload` callback into
1588    /// the `pmain` C closure, paired with [`GlobalState::cli_argv`]. The type
1589    /// matches `lua-cli::interp::run`'s `preload` parameter.
1590    pub cli_preload: Option<fn(&mut LuaState) -> Result<(), LuaError>>,
1591
1592    /// The Lua language version this state speaks. The single source of truth
1593    /// for version-gated behavior in the layers that read the state (parser,
1594    /// stdlib openers). The embedder sets this from the [`Lua`] instance's
1595    /// [`lua_types::LuaVersion`] at construction; it defaults to
1596    /// [`lua_types::LuaVersion::V54`] so any state built without an explicit
1597    /// version keeps the existing 5.4 behavior unchanged.
1598    pub lua_version: lua_types::LuaVersion,
1599
1600    /// Phase-B hook for reading a Lua source file from disk. Set by `lua-cli`
1601    /// (or any embedder that wants `require`/`loadfile` to reach the file
1602    /// system) since `std::fs` is banned in `lua-stdlib`. `None` makes
1603    /// `loadfile` and the Lua-file searcher report a file-not-found error.
1604    pub file_loader_hook: Option<FileLoaderHook>,
1605
1606    /// Phase-B hook for opening a file handle for read/write/append. Set by
1607    /// `lua-cli` since `std::fs` is banned in `lua-stdlib`. `None` causes
1608    /// `io.open` and `io.output(name)` to return an error; standard output and
1609    /// error are controlled separately through output hooks/native fallbacks.
1610    pub file_open_hook: Option<FileOpenHook>,
1611
1612    /// Hook for host stdout. When absent, native builds fall back to Rust stdout
1613    /// for compatibility; bare `wasm32-unknown-unknown` reports stdout
1614    /// unavailable instead of touching a stubbed stdio implementation.
1615    pub stdout_hook: Option<OutputHook>,
1616
1617    /// Hook for host stderr. See [`GlobalState::stdout_hook`].
1618    pub stderr_hook: Option<OutputHook>,
1619
1620    /// Hook for host stdin. When absent, native builds fall back to Rust stdin
1621    /// for compatibility; bare `wasm32-unknown-unknown` behaves like EOF.
1622    pub stdin_hook: Option<InputHook>,
1623
1624    /// Hook for host environment lookups. `None` makes `os.getenv` return nil.
1625    pub env_hook: Option<EnvHook>,
1626
1627    /// Hook for host wall-clock time. Required for `os.time()` and `os.date()`
1628    /// without an explicit timestamp under bare WASM.
1629    pub unix_time_hook: Option<UnixTimeHook>,
1630
1631    /// Hook for host program CPU time. Backs `os.clock`. When unset, native builds
1632    /// use a monotonic wall-clock baseline and bare WASM reports it unavailable.
1633    pub cpu_clock_hook: Option<CpuClockHook>,
1634
1635    /// Hook for the host's local timezone offset at a given instant. Backs the
1636    /// local-time semantics of `os.date` (non-`!` formats) and `os.time`. When
1637    /// unset, both use UTC, matching the prior behaviour and keeping the
1638    /// `os.date`/`os.time` round-trip exact under bare WASM.
1639    pub local_offset_hook: Option<LocalOffsetHook>,
1640
1641    /// Hook for host entropy. Used by default `math.randomseed` and table sort
1642    /// pivot randomisation; absent hooks fall back to deterministic seeds.
1643    pub entropy_hook: Option<EntropyHook>,
1644
1645    /// Hook for host temporary filenames. Used by `os.tmpname` and `io.tmpfile`.
1646    pub temp_name_hook: Option<TempNameHook>,
1647
1648    /// Phase-G hook for spawning a child process and connecting one stream
1649    /// (stdin or stdout) to a Lua file handle. Set by `lua-cli` since
1650    /// `std::process::Command` is banned in `lua-stdlib`. `None` causes
1651    /// `io.popen` to raise a Lua error rather than panic.
1652    pub popen_hook: Option<PopenHook>,
1653
1654    /// Phase-B hook for removing a file. Set by `lua-cli` since `std::fs` is
1655    /// banned in `lua-stdlib`. `None` causes `os.remove` to return an error.
1656    pub file_remove_hook: Option<FileRemoveHook>,
1657
1658    /// Phase-B hook for renaming a file. Set by `lua-cli` since `std::fs` is
1659    /// banned in `lua-stdlib`. `None` causes `os.rename` to return an error.
1660    pub file_rename_hook: Option<FileRenameHook>,
1661
1662    /// Phase-G hook for executing a shell command. Set by `lua-cli` since
1663    /// `std::process` is banned in `lua-stdlib`. `None` causes `os.execute`
1664    /// to report no shell available (matching C-Lua's `system(NULL) == 0`).
1665    pub os_execute_hook: Option<OsExecuteHook>,
1666
1667    /// Phase-D-3.5 hook for loading a dynamic library (`dlopen` /
1668    /// `LoadLibraryEx`). Set by `lua-cli` since `libloading` is FFI and
1669    /// requires `unsafe`, which is banned in `lua-stdlib`. `None` causes
1670    /// `package.loadlib` to return the `"absent"` fallback shape.
1671    pub dynlib_load_hook: Option<DynLibLoadHook>,
1672
1673    /// Phase-D-3.5 hook for resolving a symbol in a previously loaded
1674    /// dynamic library (`dlsym` / `GetProcAddress`). Set by `lua-cli`.
1675    /// `None` is treated as "absent" by `package.loadlib`.
1676    pub dynlib_symbol_hook: Option<DynLibSymbolHook>,
1677
1678    /// Phase-D-3.5 hook for unloading a dynamic library (`dlclose` /
1679    /// `FreeLibrary`). Set by `lua-cli`. `None` keeps libraries loaded
1680    /// until process exit, which matches `libloading`'s safety model.
1681    pub dynlib_unload_hook: Option<DynLibUnloadHook>,
1682
1683    /// Per-runtime sandbox budget shared across all threads. Inactive by
1684    /// default (`interval == 0`); see [`SandboxLimits`].
1685    pub sandbox: SandboxLimits,
1686
1687    // types.tsv: global_State.GCdebt → isize
1688    pub gc_debt: isize,
1689
1690    pub gc_estimate: usize,
1691
1692    // types.tsv: global_State.lastatomic → usize
1693    pub lastatomic: usize,
1694
1695    // types.tsv: global_State.strt → StringPool
1696    pub strt: StringPool,
1697
1698    // types.tsv: global_State.l_registry → LuaValue
1699    pub l_registry: LuaValue,
1700
1701    /// External Rust handles root their referents here while they are live.
1702    /// Traced from `GlobalState::trace`.
1703    pub external_roots: ExternalRootSet,
1704
1705    // PORT NOTE (phase-b-reconcile): The lua-types LuaTable placeholder has
1706    // no storage, so we cannot persist `registry[LUA_RIDX_GLOBALS] = globals`
1707    // via the canonical registry path. Until the placeholder reconciles with
1708    // lua-vm::table::LuaTable, the globals table lives in a direct field
1709    // and `get_global_table` reads it from here. Same for `loaded` (the
1710    // module cache normally at `registry[_LOADED]`).
1711    pub globals: LuaValue,
1712    pub loaded: LuaValue,
1713
1714    // types.tsv: global_State.nilvalue → LuaValue
1715    // PORT NOTE: In Rust we use a dedicated `is_complete: bool` flag rather than
1716    // the C trick of checking `ttisnil(&g->nilvalue)`. See `is_complete()`.
1717    pub nilvalue: LuaValue,
1718
1719    // types.tsv: global_State.seed → u32
1720    pub seed: u32,
1721
1722    // types.tsv: global_State.currentwhite → u8
1723    pub currentwhite: u8,
1724
1725    pub gcstate: u8,
1726
1727    pub gckind: u8,
1728
1729    pub gcstopem: bool,
1730
1731    // types.tsv: global_State.genminormul → u8
1732    pub genminormul: u8,
1733
1734    pub genmajormul: u8,
1735
1736    pub gcstp: u8,
1737
1738    pub gcemergency: bool,
1739
1740    // types.tsv: global_State.gcpause → u8
1741    pub gcpause: u8,
1742
1743    // types.tsv: global_State.gcstepmul → u8
1744    pub gcstepmul: u8,
1745
1746    pub gcstepsize: u8,
1747
1748    /// Lua 5.5 `collectgarbage("param", name [, value])` storage, indexed by
1749    /// [`Gc55Param`]: `[minormul, majorminor, minormajor, pause, stepmul,
1750    /// stepsize]`. The 5.5 GC parameters use a wider value range than the
1751    /// packed `u8` fields above, so they get their own storage. This is a
1752    /// faithful-shape backing store: it preserves the read-current /
1753    /// write-returns-old contract and the upstream default values, without
1754    /// claiming to retune the incremental collector. Initialized to the
1755    /// values observed on the reference `lua5.5.0` binary.
1756    pub gc55_params: [i64; 6],
1757
1758    // Phase-D NOTE: the old C-Lua intrusive GC list mirrors were declared here as
1759    // `Vec<GcRef<dyn Collectable>>` during Phase A but never populated or
1760    // read. The real GC owns its allgc/finobj/tobefnz/grayagain intrusive
1761    // lists inside `self.heap` (lua_gc::Heap).
1762    pub sweepgc_cursor: usize,
1763
1764    /// Cross-table weak-sweep registry.
1765    ///
1766    /// Heap collection snapshots this list before mark, then the post-mark
1767    /// weak-table pass clears entries whose weak target is held only by other
1768    /// weak slots. The registry holds weak table entries so it does not pin
1769    /// dead weak tables after sweep removes their heap allocation token.
1770    /// Replaced by proper `weak` / `ephemeron` / `allweak` cohorts once the
1771    /// Lua-style generational lists land.
1772    pub weak_tables_registry: lua_gc::WeakRegistry<WeakTableEntry>,
1773
1774    /// Typed handles for finalizable tables/userdata. The heap owns the
1775    /// corresponding intrusive finobj/tobefnz list placement.
1776    pub finalizers: lua_gc::FinalizerRegistry<FinalizerObject>,
1777
1778    /// Error raised by a `__gc` finalizer during an explicit `collectgarbage`
1779    /// on 5.2 / 5.3, parked here for the `collectgarbage` wrapper to re-raise.
1780    ///
1781    /// C-Lua re-throws the wrapped `error in __gc metamethod (%s)` directly out
1782    /// of `GCTM` via `luaD_throw`. The Rust `api::gc` entry point returns `i32`
1783    /// (its many callers cannot all thread a `Result`), so the explicit-collect
1784    /// path stashes the wrapped error here and the `collectgarbage` built-in
1785    /// drains it into the `Result<usize, LuaError>` it already returns. Only
1786    /// the explicit-collect path sets this; the automatic GC-step and close
1787    /// paths never do (matching `GCTM(L, 0)` and the dispatch-loop swallow).
1788    pub gc_finalizer_error: Option<LuaValue>,
1789
1790    // Phase-D NOTE: fixedgc removed (dead since Phase A — see sibling note
1791    // above re allgc et al). Finalizable typed handles live in `finalizers`
1792    // above; fixed objects live in heap.allgc with the GC's own `fixed` bit.
1793
1794    // Generational cohort markers — Phase D only
1795    // types.tsv: global_State.survival/old1/reallyold/firstold1/finobjsur/finobjold1/finobjrold
1796    //   → (removed; replaced by index cursors in Phase D)
1797
1798    // types.tsv: global_State.twups → Vec<GcRef<LuaState>>
1799    pub twups: Vec<GcRef<LuaState>>,
1800
1801    // types.tsv: global_State.panic → Option<lua_CFunction>
1802    pub panic: Option<LuaCFunction>,
1803
1804    // types.tsv: global_State.mainthread → GcRef<LuaState>
1805    // TODO(port): self-referential Rc cycle; Phase D GC handles cycles properly
1806    pub mainthread: Option<GcRef<LuaState>>,
1807
1808    /// Registry of all live coroutine threads, keyed by `ThreadId`. Phase E-1
1809    /// replaces the `thread_token` placeholder with a real id-indexed map so
1810    /// `coroutine.create` allocates a fresh `LuaState`, registers it, and
1811    /// returns a value that resolves back to the same state on every
1812    /// `coroutine.status` / `coroutine.resume` call.
1813    ///
1814    /// Each entry pairs the per-thread `LuaState` with the canonical
1815    /// `GcRef<LuaThread>` value, so two `LuaValue::Thread` pushes of the
1816    /// same id share `GcRef::ptr_eq` identity. The main thread is NOT
1817    /// stored here — its `LuaState` is owned externally by the embedder.
1818    /// `main_thread_id` is reserved as `0` and a `LuaValue::Thread`
1819    /// carrying id `0` is recognized as the main thread by lookup helpers.
1820    pub threads: std::collections::HashMap<u64, ThreadRegistryEntry>,
1821
1822    /// Cached `LuaValue::Thread` payload for the main thread (id 0).
1823    /// Built once during `new_state` so every `push_thread` on the main
1824    /// thread shares the same `GcRef<LuaThread>` and thus compares
1825    /// pointer-equal under `LuaValue::PartialEq`.
1826    pub main_thread_value: GcRef<lua_types::value::LuaThread>,
1827
1828    /// Identity of the currently-running thread. `0` (main) until a
1829    /// coroutine resume swaps it in slice 02b. The Phase E-1 slice
1830    /// always leaves this at `main_thread_id` because resume is not yet
1831    /// implemented.
1832    pub current_thread_id: u64,
1833
1834    /// Thread currently being reset/closed by `coroutine.close`, if any. This is
1835    /// used to recognize reentrant closes from that thread's `__close` methods.
1836    pub closing_thread_id: Option<u64>,
1837
1838    /// Identity of the main thread. Convention: `0`. Held as a field so
1839    /// the lookup helpers can read it without hard-coding the constant.
1840    pub main_thread_id: u64,
1841
1842    /// Monotonic counter handing out fresh ids in `new_thread`. Starts
1843    /// at `1` because `0` is reserved for the main thread.
1844    pub next_thread_id: u64,
1845
1846    /// Lua 5.1 per-thread global table (`lua_State.l_gt`) for coroutine
1847    /// threads, keyed by thread id.
1848    ///
1849    /// In 5.1 every thread has its own global table: `setfenv(0, t)` and
1850    /// `getfenv(0)` read/write the *running* thread's `l_gt`, and a freshly
1851    /// loaded top-level chunk takes the running thread's `l_gt` as its
1852    /// environment. A coroutine inherits its creator's `l_gt` at creation,
1853    /// after which the two are independent — a coroutine's `setfenv(0, t)`
1854    /// must not clobber the main thread's globals.
1855    ///
1856    /// The main thread (`main_thread_id`) is the single exception: its `l_gt`
1857    /// is the existing [`Self::globals`] field (single source of truth, never
1858    /// duplicated here). Only coroutine ids appear as keys, seeded in
1859    /// `new_thread`. Empty (and never read) on 5.2–5.5, where all threads
1860    /// share one global table and this field is inert. Traced as a root and
1861    /// pruned of dead-thread entries after each collection.
1862    pub thread_globals: std::collections::HashMap<u64, LuaValue>,
1863
1864    /// Lua 5.1 per-closure environment for closures that carry no `_ENV`
1865    /// upvalue, keyed by closure identity ([`GcRef::identity`]).
1866    ///
1867    /// The reused modern parser threads an `_ENV` upvalue only onto closures
1868    /// that reference a free (global) name; a closure that references none has
1869    /// no such upvalue. 5.1's `setfenv(f, t)` must still set such a closure's
1870    /// environment and `getfenv(f)` must read it back, so the environment is
1871    /// stored here, independent of the upvalue array. A closure that *does*
1872    /// have an `_ENV` upvalue never appears here — its environment is that
1873    /// upvalue's closed value. Empty (and never read) on 5.2–5.5. Traced as a
1874    /// root and pruned of dead-closure entries after each collection.
1875    pub closure_envs: std::collections::HashMap<usize, LuaValue>,
1876
1877    // types.tsv: global_State.memerrmsg → GcRef<LuaString>
1878    pub memerrmsg: GcRef<LuaString>,
1879
1880    // types.tsv: global_State.tmname → [GcRef<LuaString>; TM_N]
1881    // TODO(port): TM_N constant and TagMethod enum come from ltm.c → tagmethods.rs
1882    pub tmname: Vec<GcRef<LuaString>>,
1883
1884    // types.tsv: global_State.mt → [Option<GcRef<LuaTable>>; LUA_NUMTYPES]
1885    pub mt: [Option<GcRef<LuaTable>>; LUA_NUMTYPES],
1886
1887    // types.tsv: global_State.strcache → [[GcRef<LuaString>; STRCACHE_M]; STRCACHE_N]
1888    pub strcache: [[GcRef<LuaString>; STRCACHE_M]; STRCACHE_N],
1889
1890    /// Stable intern map for the public [`LuaString`] type. Distinct from
1891    /// `strt` (which keys internal `LuaStringImpl`) because the parser and
1892    /// stdlib need pointer-equality across `intern_str` calls so
1893    /// `GcRef::ptr_eq` can resolve variable identity. Without this map each
1894    /// call allocates a fresh `GcRef` and locals/upvalues fail to resolve.
1895    pub interned_lt: InternedStringMap,
1896
1897    // types.tsv: global_State.warnf → Option<Box<dyn FnMut(&[u8], bool)>>
1898    pub warnf: Option<Box<dyn FnMut(&[u8], bool)>>,
1899
1900    /// State of the default warning handler (the `warnfoff`/`warnfon`/
1901    /// `warnfcont` chain from upstream `lauxlib.c`). `luaL_openlibs` installs
1902    /// `warnfoff`, so warnings start disabled until `warn("@on")`. Only
1903    /// consulted when no custom `warnf` was installed via the C API.
1904    pub warn_mode: WarnMode,
1905
1906    /// testC/ltests warning sink, enabled only by the CLI's `LUA_RS_TESTC`
1907    /// support. It mirrors `ltests.c`'s `warnf`: a separate on/off bit, an
1908    /// output mode (`normal`, `allow`, `store`), and a continuation buffer so
1909    /// multi-part warnings can be asserted via global `_WARN`.
1910    pub test_warn_enabled: bool,
1911    pub test_warn_on: bool,
1912    pub test_warn_mode: TestWarnMode,
1913    pub test_warn_last_to_cont: bool,
1914    pub test_warn_buffer: Vec<u8>,
1915
1916    /// Registry of native `LuaCFunction` pointers. Lua-types cannot reference
1917    /// `LuaState`, so `LuaClosure::LightC` carries a `usize` index into this
1918    /// vector instead of the real function pointer. `push_c_function`
1919    /// registers the function and stores the resulting index in the closure.
1920    pub c_functions: Vec<LuaCallable>,
1921
1922    /// Phase-D heap. Owns the allgc intrusive list and runs collections.
1923    /// During Phase A-C this is `paused=true`, so allocations don't auto-
1924    /// register and `step` is a no-op. Phase D-1d wires `unpause()` after
1925    /// state initialization, at which point `step` runs during VM dispatch.
1926    pub heap: lua_gc::Heap,
1927
1928    /// Phase E-3 cross-thread open-upvalue mirror. Maps `(thread_id, stack_idx)`
1929    /// to the live value of an open upvalue whose home thread is currently
1930    /// suspended while another thread runs. `coroutine.resume` snapshots the
1931    /// parent's open upvalues into this map before yielding control to the
1932    /// child, and reads the (possibly mutated) values back into the parent's
1933    /// stack when the child suspends or returns. From the running thread's
1934    /// perspective, `upvalue_get` / `upvalue_set` consult the mirror whenever
1935    /// an open upvalue's `thread_id` does not match `current_thread_id`.
1936    ///
1937    /// This avoids a stack refactor: the parent's `LuaState` is held by a
1938    /// `&mut` reference up the call stack during resume, so its stack cannot
1939    /// be reached directly through any `Rc<RefCell<_>>`. The mirror is the
1940    /// shared scratchpad that bridges the gap for the duration of a resume.
1941    pub cross_thread_upvals: std::collections::HashMap<(u64, StackIdx), LuaValue>,
1942
1943    /// Phase F-1.a workaround for GC use-after-free across coroutine boundaries.
1944    /// When `aux_resume` switches to a child thread, the parent's live stack
1945    /// values would otherwise become unreachable to the tracer for the duration
1946    /// of the resume (the parent `LuaState` is held only as a stack-borrowed
1947    /// `&mut` up the call chain and is not part of any traced root set). To
1948    /// keep those values alive, `aux_resume` pushes a snapshot of the parent
1949    /// stack here before transferring control, and pops it on suspension or
1950    /// completion. The tracer visits every snapshot as a GC root via the
1951    /// `Trace for GlobalState` impl in `trace_impls.rs`.
1952    ///
1953    /// Phase F-2.b added a reachability-driven thread sweep that supersedes
1954    /// most of this, but the snapshot still guards values that live only on
1955    /// the parent's stack (i.e. not yet rooted by any thread node).
1956    pub suspended_parent_stacks: Vec<Vec<LuaValue>>,
1957
1958    /// Open-upvalue handles belonging to the same suspended parent windows as
1959    /// `suspended_parent_stacks`. Stack snapshots keep the pointed-to values
1960    /// alive; this roots the `UpVal` objects themselves so a GC inside the
1961    /// child coroutine cannot sweep entries still present in the parent's
1962    /// `openupval` list.
1963    pub suspended_parent_open_upvals: Vec<Vec<GcRef<UpVal>>>,
1964
1965    /// Capacity pools for the two snapshot kinds above. A resume previously
1966    /// allocated and freed a fresh `Vec` per snapshot — 2 mallocs + 2 frees
1967    /// on every `coroutine.resume` (the top allocator frames in the
1968    /// coroutine_pingpong profile, 20260609T2243Z). Popped snapshots park
1969    /// their (cleared) buffers here for reuse; pool depth is bounded by the
1970    /// maximum resume nesting depth. The pooled vectors are always empty, so
1971    /// they root nothing.
1972    pub snapshot_stack_pool: Vec<Vec<LuaValue>>,
1973    pub snapshot_upval_pool: Vec<Vec<GcRef<UpVal>>>,
1974
1975    /// Capacity pool for the transient value buffers a single `aux_resume`
1976    /// builds — the argument list copied off the parent stack on entry and the
1977    /// result list copied off the child stack on return. Each was a fresh
1978    /// `Vec<LuaValue>` allocated and freed per resume. Callers pop a buffer,
1979    /// fill it, drain it back to empty, then park it here; pool depth is
1980    /// bounded by the maximum resume-nesting depth. Parked buffers are always
1981    /// empty, so they root nothing.
1982    pub resume_value_pool: Vec<Vec<LuaValue>>,
1983
1984    /// Capacity pool for the open-upvalue slot list `aux_resume` snapshots on
1985    /// entry (parent thread id + stack index per open upvalue). This buffer
1986    /// spans the child resume — it is read again on return to flush
1987    /// cross-thread upvalues back — so it follows the same pop/park discipline
1988    /// as the snapshot pools. The pairs are plain `Copy` data and root nothing.
1989    pub resume_upval_slot_pool: Vec<Vec<(u64, StackIdx)>>,
1990
1991    /// Capacity pool for the upvalue-flush buffer `aux_resume` builds on return
1992    /// (stack index + mutated value per cross-thread upvalue). Popped, filled,
1993    /// drained back to the parent stack, then parked empty.
1994    pub resume_flush_pool: Vec<Vec<(StackIdx, LuaValue)>>,
1995}
1996
1997/// `LUA_MASKCOUNT` (`1 << LUA_HOOKCOUNT`) — the count-hook event mask the
1998/// sandbox arms on every thread to drive per-interval budget enforcement.
1999const SANDBOX_COUNT_MASK: u8 = 1 << 3;
2000
2001/// Sandbox trip code: not tripped.
2002pub const SANDBOX_TRIP_NONE: u8 = 0;
2003/// Sandbox trip code: the instruction budget reached zero.
2004pub const SANDBOX_TRIP_INSTRUCTIONS: u8 = 1;
2005/// Sandbox trip code: GC-tracked memory exceeded the configured ceiling.
2006pub const SANDBOX_TRIP_MEMORY: u8 = 2;
2007
2008/// Per-runtime sandbox budget, shared by every thread (main + coroutines) via
2009/// the `Rc<RefCell<GlobalState>>` they all hold. Every field is a `Cell` so the
2010/// VM can charge the budget through the shared `Ref` it borrows in the
2011/// count-hook path — no `&mut` and no write-borrow on the hot path.
2012/// `interval == 0` means inactive; in that case the VM never sets the
2013/// count-hook mask, so there is zero overhead.
2014#[derive(Default)]
2015pub struct SandboxLimits {
2016    /// Count-hook interval in instructions; `0` = sandbox inactive.
2017    pub interval: std::cell::Cell<i32>,
2018    /// Whether an instruction budget is enforced.
2019    pub instr_limited: std::cell::Cell<bool>,
2020    /// Instructions left before the budget trips.
2021    pub instr_remaining: std::cell::Cell<u64>,
2022    /// Configured instruction limit, retained so `reset` can refill.
2023    pub instr_limit: std::cell::Cell<u64>,
2024    /// GC-byte ceiling; `None` = no memory limit.
2025    pub mem_limit: std::cell::Cell<Option<usize>>,
2026    /// One of the `SANDBOX_TRIP_*` codes.
2027    pub tripped: std::cell::Cell<u8>,
2028    /// Sticky once a limit trips: the abort is *uncatchable*. While set,
2029    /// `pcall`/`xpcall`/`coroutine.resume` re-raise the trip error instead of
2030    /// swallowing it, so untrusted code cannot defeat the budget by catching
2031    /// it in a loop. Cleared only by [`LuaState::sandbox_reset`].
2032    pub aborting: std::cell::Cell<bool>,
2033}
2034
2035impl GlobalState {
2036    /// True while a sandbox instruction/memory budget is active on this runtime.
2037    pub fn sandbox_active(&self) -> bool {
2038        self.sandbox.interval.get() != 0
2039    }
2040
2041    /// Total live bytes allocated, as reported by the collector-owned heap
2042    /// accounting model.
2043    ///
2044    /// macros.tsv: `gettotalbytes → g.total_bytes()`
2045    pub fn total_bytes(&self) -> usize {
2046        self.heap.bytes_used().max(1)
2047    }
2048
2049    /// Look up the coroutine `LuaState` registered under `id`. Returns
2050    /// `None` for the main-thread id (the main `LuaState` is owned by
2051    /// the embedder, not stored in `threads`) and for ids that were
2052    /// never issued or have already been closed.
2053    pub fn get_thread(&self, id: u64) -> Option<&ThreadRegistryEntry> {
2054        self.threads.get(&id)
2055    }
2056
2057    /// Return the canonical `GcRef<LuaThread>` for `id`. For the main
2058    /// thread that's `main_thread_value`; for a coroutine it's the
2059    /// value stored in the registry. Returns `None` if `id` is unknown.
2060    pub fn thread_value_for(&self, id: u64) -> Option<GcRef<lua_types::value::LuaThread>> {
2061        if id == self.main_thread_id {
2062            Some(self.main_thread_value.clone())
2063        } else {
2064            self.threads.get(&id).map(|e| e.value.clone())
2065        }
2066    }
2067
2068    /// Returns `true` when the state has been fully initialized.
2069    ///
2070    /// macros.tsv: `completestate → g.is_complete()`
2071    ///
2072    /// PORT NOTE: C uses `g->nilvalue` being nil as the "complete" signal.
2073    /// We replicate the same logic: `nilvalue == Nil` means complete.
2074    pub fn is_complete(&self) -> bool {
2075        matches!(self.nilvalue, LuaValue::Nil)
2076    }
2077
2078    /// Returns the "current white" GC color bitmask.
2079    ///
2080    /// macros.tsv: `luaC_white → g.current_white()`
2081    ///
2082    /// PORT NOTE: the effective dual-white collector state lives in
2083    /// `lua_gc::Heap`; this field preserves the translated `global_State`
2084    /// shape for code that still reads the upstream bitmask.
2085    pub fn current_white(&self) -> u8 {
2086        self.currentwhite
2087    }
2088
2089    /// Returns the "other white" GC color bitmask.
2090    ///
2091    /// macros.tsv: `otherwhite → g.other_white()`
2092    pub fn other_white(&self) -> u8 {
2093        self.currentwhite ^ 0x03
2094    }
2095
2096    /// Returns `true` if the GC is in generational mode.
2097    ///
2098    /// macros.tsv: `isdecGCmodegen → g.is_gen_mode()`
2099    pub fn is_gen_mode(&self) -> bool {
2100        self.gckind == GcKind::Generational as u8 || self.lastatomic != 0
2101    }
2102
2103    /// Returns `true` if the GC is currently running.
2104    ///
2105    /// macros.tsv: `gcrunning → g.gc_running()`
2106    pub fn gc_running(&self) -> bool {
2107        self.gcstp == 0
2108    }
2109
2110    /// Returns `true` while the GC is in its propagation phase.
2111    ///
2112    /// macros.tsv: `keepinvariant → g.keep_invariant()`
2113    pub fn keep_invariant(&self) -> bool {
2114        self.heap.gc_state().is_invariant()
2115    }
2116
2117    /// Returns `true` while the GC is in a sweep phase.
2118    ///
2119    /// macros.tsv: `issweepphase → g.is_sweep_phase()`
2120    pub fn is_sweep_phase(&self) -> bool {
2121        self.heap.gc_state().is_sweep()
2122    }
2123
2124    // ── Phase-B stubs ─────────────────────────────────────────────────────────
2125    pub fn gc_debt(&self) -> isize {
2126        self.gc_debt
2127    }
2128    pub fn set_gc_debt(&mut self, d: isize) {
2129        self.gc_debt = d;
2130    }
2131    pub fn gc_at_pause(&self) -> bool {
2132        self.heap.gc_state().is_pause()
2133    }
2134    fn get_gc_param(p: u8) -> i32 {
2135        (p as i32) * 4
2136    }
2137    fn set_gc_param_slot(slot: &mut u8, p: i32) {
2138        *slot = (p / 4) as u8;
2139    }
2140    pub fn gc_pause_param(&self) -> i32 {
2141        Self::get_gc_param(self.gcpause)
2142    }
2143    pub fn set_gc_pause_param(&mut self, p: i32) {
2144        Self::set_gc_param_slot(&mut self.gcpause, p);
2145    }
2146    pub fn gc_stepmul_param(&self) -> i32 {
2147        Self::get_gc_param(self.gcstepmul)
2148    }
2149    pub fn set_gc_stepmul_param(&mut self, p: i32) {
2150        Self::set_gc_param_slot(&mut self.gcstepmul, p);
2151    }
2152    pub fn gc_genmajormul_param(&self) -> i32 {
2153        Self::get_gc_param(self.genmajormul)
2154    }
2155    pub fn set_gc_genmajormul(&mut self, p: i32) {
2156        Self::set_gc_param_slot(&mut self.genmajormul, p);
2157    }
2158    /// Lua 5.5 `collectgarbage("param", name [, value])`. `idx` is the 0-based
2159    /// param index (`minormul=0 .. stepsize=5`). When `value >= 0` the param is
2160    /// set; the previous value is always returned.
2161    pub fn gc55_param(&mut self, idx: usize, value: i64) -> i64 {
2162        let old = self.gc55_params[idx];
2163        if value >= 0 {
2164            self.gc55_params[idx] = value;
2165        }
2166        old
2167    }
2168    pub fn gc_stop_flags(&self) -> u8 {
2169        self.gcstp
2170    }
2171    pub fn set_gc_stop_flags(&mut self, f: u8) {
2172        self.gcstp = f;
2173    }
2174    pub fn stop_gc_internal(&mut self) -> u8 {
2175        let old = self.gcstp;
2176        self.gcstp |= GCSTPGC;
2177        old
2178    }
2179    pub fn set_gc_stop_user(&mut self) {
2180        // GCSTPUSR (lgc.h:155) = 1 — bit set when GC is stopped by user (lua_gc(L, LUA_GCSTOP)).
2181        self.gcstp = GCSTPUSR;
2182    }
2183    pub fn clear_gc_stop(&mut self) {
2184        self.gcstp = 0;
2185    }
2186    pub fn is_gc_running(&self) -> bool {
2187        self.gcstp == 0
2188    }
2189    /// True when the GC has been disabled internally (state setup, mid-GC,
2190    /// or while closing); user-stop via `collectgarbage("stop")` does NOT
2191    /// set this bit, so `lua_gc` continues to honour Count/Step/etc.
2192    ///
2193    pub fn is_gc_stopped_internally(&self) -> bool {
2194        (self.gcstp & GCSTPGC) != 0
2195    }
2196
2197    /// Returns the interned `__xxx` name string for tag method `tm`, or
2198    /// `None` if `tmname` has not yet been initialised (early bootstrap).
2199    ///
2200    /// macros.tsv: `getshrstr(G(L)->tmname[tm]) → g.tm_name(tm)`.
2201    ///
2202    /// PORT NOTE: The lua-vm crate carries two distinct `TagMethod` enums
2203    /// (one in `lua-types`, one in `crate::tagmethods`) with identical
2204    /// `#[repr(u8)]` ordering. The [`TmIndex`] trait bridges them so callers
2205    /// from either side can index `tmname` uniformly.
2206    pub fn tm_name<T: TmIndex>(&self, tm: T) -> Option<GcRef<LuaString>> {
2207        self.tmname.get(tm.tm_index()).cloned()
2208    }
2209}
2210
2211/// Discriminant-to-index conversion for the two parallel `TagMethod` enums.
2212///
2213/// Both `lua_types::tagmethod::TagMethod` and `crate::tagmethods::TagMethod`
2214/// are `#[repr(u8)]` with the same ORDER TM layout, so casting through `u8`
2215/// yields the correct `GlobalState.tmname` index for either type.
2216pub trait TmIndex: Copy {
2217    fn tm_index(self) -> usize;
2218}
2219impl TmIndex for lua_types::tagmethod::TagMethod {
2220    fn tm_index(self) -> usize {
2221        self as u8 as usize
2222    }
2223}
2224impl TmIndex for crate::tagmethods::TagMethod {
2225    fn tm_index(self) -> usize {
2226        self as u8 as usize
2227    }
2228}
2229impl TmIndex for usize {
2230    fn tm_index(self) -> usize {
2231        self
2232    }
2233}
2234impl TmIndex for u8 {
2235    fn tm_index(self) -> usize {
2236        self as usize
2237    }
2238}
2239
2240use lua_types::tagmethod::TagMethod;
2241
2242// ─── LuaState ────────────────────────────────────────────────────────────────
2243
2244/// Per-thread Lua execution state.
2245///
2246/// types.tsv: `lua_State → LuaState`
2247///
2248/// All stack-pointer fields in C (`StkIdRel`, `StkId`) become `StackIdx` (u32
2249/// index into `stack: Vec<StackValue>`).  The C intrusive `CallInfo` linked list
2250/// becomes `call_info: Vec<CallInfo>` indexed by `CallInfoIdx`.
2251pub struct LuaState {
2252    // ── Thread status ──
2253
2254    // types.tsv: lua_State.status → u8
2255    pub status: u8,
2256
2257    // types.tsv: lua_State.allowhook → bool
2258    pub allowhook: bool,
2259
2260    // types.tsv: lua_State.nci → u32
2261    pub nci: u32,
2262
2263    // ── Stack ──
2264
2265    // types.tsv: lua_State.top → StackIdx
2266    pub top: StackIdx,
2267
2268    // types.tsv: lua_State.stack_last → StackIdx (redundant once Vec; kept for parity)
2269    pub stack_last: StackIdx,
2270
2271    // types.tsv: lua_State.stack → Vec<StackValue>
2272    pub stack: Vec<StackValue>,
2273
2274    // ── Call info ──
2275
2276    // types.tsv: lua_State.ci → CallInfoIdx
2277    pub ci: CallInfoIdx,
2278
2279    // types.tsv: lua_State.base_ci → CallInfo  (Vec element 0)
2280    // PORT NOTE: In Rust, base_ci is call_info[0]. There is no separate field.
2281    pub call_info: Vec<CallInfo>,
2282
2283    // ── Upvalues / to-be-closed ──
2284
2285    // types.tsv: lua_State.openupval → Vec<GcRef<UpVal>>
2286    pub openupval: Vec<GcRef<UpVal>>,
2287
2288    /// Per-thread mirror of C 5.3's per-open-upvalue `touched` flag
2289    /// (`lfunc.h` `UpVal.u.open.touched`). Set when this thread creates or
2290    /// writes an open upvalue; consulted only by the legacy (5.1/5.2/5.3)
2291    /// `remarkupvals` pass in [`trace_reachable_threads`]. In those versions
2292    /// an unmarked thread's touched open upvalues have their values re-marked
2293    /// **once** during the atomic phase, then the flag is cleared (mirroring
2294    /// C clearing `touched` and dropping the thread from `g->twups`). This is
2295    /// what makes a cycle reachable only through a suspended thread survive
2296    /// one extra collection before being finalized. From 5.4 the C condition
2297    /// became `!iswhite(uv)` instead of `touched`, so this flag is never read
2298    /// for modern versions and the baked 5.4/5.5 behavior is unchanged.
2299    pub legacy_open_upval_touched: std::cell::Cell<bool>,
2300
2301    // types.tsv: lua_State.tbclist → Vec<StackIdx>
2302    pub tbclist: Vec<StackIdx>,
2303
2304    // ── Global state ──
2305
2306    // types.tsv: lua_State.l_G → (accessed via method)
2307    // PORT NOTE: Rc<RefCell<>> for shared ownership across coroutine threads.
2308    pub(crate) global: Rc<RefCell<GlobalState>>,
2309
2310    // ── Hooks ──
2311
2312    // types.tsv: lua_State.hook → Option<Box<dyn FnMut(&mut LuaState, &LuaDebug)>>
2313    pub hook: Option<Box<dyn FnMut(&mut LuaState, &crate::debug::LuaDebug)>>,
2314
2315    // types.tsv: lua_State.hookmask → u8
2316    pub hookmask: u8,
2317
2318    // types.tsv: lua_State.basehookcount → i32
2319    pub basehookcount: i32,
2320
2321    // types.tsv: lua_State.hookcount → i32
2322    pub hookcount: i32,
2323
2324    // ── Error handling ──
2325
2326    // types.tsv: lua_State.errorJmp → (removed; replaced by Result<T, LuaError>)
2327    // PORT NOTE: Entirely removed. The `?` operator replaces setjmp/longjmp.
2328
2329    // types.tsv: lua_State.errfunc → isize
2330    pub errfunc: isize,
2331
2332    // ── C-call depth ──
2333
2334    // types.tsv: lua_State.n_ccalls → u32
2335    pub n_ccalls: u32,
2336
2337    // ── Debug / hooks ──
2338
2339    // types.tsv: lua_State.oldpc → u32
2340    pub oldpc: u32,
2341
2342    // ── GC color (Phase D) ──
2343
2344    // types.tsv: GCObject.marked → u8
2345    pub marked: u8,
2346
2347    /// Owner thread id for this `LuaState`, cached as a plain `u64` so the
2348    /// hot path of `upvalue_get` can compare against an open upvalue's
2349    /// `thread_id` without taking a `RefCell::borrow` on the shared
2350    /// `GlobalState`.
2351    ///
2352    /// Invariant: while this `LuaState` is the actively running thread,
2353    /// `GlobalState::current_thread_id == self.cached_thread_id`. This is
2354    /// maintained structurally by `new_state`/`new_thread` (which set
2355    /// `cached_thread_id` to the thread's own id once at construction)
2356    /// combined with the coroutine resume protocol: `coro_lib::resume`
2357    /// writes `co_state.global.current_thread_id = co_id` before the
2358    /// coroutine runs, and restores `parent_thread_id` on yield/return.
2359    /// Because each thread caches its own id (not the global's id), the
2360    /// invariant survives every context switch without an explicit refresh
2361    /// at the resume site.
2362    pub cached_thread_id: u64,
2363
2364    /// Local GC gate.
2365    ///
2366    /// Avoids borrowing `GlobalState` on every call edge when GC/finalizers
2367    /// are not currently due.
2368    pub gc_check_needed: bool,
2369}
2370
2371impl LuaState {
2372    /// Access the process-wide `GlobalState` immutably.
2373    ///
2374    /// macros.tsv: `G → state.global()`
2375    ///
2376    /// PORT NOTE: Returns `std::cell::Ref<GlobalState>` because GlobalState is held in
2377    /// `Rc<RefCell<...>>`. Call sites that do `state.global().field` should work fine
2378    /// via `Deref`. Callers must not hold the `Ref` across a `global_mut()` call.
2379    pub fn global(&self) -> std::cell::Ref<'_, GlobalState> {
2380        self.global.borrow()
2381    }
2382
2383    /// Access the process-wide `GlobalState` mutably.
2384    ///
2385    /// macros.tsv: `G → state.global()` (writes use `state.global_mut()`)
2386    pub fn global_mut(&self) -> std::cell::RefMut<'_, GlobalState> {
2387        self.global.borrow_mut()
2388    }
2389
2390    /// Clone the `Rc` handle to the GlobalState for sharing with a new coroutine.
2391    ///
2392    /// Used in `new_thread` to give the child thread access to the same GlobalState.
2393    pub fn global_rc(&self) -> Rc<RefCell<GlobalState>> {
2394        Rc::clone(&self.global)
2395    }
2396
2397    /// Lua 5.1 global table of the thread `id` (`lua_State.l_gt`).
2398    ///
2399    /// The main thread's `l_gt` is the shared [`GlobalState::globals`] field;
2400    /// a coroutine's lives in [`GlobalState::thread_globals`] under its id,
2401    /// seeded at creation in `new_thread`. A coroutine id that is missing from
2402    /// the map is a `new_thread` seeding bug, not a recoverable state.
2403    ///
2404    /// 5.1 only. Other versions never call this — they share one global table.
2405    pub fn v51_thread_lgt(&self, id: u64) -> LuaValue {
2406        let g = self.global();
2407        if id == g.main_thread_id {
2408            g.globals.clone()
2409        } else {
2410            g.thread_globals
2411                .get(&id)
2412                .expect("v51 coroutine l_gt must be seeded in new_thread")
2413                .clone()
2414        }
2415    }
2416
2417    /// Set the Lua 5.1 global table of thread `id` (`lua_State.l_gt`).
2418    ///
2419    /// Writes the main thread's `l_gt` through [`GlobalState::globals`] and a
2420    /// coroutine's through [`GlobalState::thread_globals`]. 5.1 only.
2421    pub fn v51_set_thread_lgt(&self, id: u64, t: LuaValue) {
2422        let mut g = self.global_mut();
2423        if id == g.main_thread_id {
2424            g.globals = t;
2425        } else {
2426            g.thread_globals.insert(id, t);
2427        }
2428    }
2429
2430    /// Enable the ltests-style warning sink used by `LUA_RS_TESTC`.
2431    pub fn enable_test_warning_handler(&mut self) -> Result<(), LuaError> {
2432        {
2433            let mut g = self.global_mut();
2434            g.test_warn_enabled = true;
2435            g.test_warn_on = false;
2436            g.test_warn_mode = TestWarnMode::Normal;
2437            g.test_warn_last_to_cont = false;
2438            g.test_warn_buffer.clear();
2439        }
2440        self.push(LuaValue::Bool(false));
2441        crate::api::set_global(self, b"_WARN")
2442    }
2443
2444    /// Return the current C-call recursion depth (lower 16 bits of `n_ccalls`).
2445    ///
2446    /// macros.tsv: `getCcalls → state.c_calls()`
2447    pub fn c_calls(&self) -> u32 {
2448        self.n_ccalls & 0xffff
2449    }
2450
2451    /// Increment the non-yieldable call count (upper 16 bits of `n_ccalls`).
2452    ///
2453    /// macros.tsv: `incnny → state.inc_nny()`
2454    pub fn inc_nny(&mut self) {
2455        self.n_ccalls += 0x10000;
2456    }
2457
2458    /// Decrement the non-yieldable call count.
2459    ///
2460    /// macros.tsv: `decnny → state.dec_nny()`
2461    pub fn dec_nny(&mut self) {
2462        self.n_ccalls -= 0x10000;
2463    }
2464
2465    /// Returns `true` if the thread can yield (no non-yieldable frames on the stack).
2466    ///
2467    /// macros.tsv: `yieldable → state.is_yieldable()`
2468    pub fn is_yieldable(&self) -> bool {
2469        (self.n_ccalls & 0xffff0000) == 0
2470    }
2471
2472    /// Reset the hook countdown to the baseline.
2473    ///
2474    /// macros.tsv: `resethookcount → state.reset_hook_count()`
2475    pub fn reset_hook_count(&mut self) {
2476        self.hookcount = self.basehookcount;
2477    }
2478
2479    /// Activate the per-runtime sandbox budget and arm the current thread.
2480    ///
2481    /// Stores the budget in `GlobalState` (shared across every thread) and
2482    /// sets the count-hook mask on this thread so the dispatch loop traps every
2483    /// `interval` instructions. Coroutines created afterwards inherit the mask
2484    /// via `preinit_thread`, so metering spans all threads — closing the
2485    /// coroutine-escape that a per-thread closure could not. Pass `None` for a
2486    /// limit to leave that dimension unbounded.
2487    pub fn install_sandbox_limits(
2488        &mut self,
2489        interval: i32,
2490        instr_limit: Option<u64>,
2491        mem_limit: Option<usize>,
2492    ) {
2493        let interval = interval.max(1);
2494        {
2495            let g = self.global();
2496            g.sandbox.interval.set(interval);
2497            g.sandbox.instr_limited.set(instr_limit.is_some());
2498            g.sandbox.instr_remaining.set(instr_limit.unwrap_or(0));
2499            g.sandbox.instr_limit.set(instr_limit.unwrap_or(0));
2500            g.sandbox.mem_limit.set(mem_limit);
2501            g.sandbox.tripped.set(SANDBOX_TRIP_NONE);
2502        }
2503        self.hookmask |= SANDBOX_COUNT_MASK;
2504        self.basehookcount = interval;
2505        self.hookcount = interval;
2506        crate::debug::arm_traps(self);
2507    }
2508
2509    /// Charge the shared budget for one count-hook interval. Returns the abort
2510    /// error if a limit has been crossed (and records why in `tripped`).
2511    /// Called from `trace_exec` on every thread, once per `interval`
2512    /// instructions — never on the budget-disabled hot path.
2513    pub fn sandbox_charge_interval(&self) -> Option<LuaError> {
2514        let interval = self.global().sandbox.interval.get();
2515        self.sandbox_charge(interval as u64)
2516    }
2517
2518    /// Charge `amount` instructions against the runtime-wide budget and sample
2519    /// the memory ceiling. Returns the uncatchable abort error if a limit is
2520    /// crossed (recording the reason and arming the sticky `aborting` flag), or
2521    /// `None` otherwise. No-op when no sandbox is active.
2522    ///
2523    /// Used both by the per-interval VM charge and by loop-heavy stdlib
2524    /// functions (the pattern matcher) so a single native call cannot run for
2525    /// longer than the instruction budget allows.
2526    pub fn sandbox_charge(&self, amount: u64) -> Option<LuaError> {
2527        let g = self.global();
2528        if g.sandbox.interval.get() == 0 {
2529            return None;
2530        }
2531        if g.sandbox.instr_limited.get() {
2532            let rem = g.sandbox.instr_remaining.get().saturating_sub(amount);
2533            g.sandbox.instr_remaining.set(rem);
2534            if rem == 0 {
2535                g.sandbox.tripped.set(SANDBOX_TRIP_INSTRUCTIONS);
2536                g.sandbox.aborting.set(true);
2537                return Some(LuaError::runtime(format_args!(
2538                    "sandbox: instruction budget exhausted"
2539                )));
2540            }
2541        }
2542        if let Some(limit) = g.sandbox.mem_limit.get() {
2543            if g.total_bytes() > limit {
2544                g.sandbox.tripped.set(SANDBOX_TRIP_MEMORY);
2545                g.sandbox.aborting.set(true);
2546                return Some(LuaError::runtime(format_args!(
2547                    "sandbox: memory limit exceeded"
2548                )));
2549            }
2550        }
2551        None
2552    }
2553
2554    /// Reject a size-known-upfront allocation that would push GC-tracked memory
2555    /// past the ceiling, *before* the buffer is built. Returns the uncatchable
2556    /// memory abort if `total_bytes() + additional` exceeds the limit. Used by
2557    /// stdlib functions that allocate a large buffer of a computed size in one
2558    /// instruction (e.g. `string.rep`, `string.pack`, `table.concat`), where the
2559    /// per-instruction `sandbox_check_memory` would only fire *after* the
2560    /// allocation already happened.
2561    pub fn sandbox_reserve(&self, additional: usize) -> Option<LuaError> {
2562        let g = self.global();
2563        if g.sandbox.interval.get() == 0 {
2564            return None;
2565        }
2566        if let Some(limit) = g.sandbox.mem_limit.get() {
2567            let projected = g.total_bytes().saturating_add(additional);
2568            if projected > limit {
2569                g.sandbox.tripped.set(SANDBOX_TRIP_MEMORY);
2570                g.sandbox.aborting.set(true);
2571                return Some(LuaError::runtime(format_args!(
2572                    "sandbox: memory limit exceeded"
2573                )));
2574            }
2575        }
2576        None
2577    }
2578
2579    /// Upper bound on the work a single pattern-match call may do before it must
2580    /// stop and let the caller charge the budget. Equal to the remaining
2581    /// instruction budget when an instruction limit is active, else `0` meaning
2582    /// "unlimited" (preserving non-sandboxed behavior exactly).
2583    pub fn sandbox_match_step_limit(&self) -> u64 {
2584        let g = self.global();
2585        if g.sandbox.interval.get() != 0 && g.sandbox.instr_limited.get() {
2586            g.sandbox.instr_remaining.get()
2587        } else {
2588            0
2589        }
2590    }
2591
2592    /// Whether a sandbox abort is in flight. While true, protected-call builtins
2593    /// (`pcall`/`xpcall`/`coroutine.resume`) must re-raise rather than catch, so
2594    /// the budget trip is uncatchable. Set on trip, cleared by `sandbox_reset`.
2595    pub fn sandbox_aborting(&self) -> bool {
2596        self.global().sandbox.aborting.get()
2597    }
2598
2599    /// Whether an instruction budget is active (vs. only a memory limit / none).
2600    pub fn sandbox_instr_limited(&self) -> bool {
2601        self.global().sandbox.instr_limited.get()
2602    }
2603
2604    /// Instructions left before the budget trips (meaningful only when
2605    /// [`sandbox_instr_limited`](Self::sandbox_instr_limited)).
2606    pub fn sandbox_instr_remaining(&self) -> u64 {
2607        self.global().sandbox.instr_remaining.get()
2608    }
2609
2610    /// The configured instruction limit (for computing "used").
2611    pub fn sandbox_instr_limit(&self) -> u64 {
2612        self.global().sandbox.instr_limit.get()
2613    }
2614
2615    /// The current trip code (one of the `SANDBOX_TRIP_*` constants).
2616    pub fn sandbox_tripped_code(&self) -> u8 {
2617        self.global().sandbox.tripped.get()
2618    }
2619
2620    /// Refill the instruction budget to its configured limit and clear the
2621    /// trip flag, so the same runtime can run another chunk.
2622    pub fn sandbox_reset(&self) {
2623        let g = self.global();
2624        if g.sandbox.instr_limited.get() {
2625            g.sandbox.instr_remaining.set(g.sandbox.instr_limit.get());
2626        }
2627        g.sandbox.tripped.set(SANDBOX_TRIP_NONE);
2628        g.sandbox.aborting.set(false);
2629    }
2630
2631    /// Returns the current stack capacity (slots between base and stack_last).
2632    ///
2633    /// macros.tsv: `stacksize → state.stack_size()`
2634    pub fn stack_size(&self) -> usize {
2635        self.stack_last.0 as usize
2636    }
2637
2638    /// Push a value onto the stack, incrementing `top`.
2639    ///
2640    /// macros.tsv: `api_incr_top → gone — state.push() already increments`
2641    #[inline(always)]
2642    pub fn push(&mut self, val: LuaValue) {
2643        let top = self.top.0 as usize;
2644        if top < self.stack.len() {
2645            self.stack[top] = StackValue { val };
2646        } else {
2647            self.stack.push(StackValue { val });
2648        }
2649        self.top = StackIdx(self.top.0 + 1);
2650    }
2651
2652    /// Pop the top value from the stack, decrementing `top`.
2653    ///
2654    #[inline(always)]
2655    pub fn pop(&mut self) -> LuaValue {
2656        if self.top.0 == 0 {
2657            return LuaValue::Nil;
2658        }
2659        self.top = StackIdx(self.top.0 - 1);
2660        self.stack[self.top.0 as usize].val.clone()
2661    }
2662
2663    /// Retrieve the value at the given stack index without removing it.
2664    ///
2665    /// macros.tsv: `s2v → state.stack_at(idx)` → returns `&LuaValue`
2666    #[inline(always)]
2667    pub fn stack_val(&self, idx: StackIdx) -> &LuaValue {
2668        &self.stack[idx.0 as usize].val
2669    }
2670
2671    /// Write a value to a specific stack slot.
2672    #[inline(always)]
2673    pub fn set_stack_val(&mut self, idx: StackIdx, val: LuaValue) {
2674        self.stack[idx.0 as usize].val = val;
2675    }
2676
2677    /// Returns a no-op GC handle.
2678    ///
2679    /// macros.tsv: `luaC_checkGC → state.gc().check_step()`, etc.
2680    ///
2681    /// PORT NOTE: In Phases A–C the GC is `Rc`-based and all GC operations are
2682    /// no-ops. Phase D replaces this with real GC logic in `lua-gc`.
2683    pub fn gc(&mut self) -> GcHandle<'_> {
2684        GcHandle { _state: self }
2685    }
2686
2687    /// Pin a Lua value in the external root set and return its stable key.
2688    pub fn external_root_value(&mut self, value: LuaValue) -> ExternalRootKey {
2689        self.global_mut().external_roots.insert(value)
2690    }
2691
2692    /// Read a value currently pinned by an external root key.
2693    pub fn external_rooted_value(&self, key: ExternalRootKey) -> Option<LuaValue> {
2694        self.global().external_roots.get(key).cloned()
2695    }
2696
2697    /// Replace the value pinned by an external root key.
2698    pub fn external_replace_root(
2699        &mut self,
2700        key: ExternalRootKey,
2701        value: LuaValue,
2702    ) -> Option<LuaValue> {
2703        self.global_mut().external_roots.replace(key, value)
2704    }
2705
2706    /// Remove an external root. Returns `None` for stale or already-removed keys.
2707    pub fn external_unroot_value(&mut self, key: ExternalRootKey) -> Option<LuaValue> {
2708        self.global_mut().external_roots.remove(key)
2709    }
2710
2711    /// Best-effort external root removal for destructors that may run while
2712    /// the collector holds an immutable `GlobalState` borrow.
2713    pub fn try_external_unroot_value(
2714        &mut self,
2715        key: ExternalRootKey,
2716    ) -> std::result::Result<Option<LuaValue>, std::cell::BorrowMutError> {
2717        self.global
2718            .try_borrow_mut()
2719            .map(|mut global| global.external_roots.remove(key))
2720    }
2721
2722    /// Create a new empty table and register it with the GC.
2723    ///
2724    /// macros.tsv: `lua_newtable → state.new_table()`
2725    pub fn new_table(&mut self) -> GcRef<LuaTable> {
2726        // TODO(port): register with GC tracking (state.global_mut().allgc) in Phase D
2727        self.mark_gc_check_needed();
2728        GcRef::new(LuaTable::placeholder())
2729    }
2730
2731    /// Create a fresh table with pre-sized array/hash parts.
2732    ///
2733    /// mirrors the `luaH_new` + `luaH_resize` pair in one call so we don't
2734    /// pay an extra resize path for hot construction sites.
2735    pub fn new_table_with_sizes(
2736        &mut self,
2737        array_size: u32,
2738        hash_size: u32,
2739    ) -> Result<GcRef<LuaTable>, LuaError> {
2740        self.mark_gc_check_needed();
2741        let t = GcRef::new(LuaTable::placeholder());
2742        self.table_resize(&t, array_size as usize, hash_size as usize)?;
2743        Ok(t)
2744    }
2745
2746    /// Intern a byte string in the global string pool.
2747    ///
2748    /// In C, short strings (≤ LUAI_MAXSHORTLEN = 40 bytes) are interned globally
2749    /// via `luaS_newlstr`, while long strings allocate a fresh TString each
2750    /// call so distinct long strings keep distinct object identity (observable
2751    /// via `string.format("%p", s)`). The parser separately deduplicates
2752    /// long-string literals within a single chunk through `luaX_newstring`'s
2753    /// `ls->h` anchor table.
2754    ///
2755    /// macros.tsv: `luaS_new → state.intern_str(s)`
2756    /// Short-string interning, `luaS_newlstr` shape: one hash of the input
2757    /// bytes, a bucket walk on hit (zero allocation), and an insert that
2758    /// reuses the same hash on miss. See the `InternedStringMap` doc for why
2759    /// this replaced the owned-key `HashMap` entry API.
2760    pub fn intern_str(&mut self, bytes: &[u8]) -> Result<GcRef<LuaString>, LuaError> {
2761        if bytes.len() <= crate::string::MAX_SHORT_LEN {
2762            let hash = LuaString::hash_bytes(bytes, 0);
2763            {
2764                let global = self.global();
2765                if let Some(existing) = global.interned_lt.find(bytes, hash) {
2766                    return Ok(existing);
2767                }
2768            }
2769            let new_ref = GcRef::new(LuaString::from_slice(bytes));
2770            new_ref.account_buffer(new_ref.buffer_bytes() as isize);
2771            self.global_mut().interned_lt.insert(new_ref.clone());
2772            self.mark_gc_check_needed();
2773            Ok(new_ref)
2774        } else {
2775            self.mark_gc_check_needed();
2776            let new_ref = GcRef::new(LuaString::from_slice(bytes));
2777            new_ref.account_buffer(new_ref.buffer_bytes() as isize);
2778            Ok(new_ref)
2779        }
2780    }
2781
2782    /// Returns the current CallInfo index (the active call frame).
2783    #[inline(always)]
2784    pub fn top_idx(&self) -> StackIdx {
2785        self.top
2786    }
2787}
2788
2789// ─── Phase-B stub methods ─────────────────────────────────────────────────────
2790//
2791// The methods in the impl blocks below were referenced by api.rs, debug.rs,
2792// do_.rs, vm.rs, tagmethods.rs etc. during Phase A. Each body is a `todo!()`
2793// pinned to a phase-b task; once the corresponding C function is faithfully
2794// ported the stub will be replaced. Signatures are inferred from call sites
2795// and should be treated as Phase-B-grade approximations.
2796
2797impl LuaState {
2798    #[inline(always)]
2799    pub fn get_at(&self, idx: impl Into<StackIdxConv>) -> LuaValue {
2800        let i: StackIdx = idx.into().0;
2801        match self.stack.get(i.0 as usize) {
2802            Some(slot) => slot.val.clone(),
2803            None => LuaValue::Nil,
2804        }
2805    }
2806    #[inline(always)]
2807    pub fn set_at(&mut self, idx: impl Into<StackIdxConv>, v: LuaValue) {
2808        let i: StackIdx = idx.into().0;
2809        self.stack[i.0 as usize].val = v;
2810    }
2811
2812    /// Clear stack slots in `[start, end)` without changing `top`.
2813    ///
2814    /// Under the exact-rooting model (abe2b52) the collector traces only the
2815    /// live window `[0, top)` and, per collect, nils the dead tail above `top`
2816    /// up to the high-water mark before tracing — so a reserved-but-unused tail
2817    /// is normally cleared by the collector itself, not by callers. This helper
2818    /// exists for the call-setup paths that raise `ci.top` above the live `top`
2819    /// (e.g. a tail call reserving its callee frame): on those paths the gap
2820    /// `[live_top, new_ci_top)` can be reached by the per-collect dead-tail
2821    /// clear and the trace within a single collect off the same raised top, so
2822    /// it must not retain stale collectable `GcRef`s left by a previous frame —
2823    /// retaining them is the #140 use-after-free class. Callers clear the gap
2824    /// here so the raised top is GC-safe before any allocation can collect.
2825    pub fn clear_stack_range(&mut self, start: StackIdx, end: StackIdx) {
2826        if end.0 <= start.0 {
2827            return;
2828        }
2829        let end_u = end.0 as usize;
2830        if end_u > self.stack.len() {
2831            self.stack.resize_with(end_u, StackValue::default);
2832        }
2833        for i in start.0..end.0 {
2834            self.stack[i as usize].val = LuaValue::Nil;
2835        }
2836    }
2837    /// Hot-path accessor: returns `Some(i)` only when the stack slot at `idx`
2838    /// holds a `LuaValue::Int(i)`. Returns `None` for any other tag (including
2839    /// out-of-bounds, which behaves as `Nil`).
2840    ///
2841    /// `ttisinteger` predicate that gates the integer arithmetic fast path in
2842    /// `lvm.c`'s `op_arith_aux` macro. Avoids the full `LuaValue` clone that
2843    /// `get_at` performs — the operand is only needed for its `i64` payload.
2844    #[inline(always)]
2845    pub fn get_int_at(&self, idx: impl Into<StackIdxConv>) -> Option<i64> {
2846        let i: StackIdx = idx.into().0;
2847        match self.stack.get(i.0 as usize) {
2848            Some(slot) => match &slot.val {
2849                LuaValue::Int(v) => Some(*v),
2850                _ => None,
2851            },
2852            None => None,
2853        }
2854    }
2855    /// Hot-path accessor: returns `Some((a, b))` only when both stack slots
2856    /// at `rb` and `rc` hold integers. Equivalent to two `get_int_at` calls
2857    /// but is shaped so the arithmetic opcode dispatch arms can pattern-match
2858    /// the common case with a single `if let`.
2859    ///
2860    /// the `op_arith_aux` macro.
2861    #[inline(always)]
2862    pub fn get_int_pair_at(
2863        &self,
2864        rb: impl Into<StackIdxConv>,
2865        rc: impl Into<StackIdxConv>,
2866    ) -> Option<(i64, i64)> {
2867        let rb: StackIdx = rb.into().0;
2868        let rc: StackIdx = rc.into().0;
2869        match (self.stack[rb.0 as usize].val, self.stack[rc.0 as usize].val) {
2870            (LuaValue::Int(ib), LuaValue::Int(ic)) => Some((ib, ic)),
2871            _ => None,
2872        }
2873    }
2874    /// Hot-path accessor: returns `Some(f)` when the slot holds a `Float(f)`
2875    /// or coerces an `Int(i)` to `f64`. Returns `None` for any other tag.
2876    /// No `LuaValue` clone — only the primitive payload travels back.
2877    ///
2878    #[inline(always)]
2879    pub fn get_num_at(&self, idx: impl Into<StackIdxConv>) -> Option<f64> {
2880        let i: StackIdx = idx.into().0;
2881        match self.stack.get(i.0 as usize) {
2882            Some(slot) => match &slot.val {
2883                LuaValue::Float(f) => Some(*f),
2884                LuaValue::Int(v) => Some(*v as f64),
2885                _ => None,
2886            },
2887            None => None,
2888        }
2889    }
2890    /// Hot-path accessor: returns `Some(f)` only when the slot holds a
2891    /// `LuaValue::Float(f)`. Does NOT coerce integers; the integer branch is
2892    /// the caller's responsibility. Used by opcode arms that have already
2893    /// ruled out the integer fast path.
2894    #[inline(always)]
2895    pub fn get_float_at(&self, idx: impl Into<StackIdxConv>) -> Option<f64> {
2896        let i: StackIdx = idx.into().0;
2897        match self.stack.get(i.0 as usize) {
2898            Some(slot) => match &slot.val {
2899                LuaValue::Float(f) => Some(*f),
2900                _ => None,
2901            },
2902            None => None,
2903        }
2904    }
2905    /// Hot-path accessor: pair version of `get_num_at` — returns `Some((a,b))`
2906    /// when both slots coerce to `f64` (Float or Int), `None` if either does
2907    /// not. Used by the float fast path of the arith opcodes.
2908    ///
2909    #[inline(always)]
2910    pub fn get_num_pair_at(
2911        &self,
2912        rb: impl Into<StackIdxConv>,
2913        rc: impl Into<StackIdxConv>,
2914    ) -> Option<(f64, f64)> {
2915        let rb: StackIdx = rb.into().0;
2916        let rc: StackIdx = rc.into().0;
2917        match (self.stack[rb.0 as usize].val, self.stack[rc.0 as usize].val) {
2918            (LuaValue::Float(nb), LuaValue::Float(nc)) => Some((nb, nc)),
2919            (LuaValue::Int(ib), LuaValue::Int(ic)) => Some((ib as f64, ic as f64)),
2920            (LuaValue::Int(ib), LuaValue::Float(nc)) => Some((ib as f64, nc)),
2921            (LuaValue::Float(nb), LuaValue::Int(ic)) => Some((nb, ic as f64)),
2922            _ => None,
2923        }
2924    }
2925    /// Set `top` to an absolute stack index. Grows the backing stack vector
2926    /// (filling new slots with `Nil`) when `idx` is past `stack.len()`, but
2927    /// never clobbers existing slots between the old top and the new top —
2928    /// VM opcodes (Call, ForPrep, etc.) write registers via `set_at` and then
2929    /// raise `top` to signal "these are now live"; nil-filling here would
2930    /// erase the just-written values.
2931    ///
2932    /// setnilvalue(s2v(L->top.p++))` clear loop in `lua_settop` (lapi.c) is
2933    /// part of the public API path and lives in `api::set_top` instead.
2934    /// PORT NOTE: callers pass an absolute `StackIdx`, not the relative `idx`
2935    /// of the public `lua_settop`. The to-be-closed (`tbclist`) close path
2936    /// is Phase E and not handled here.
2937    #[inline(always)]
2938    pub fn set_top(&mut self, idx: impl Into<StackIdxConv>) {
2939        let new_top: StackIdx = idx.into().0;
2940        let new_top_u = new_top.0 as usize;
2941        if new_top_u > self.stack.len() {
2942            self.stack.resize_with(new_top_u, StackValue::default);
2943        }
2944        self.top = new_top;
2945    }
2946    /// Primitive "set top index" — just writes `self.top`, no nil-fill.
2947    ///
2948    /// PORT NOTE: callers (`api.rs::set_top`, `raw_set`, etc.) pre-nil-fill or
2949    /// only shrink, so this routine intentionally does no clearing or resizing.
2950    /// The to-be-closed (`tbclist`) close path is Phase E.
2951    #[inline(always)]
2952    pub fn set_top_idx(&mut self, idx: impl Into<StackIdxConv>) {
2953        let new_top: StackIdx = idx.into().0;
2954        self.top = new_top;
2955    }
2956    /// Decrement `top` by 1 (saturating at zero).
2957    ///
2958    #[inline(always)]
2959    pub fn dec_top(&mut self) {
2960        if self.top.0 > 0 {
2961            self.top = StackIdx(self.top.0 - 1);
2962        }
2963    }
2964    #[inline(always)]
2965    pub fn pop_n(&mut self, n: usize) {
2966        let cur = self.top.0 as usize;
2967        let new = cur.saturating_sub(n);
2968        self.top = StackIdx(new as u32);
2969    }
2970    /// Returns the value at the given stack index without removing it.
2971    ///
2972    #[inline(always)]
2973    pub fn peek_at(&mut self, idx: impl Into<StackIdxConv>) -> LuaValue {
2974        let i: StackIdx = idx.into().0;
2975        match self.stack.get(i.0 as usize) {
2976            Some(slot) => slot.val.clone(),
2977            None => LuaValue::Nil,
2978        }
2979    }
2980    /// Returns the value just below `top` (the topmost live slot) without
2981    /// removing it.
2982    ///
2983    #[inline(always)]
2984    pub fn peek_top(&mut self) -> LuaValue {
2985        if self.top.0 == 0 {
2986            return LuaValue::Nil;
2987        }
2988        self.stack[(self.top.0 - 1) as usize].val.clone()
2989    }
2990    /// Returns the topmost slot interpreted as a string. Panics if the slot
2991    /// is not a `LuaValue::Str`. Callers (e.g. `luaO_pushvfstring`) guarantee
2992    /// the value has been pushed as an interned string immediately prior.
2993    ///
2994    pub fn peek_string_at_top(&mut self) -> GcRef<LuaString> {
2995        match self.peek_top() {
2996            LuaValue::Str(s) => s,
2997            _ => panic!("peek_string_at_top: top of stack is not a string"),
2998        }
2999    }
3000    /// Mutable reference to the value at the given stack slot.
3001    ///
3002    pub fn stack_at(&mut self, idx: impl Into<StackIdxConv>) -> &mut LuaValue {
3003        let i: StackIdx = idx.into().0;
3004        &mut self.stack[i.0 as usize].val
3005    }
3006    /// Writes `Nil` to the given stack slot.
3007    ///
3008    pub fn stack_set_nil(&mut self, idx: impl Into<StackIdxConv>) {
3009        let i: StackIdx = idx.into().0;
3010        let slot = i.0 as usize;
3011        if slot < self.stack.len() {
3012            self.stack[slot].val = LuaValue::Nil;
3013        }
3014    }
3015    /// Resizes the underlying stack vector to `size` slots, padding new slots
3016    /// with `StackValue::default()` (which is `Nil`). Returns `Ok(())` on
3017    /// success — `Vec::resize_with` in Rust does not have a fallible path the
3018    /// way `luaM_reallocvector` does in C, so the `Result` is here for
3019    /// signature parity with future fallible allocators.
3020    ///
3021    /// newsize+EXTRA_STACK, StackValue)`.
3022    pub fn stack_resize(&mut self, size: usize) -> Result<(), LuaError> {
3023        self.stack.resize_with(size, StackValue::default);
3024        Ok(())
3025    }
3026    pub fn stack_available(&mut self) -> usize {
3027        (self.stack_last.0 as usize).saturating_sub(self.top.0 as usize)
3028    }
3029    pub fn check_stack(&mut self, n: i32) -> Result<(), LuaError> {
3030        let free = (self.stack_last.0 as i32) - (self.top.0 as i32);
3031        if free <= n {
3032            self.grow_stack(n, true)?;
3033        }
3034        Ok(())
3035    }
3036    /// Inherent method wrapper around the free function `do_::grow_stack`,
3037    /// preserving the historical `Result<(), LuaError>` signature used by
3038    /// `check_stack` and other VM call sites. The bool returned by the
3039    /// underlying implementation distinguishes soft failure (when
3040    /// `raise_error` is false) from success; that distinction is dropped here
3041    /// because every current caller passes `raise_error = true` and only
3042    /// cares about error propagation.
3043    ///
3044    pub fn grow_stack(&mut self, n: i32, raise_error: bool) -> Result<(), LuaError> {
3045        crate::do_::grow_stack(self, n, raise_error).map(|_| ())
3046    }
3047
3048    #[inline(always)]
3049    pub fn get_ci(&self, idx: CallInfoIdx) -> &CallInfo {
3050        &self.call_info[idx.as_usize()]
3051    }
3052    #[inline(always)]
3053    pub fn get_ci_mut(&mut self, idx: CallInfoIdx) -> &mut CallInfo {
3054        &mut self.call_info[idx.as_usize()]
3055    }
3056    #[inline(always)]
3057    pub fn current_call_info(&self) -> &CallInfo {
3058        &self.call_info[self.ci.as_usize()]
3059    }
3060    #[inline(always)]
3061    pub fn current_call_info_mut(&mut self) -> &mut CallInfo {
3062        let i = self.ci.as_usize();
3063        &mut self.call_info[i]
3064    }
3065    #[inline(always)]
3066    pub fn current_ci_idx(&self) -> CallInfoIdx {
3067        self.ci
3068    }
3069    pub fn call_stack_mut(&mut self) -> &mut Vec<CallInfo> {
3070        &mut self.call_info
3071    }
3072    #[inline(always)]
3073    pub fn next_ci(&mut self) -> Result<CallInfoIdx, LuaError> {
3074        let idx = match self.call_info[self.ci.as_usize()].next {
3075            Some(idx) => idx,
3076            None => extend_ci(self),
3077        };
3078        self.call_info[idx.as_usize()].tailcalls = 0;
3079        Ok(idx)
3080    }
3081
3082    /// Records that a Lua tail call reused the frame at `ci`, so the 5.1
3083    /// `debug.getstack` level walk can synthesize the `(tail call)` frames the
3084    /// 5.1 debug model exposes. Gated to 5.1: 5.2+ dropped synthetic tail frames
3085    /// for the `istailcall` flag and never read `tailcalls`, so on those versions
3086    /// this is a single version compare and return — the hot tail-call path is
3087    /// otherwise byte-identical.
3088    #[inline(always)]
3089    pub fn note_lua_tailcall(&mut self, ci: CallInfoIdx) {
3090        if self.global().lua_version == lua_types::LuaVersion::V51 {
3091            self.call_info[ci.as_usize()].tailcalls =
3092                self.call_info[ci.as_usize()].tailcalls.saturating_add(1);
3093        }
3094    }
3095    #[inline(always)]
3096    pub fn prev_ci(&self, idx: CallInfoIdx) -> Option<CallInfoIdx> {
3097        self.call_info[idx.as_usize()].previous
3098    }
3099    pub fn get_prev_ci(&self, idx: CallInfoIdx) -> Option<&CallInfo> {
3100        self.call_info[idx.as_usize()]
3101            .previous
3102            .map(|p| &self.call_info[p.as_usize()])
3103    }
3104    #[inline(always)]
3105    pub fn is_base_ci(&self, idx: CallInfoIdx) -> bool {
3106        idx.as_usize() == 0
3107    }
3108    #[inline(always)]
3109    pub fn is_current_ci(&self, idx: CallInfoIdx) -> bool {
3110        idx == self.ci
3111    }
3112    pub fn ci_next_func(&self, idx: CallInfoIdx) -> StackIdx {
3113        let next = self.call_info[idx.as_usize()]
3114            .next
3115            .expect("ci_next_func: no next CallInfo");
3116        self.call_info[next.as_usize()].func
3117    }
3118    #[inline(always)]
3119    pub fn ci_top(&self, idx: CallInfoIdx) -> StackIdx {
3120        self.call_info[idx.as_usize()].top
3121    }
3122    /// Hot-loop trap read: `(callstatus & CIST_TRAP) != 0`, one mask with no
3123    /// enum-discriminant branch. A C frame never has `CIST_TRAP` set, so this
3124    /// returns `false` for C frames identically to the pre-flatten fallback,
3125    /// without needing a frame-kind branch — that is the whole point of T2-C2.
3126    #[inline(always)]
3127    pub fn ci_trap(&mut self, idx: CallInfoIdx) -> bool {
3128        self.call_info[idx.as_usize()].trap()
3129    }
3130    #[inline(always)]
3131    pub fn ci_savedpc(&self, idx: CallInfoIdx) -> u32 {
3132        self.call_info[idx.as_usize()].saved_pc()
3133    }
3134    #[inline(always)]
3135    pub fn set_ci_savedpc(&mut self, idx: CallInfoIdx, pc: u32) {
3136        self.call_info[idx.as_usize()].set_saved_pc(pc);
3137    }
3138    #[inline(always)]
3139    pub fn set_ci_previous(&mut self, idx: CallInfoIdx) {
3140        self.ci = self.call_info[idx.as_usize()]
3141            .previous
3142            .expect("set_ci_previous: returning frame has no previous CallInfo");
3143    }
3144    #[inline(always)]
3145    pub fn ci_previous(&self, idx: CallInfoIdx) -> Option<CallInfoIdx> {
3146        self.call_info[idx.as_usize()].previous
3147    }
3148    #[inline(always)]
3149    pub fn ci_adjust_func(&mut self, idx: CallInfoIdx, delta: i32) {
3150        let ci = &mut self.call_info[idx.as_usize()];
3151        ci.func = StackIdx((ci.func.0 as i32 - delta) as u32);
3152    }
3153    #[inline(always)]
3154    pub fn ci_base(&self, idx: CallInfoIdx) -> StackIdx {
3155        self.call_info[idx.as_usize()].func + 1
3156    }
3157    #[inline(always)]
3158    pub fn ci_is_fresh(&self, idx: CallInfoIdx) -> bool {
3159        (self.call_info[idx.as_usize()].callstatus & CIST_FRESH) != 0
3160    }
3161    #[inline(always)]
3162    pub fn ci_lua_closure(
3163        &self,
3164        idx: CallInfoIdx,
3165    ) -> Option<GcRef<lua_types::closure::LuaLClosure>> {
3166        let func_idx = self.call_info[idx.as_usize()].func;
3167        match self.stack.get(func_idx.0 as usize).map(|slot| slot.val) {
3168            Some(LuaValue::Function(lua_types::closure::LuaClosure::Lua(cl))) => Some(cl),
3169            _ => None,
3170        }
3171    }
3172    #[inline(always)]
3173    pub fn ci_nextraargs(&self, idx: CallInfoIdx) -> i32 {
3174        self.call_info[idx.as_usize()].nextra_args()
3175    }
3176    #[inline(always)]
3177    pub fn ci_nres(&self, idx: CallInfoIdx) -> i32 {
3178        self.call_info[idx.as_usize()].u2.value
3179    }
3180    #[inline(always)]
3181    pub fn ci_nres_set(&mut self, idx: CallInfoIdx, n: i32) {
3182        self.call_info[idx.as_usize()].u2.value = n;
3183    }
3184    #[inline(always)]
3185    pub fn ci_nresults(&self, idx: CallInfoIdx) -> i32 {
3186        self.call_info[idx.as_usize()].nresults as i32
3187    }
3188    pub fn ci_prev_instruction(&self, idx: CallInfoIdx) -> lua_types::opcode::Instruction {
3189        let pc = self.call_info[idx.as_usize()].saved_pc();
3190        let cl = self
3191            .ci_lua_closure(idx)
3192            .expect("ci_prev_instruction: CallInfo does not hold a Lua closure");
3193        cl.proto.code[(pc - 1) as usize]
3194    }
3195    pub fn ci_prev2_instruction(&self, idx: CallInfoIdx) -> lua_types::opcode::Instruction {
3196        let pc = self.call_info[idx.as_usize()].saved_pc();
3197        let cl = self
3198            .ci_lua_closure(idx)
3199            .expect("ci_prev2_instruction: CallInfo does not hold a Lua closure");
3200        cl.proto.code[(pc - 2) as usize]
3201    }
3202    pub fn ci_skip_next_instruction(&mut self, idx: CallInfoIdx) {
3203        let pc = self.call_info[idx.as_usize()].saved_pc();
3204        self.call_info[idx.as_usize()].set_saved_pc(pc + 1);
3205    }
3206    pub fn ci_step_pc_back(&mut self, idx: CallInfoIdx) {
3207        let pc = self.call_info[idx.as_usize()].saved_pc();
3208        self.call_info[idx.as_usize()].set_saved_pc(pc - 1);
3209    }
3210    pub fn get_ci_pcrel(&mut self, idx: CallInfoIdx) -> u32 {
3211        self.call_info[idx.as_usize()].saved_pc().saturating_sub(1)
3212    }
3213    pub fn get_ci_u2_funcidx(&mut self, idx: CallInfoIdx) -> i32 {
3214        self.call_info[idx.as_usize()].u2.value
3215    }
3216    pub fn get_ci_u2_nres(&mut self, idx: CallInfoIdx) -> i32 {
3217        self.call_info[idx.as_usize()].u2.value
3218    }
3219    pub fn get_ci_u2_nyield(&mut self, idx: CallInfoIdx) -> i32 {
3220        self.call_info[idx.as_usize()].u2.value
3221    }
3222    pub fn get_ci_vararg_info(&mut self, idx: CallInfoIdx) -> (bool, i32, i32) {
3223        let nextraargs = self.call_info[idx.as_usize()].nextra_args();
3224        match self.ci_lua_closure(idx) {
3225            Some(cl) => (cl.proto.is_vararg, nextraargs, cl.proto.numparams as i32),
3226            None => (false, nextraargs, 0),
3227        }
3228    }
3229    pub fn get_ci_lua_proto_numparams(&mut self, idx: CallInfoIdx) -> u8 {
3230        self.ci_lua_closure(idx)
3231            .map(|cl| cl.proto.numparams)
3232            .unwrap_or(0)
3233    }
3234    pub fn set_ci_u2_nres(&mut self, idx: CallInfoIdx, n: i32) {
3235        self.call_info[idx.as_usize()].u2.value = n;
3236    }
3237    pub fn set_ci_u2_nyield(&mut self, idx: CallInfoIdx, n: i32) {
3238        self.call_info[idx.as_usize()].u2.value = n;
3239    }
3240    pub fn set_ci_transfer_info(&mut self, idx: CallInfoIdx, ftransfer: u16, ntransfer: u16) {
3241        let ci = &mut self.call_info[idx.as_usize()];
3242        ci.u2.ftransfer = ftransfer;
3243        ci.u2.ntransfer = ntransfer;
3244    }
3245    pub fn shrink_ci(&mut self) {
3246        shrink_ci(self)
3247    }
3248    pub fn check_c_stack(&mut self) -> Result<(), LuaError> {
3249        check_c_stack(self)
3250    }
3251
3252    pub fn status(&mut self) -> LuaStatus {
3253        LuaStatus::from_raw(self.status as i32)
3254    }
3255    pub fn errfunc(&mut self) -> isize {
3256        self.errfunc
3257    }
3258    pub fn old_pc(&mut self) -> u32 {
3259        self.oldpc
3260    }
3261    pub fn set_old_pc(&mut self, pc: u32) {
3262        self.oldpc = pc;
3263    }
3264    pub fn set_oldpc(&mut self, pc: u32) {
3265        self.oldpc = pc;
3266    }
3267    pub fn _hook_call_noargs(&mut self) {}
3268    pub fn hook(&self) -> Option<&Box<dyn FnMut(&mut LuaState, &crate::debug::LuaDebug)>> {
3269        self.hook.as_ref()
3270    }
3271    pub fn has_hook(&mut self) -> bool {
3272        self.hook.is_some()
3273    }
3274    pub fn hook_count(&mut self) -> i32 {
3275        self.hookcount
3276    }
3277    pub fn set_hook_count(&mut self, n: i32) {
3278        self.hookcount = n;
3279    }
3280    pub fn hook_mask(&self) -> u8 {
3281        self.hookmask
3282    }
3283    pub fn set_hook_mask(&mut self, m: u8) {
3284        self.hookmask = m;
3285    }
3286    pub fn base_hook_count(&self) -> i32 {
3287        self.basehookcount
3288    }
3289    pub fn set_base_hook_count(&mut self, n: i32) {
3290        self.basehookcount = n;
3291    }
3292    pub fn set_hook(&mut self, h: Option<Box<dyn FnMut(&mut LuaState, &crate::debug::LuaDebug)>>) {
3293        self.hook = h;
3294    }
3295    /// Fire a count or line hook on the currently executing frame.
3296    ///
3297    /// C-Lua's `luaG_traceexec` is the sole caller of `luaD_hook` for count and
3298    /// line events, and it relies on `L->ci` being the same Lua frame after the
3299    /// hook as before it: the bytecode dispatch loop in `luaV_execute` reads
3300    /// `L->ci` (via `trace_exec`) on the next instruction and writes its
3301    /// `savedpc`, which is only valid on a Lua frame. C maintains that invariant
3302    /// implicitly — the unprotected `lua_call` inside the hook restores `L->ci`
3303    /// via `luaD_poscall` on success, and on a hook error it `longjmp`s straight
3304    /// to the nearest protected boundary, which resets `L->ci`, so a corrupted
3305    /// `ci` never re-enters the dispatch loop.
3306    ///
3307    /// Our port reports hook-callback errors through a different path that can
3308    /// leave `self.ci` advanced to the hook's own (C) frame instead of unwinding
3309    /// it, so the invariant the dispatch loop depends on would be broken. Saving
3310    /// `self.ci` here and restoring it after the hook reasserts exactly the
3311    /// invariant C guarantees, so the next `trace_exec` writes `savedpc` on the
3312    /// Lua frame it is actually executing rather than panicking on a C frame.
3313    pub fn call_hook_event(&mut self, event: i32, line: i32) -> Result<(), LuaError> {
3314        let saved_ci = self.ci;
3315        let r = crate::do_::hook(self, event, line, 0, 0);
3316        self.ci = saved_ci;
3317        r
3318    }
3319
3320    pub fn registry_value(&self) -> LuaValue {
3321        self.global().l_registry.clone()
3322    }
3323    pub fn registry_get(&self, key: usize) -> LuaValue {
3324        let reg = self.global().l_registry.clone();
3325        match reg {
3326            LuaValue::Table(t) => t.get(&LuaValue::Int(key as i64)),
3327            _ => LuaValue::Nil,
3328        }
3329    }
3330
3331    pub fn new_string(&mut self, bytes: &[u8]) -> Result<GcRef<LuaString>, LuaError> {
3332        self.intern_or_create_str(bytes)
3333    }
3334
3335    // ── Phase D-1a: state-owned allocation API ──────────────────────────────
3336    // These methods are the canonical allocation surface. They wrap
3337    // `GcRef::new` today; at D-1e they route through `state.global.heap.allocate`.
3338    // Callers must reach them through `&mut LuaState`, which mirrors C-Lua's
3339    // requirement that every allocation passes `lua_State *L`.
3340
3341    /// Allocate a new Lua function prototype.
3342    ///
3343    /// Caller mutates the returned proto in place (it's behind GcRef, which is
3344    /// Rc during Phase D-1; mutable access via `Rc::get_mut` only works while
3345    /// no other GcRefs alias it — true at construction).
3346    pub fn new_proto(&mut self) -> GcRef<LuaProto> {
3347        self.mark_gc_check_needed();
3348        GcRef::new(LuaProto::placeholder())
3349    }
3350
3351    /// Allocate a Lua-side closure (compiled function + upvalue slots).
3352    pub fn new_lclosure(&mut self, proto: GcRef<LuaProto>, nupvals: usize) -> GcRef<LuaClosureLua> {
3353        self.mark_gc_check_needed();
3354        let mut upvals = Vec::with_capacity(nupvals);
3355        for _ in 0..nupvals {
3356            upvals.push(std::cell::Cell::new(self.new_upval_closed(LuaValue::Nil)));
3357        }
3358        let upvals = upvals.into_boxed_slice();
3359        let closure = GcRef::new(LuaClosureLua { proto, upvals });
3360        closure.account_buffer(closure.buffer_bytes() as isize);
3361        closure
3362    }
3363
3364    /// Allocate a closed upvalue holding the given value.
3365    pub fn new_upval_closed(&mut self, v: LuaValue) -> GcRef<UpVal> {
3366        self.mark_gc_check_needed();
3367        GcRef::new(UpVal::closed(v))
3368    }
3369
3370    /// Allocate an open upvalue referring to a thread's stack slot.
3371    pub fn new_upval_open(&mut self, thread_id: usize, level: StackIdx) -> GcRef<UpVal> {
3372        self.mark_gc_check_needed();
3373        self.legacy_open_upval_touched.set(true);
3374        GcRef::new(UpVal::open(thread_id, level))
3375    }
3376    /// Mirrors `luaS_newlstr`: short strings are interned globally so equal
3377    /// content shares a single TString; long strings (> LUAI_MAXSHORTLEN = 40)
3378    /// always create a fresh TString without interning. This is what lets
3379    /// `string.format("%p", "long" .. "concat")` differ from a same-content
3380    /// literal — concat must produce a new object even when the literal already
3381    /// lives in the lexer's constant pool.
3382    pub fn intern_or_create_str(&mut self, bytes: &[u8]) -> Result<GcRef<LuaString>, LuaError> {
3383        self.intern_str(bytes)
3384    }
3385    pub fn new_userdata(
3386        &mut self,
3387        _size: usize,
3388        _nuvalue: usize,
3389    ) -> Result<GcRef<LuaUserData>, LuaError> {
3390        Err(LuaError::runtime(format_args!(
3391            "new_userdata not implemented in this Phase-B build; use new_userdata_typed instead"
3392        )))
3393    }
3394    pub fn new_c_closure(&mut self, _f: LuaCFunction, _n: i32) -> Result<LuaClosure, LuaError> {
3395        Err(LuaError::runtime(format_args!("new_c_closure not implemented in this Phase-B build; use push_cclosure in lua_vm::api instead")))
3396    }
3397    pub fn push_closure(
3398        &mut self,
3399        proto_idx: usize,
3400        ci: CallInfoIdx,
3401        base: StackIdx,
3402        ra: StackIdx,
3403    ) -> Result<(), LuaError> {
3404        let parent_cl = self
3405            .ci_lua_closure(ci)
3406            .expect("push_closure: current frame is not a Lua closure");
3407        let child_proto = parent_cl.proto.p[proto_idx].clone();
3408        let nup = child_proto.upvalues.len();
3409        let mut upvals: Vec<std::cell::Cell<GcRef<UpVal>>> = Vec::with_capacity(nup);
3410        for i in 0..nup {
3411            let desc = &child_proto.upvalues[i];
3412            let uv = if desc.instack {
3413                let level = base + desc.idx as i32;
3414                crate::func::find_upval(self, level)
3415            } else {
3416                parent_cl.upval(desc.idx as usize)
3417            };
3418            upvals.push(std::cell::Cell::new(uv));
3419        }
3420        // LUA_COMPAT closure caching (5.2/5.3 only): if the last closure built
3421        // from this proto captured the identical upvalues, reuse it so the two
3422        // compare `==` (C's `getcached`). 5.1 never cached; 5.4/5.5 removed it.
3423        let cache_enabled = matches!(
3424            self.global().lua_version,
3425            lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53
3426        );
3427        if cache_enabled {
3428            if let Some(cached) = child_proto.cache.borrow().as_ref() {
3429                if cached.upvals.len() == nup
3430                    && (0..nup).all(|i| GcRef::ptr_eq(&cached.upvals[i].get(), &upvals[i].get()))
3431                {
3432                    let reused = cached.clone();
3433                    self.set_at(ra, LuaValue::Function(LuaClosure::Lua(reused)));
3434                    return Ok(());
3435                }
3436            }
3437        }
3438        // TODO(D-1c-bridge): upvals are pre-populated from parent frame; state.new_lclosure
3439        // fills with fresh Nil upvals which would drop the captured bindings.
3440        self.mark_gc_check_needed();
3441        let new_cl = GcRef::new(LuaClosureLua {
3442            proto: child_proto.clone(),
3443            upvals: upvals.into_boxed_slice(),
3444        });
3445        new_cl.account_buffer(new_cl.buffer_bytes() as isize);
3446        if cache_enabled {
3447            *child_proto.cache.borrow_mut() = Some(new_cl.clone());
3448        }
3449        self.set_at(ra, LuaValue::Function(LuaClosure::Lua(new_cl)));
3450        Ok(())
3451    }
3452    pub fn new_tbc_upval(&mut self, idx: StackIdx) -> Result<(), LuaError> {
3453        crate::func::new_tbc_upval(self, idx)
3454    }
3455
3456    /// Read an open or closed upvalue.
3457    ///
3458    /// Closed upvalues own their value and read trivially. Open upvalues
3459    /// point at a stack slot on the home thread that captured them.
3460    ///
3461    /// Resolution order for an open upvalue whose home is not the current
3462    /// thread:
3463    ///
3464    /// 1. If the home thread is registered in `GlobalState::threads` and
3465    ///    its `RefCell` is currently borrowable, read straight from its
3466    ///    stack. This is the path used when the main thread reads a
3467    ///    closure created inside a now-suspended coroutine, or when one
3468    ///    coroutine reads an upvalue homed on a sibling suspended
3469    ///    coroutine.
3470    /// 2. Otherwise fall back to `GlobalState::cross_thread_upvals`. This
3471    ///    is the path used while inside a `coroutine.resume`: the parent
3472    ///    thread's `LuaState` is held by an outer `&mut` and is not
3473    ///    reachable through any `Rc<RefCell<_>>`, so `aux_resume`
3474    ///    snapshots the parent's open upvalues into the mirror across the
3475    ///    resume boundary.
3476    #[inline(always)]
3477    pub fn upvalue_get(&self, cl: &GcRef<LuaClosureLua>, n: usize) -> LuaValue {
3478        let uv = cl.upval(n);
3479        let (thread_id, idx) = match uv.try_open_payload() {
3480            Some(p) => p,
3481            None => return uv.closed_value(),
3482        };
3483        let current = self.cached_thread_id;
3484        let tid = thread_id as u64;
3485        if tid == current {
3486            return self.stack[idx.0 as usize].val;
3487        }
3488        self.upvalue_get_cross_thread(tid, idx)
3489    }
3490
3491    #[cold]
3492    #[inline(never)]
3493    fn upvalue_get_cross_thread(&self, tid: u64, idx: StackIdx) -> LuaValue {
3494        let entry_rc = {
3495            let g = self.global();
3496            g.threads.get(&tid).map(|e| e.state.clone())
3497        };
3498        if let Some(rc) = entry_rc {
3499            if let Ok(home_state) = rc.try_borrow() {
3500                return home_state.get_at(idx);
3501            }
3502        }
3503        let g = self.global();
3504        match g.cross_thread_upvals.get(&(tid, idx)) {
3505            Some(v) => *v,
3506            None => LuaValue::Nil,
3507        }
3508    }
3509    /// Write an open or closed upvalue.
3510    ///
3511    /// Mirrors [`upvalue_get`]: open upvalues homed on the current thread
3512    /// write through `self.stack`. For cross-thread open upvalues, the
3513    /// home thread's stack is written directly when its `RefCell` is
3514    /// borrowable, otherwise the write lands in
3515    /// `GlobalState::cross_thread_upvals` (the active-resume case where
3516    /// the home thread is borrow-locked further up the call stack).
3517    #[inline(always)]
3518    pub fn upvalue_set(
3519        &mut self,
3520        cl: &GcRef<LuaClosureLua>,
3521        n: usize,
3522        val: LuaValue,
3523    ) -> Result<(), LuaError> {
3524        let uv = cl.upval(n);
3525        match uv.try_open_payload() {
3526            Some((thread_id, idx)) => {
3527                let tid = thread_id as u64;
3528                let current = self.cached_thread_id;
3529                if tid == current {
3530                    self.stack[idx.0 as usize].val = val;
3531                } else {
3532                    self.upvalue_set_cross_thread(tid, idx, val)?;
3533                }
3534            }
3535            None => {
3536                uv.set_closed_value(val);
3537            }
3538        }
3539        if val.is_collectable() {
3540            self.gc_barrier_upval(&uv, &val);
3541        }
3542        Ok(())
3543    }
3544
3545    #[cold]
3546    #[inline(never)]
3547    fn upvalue_set_cross_thread(
3548        &mut self,
3549        tid: u64,
3550        idx: StackIdx,
3551        val: LuaValue,
3552    ) -> Result<(), LuaError> {
3553        let entry_rc = {
3554            let g = self.global();
3555            g.threads.get(&tid).map(|e| e.state.clone())
3556        };
3557        if let Some(rc) = entry_rc {
3558            if let Ok(mut home_state) = rc.try_borrow_mut() {
3559                home_state.set_at(idx, val);
3560                return Ok(());
3561            }
3562        }
3563        let mut g = self.global_mut();
3564        g.cross_thread_upvals.insert((tid, idx), val);
3565        Ok(())
3566    }
3567
3568    pub fn protected_call_raw(
3569        &mut self,
3570        func: StackIdx,
3571        nresults: i32,
3572        errfunc: StackIdx,
3573    ) -> Result<(), LuaError> {
3574        let ef = errfunc.0 as isize;
3575        let status = crate::do_::pcall(self, |s| s.call_no_yield(func, nresults), func, ef);
3576        match status {
3577            LuaStatus::Ok => Ok(()),
3578            LuaStatus::ErrSyntax => {
3579                let err_val = self.get_at(func);
3580                self.set_top(func);
3581                Err(LuaError::Syntax(err_val))
3582            }
3583            LuaStatus::Yield => {
3584                self.set_top(func);
3585                Err(LuaError::Yield)
3586            }
3587            _ => {
3588                let err_val = self.get_at(func);
3589                self.set_top(func);
3590                Err(LuaError::Runtime(err_val))
3591            }
3592        }
3593    }
3594    pub fn protected_parser(
3595        &mut self,
3596        z: crate::zio::ZIO,
3597        name: &[u8],
3598        mode: Option<&[u8]>,
3599    ) -> LuaStatus {
3600        crate::do_::protected_parser(self, z, name, mode)
3601    }
3602    pub fn do_call(&mut self, func: StackIdx, nresults: i32) -> Result<(), LuaError> {
3603        crate::do_::call(self, func, nresults)
3604    }
3605    pub fn do_call_no_yield(&mut self, func: StackIdx, nresults: i32) -> Result<(), LuaError> {
3606        crate::do_::callnoyield(self, func, nresults)
3607    }
3608    pub fn call_no_yield(&mut self, func: StackIdx, nresults: i32) -> Result<(), LuaError> {
3609        crate::do_::callnoyield(self, func, nresults)
3610    }
3611    pub fn call_at(&mut self, func: StackIdx, nresults: i32) -> Result<(), LuaError> {
3612        crate::do_::call(self, func, nresults)
3613    }
3614    #[inline(always)]
3615    pub fn call_known_c_at(&mut self, func: StackIdx, nresults: i32) -> Result<bool, LuaError> {
3616        crate::do_::call_known_c(self, func, nresults)
3617    }
3618    #[inline(always)]
3619    pub fn precall(
3620        &mut self,
3621        func: StackIdx,
3622        nresults: i32,
3623    ) -> Result<Option<CallInfoIdx>, LuaError> {
3624        crate::do_::precall(self, func, nresults)
3625    }
3626    #[inline(always)]
3627    pub fn pretailcall(
3628        &mut self,
3629        ci: CallInfoIdx,
3630        func: StackIdx,
3631        narg1: i32,
3632        delta: i32,
3633    ) -> Result<i32, LuaError> {
3634        crate::do_::pretailcall(self, ci, func, narg1, delta)
3635    }
3636    #[inline(always)]
3637    pub fn poscall<N: TryInto<i32>>(&mut self, ci: CallInfoIdx, nres: N) -> Result<(), LuaError>
3638    where
3639        <N as TryInto<i32>>::Error: std::fmt::Debug,
3640    {
3641        let n = nres.try_into().expect("poscall: nres out of i32 range");
3642        crate::do_::poscall(self, ci, n)
3643    }
3644    pub fn adjust_results(&mut self, nresults: i32) {
3645        const LUA_MULTRET: i32 = -1;
3646        if nresults <= LUA_MULTRET {
3647            let ci_idx = self.ci.as_usize();
3648            if self.call_info[ci_idx].top.0 < self.top.0 {
3649                self.call_info[ci_idx].top = self.top;
3650            }
3651        }
3652    }
3653    pub fn adjust_varargs(
3654        &mut self,
3655        ci: CallInfoIdx,
3656        nfixparams: i32,
3657        cl: &GcRef<lua_types::closure::LuaLClosure>,
3658    ) -> Result<(), LuaError> {
3659        crate::tagmethods::adjust_varargs(self, nfixparams, ci, &cl.0.proto)
3660    }
3661    pub fn get_varargs(&mut self, ci: CallInfoIdx, ra: StackIdx, n: i32) -> Result<i32, LuaError> {
3662        crate::tagmethods::get_varargs(self, ci, ra, n)?;
3663        Ok(0)
3664    }
3665
3666    pub fn close_upvals(&mut self, level: StackIdx) -> Result<(), LuaError> {
3667        crate::func::close_upval(self, level);
3668        Ok(())
3669    }
3670    pub fn close_upvals_status(&mut self, level: StackIdx, _status: i32) -> Result<(), LuaError> {
3671        crate::func::close_upval(self, level);
3672        Ok(())
3673    }
3674    pub fn close_upvals_from_base(&mut self, ci: CallInfoIdx) -> Result<(), LuaError> {
3675        let base = self.ci_base(ci);
3676        crate::func::close_upval(self, base);
3677        Ok(())
3678    }
3679
3680    pub fn arith_op(
3681        &mut self,
3682        op: i32,
3683        p1: &LuaValue,
3684        p2: &LuaValue,
3685    ) -> Result<LuaValue, LuaError> {
3686        let arith_op = match op {
3687            0 => lua_types::arith::ArithOp::Add,
3688            1 => lua_types::arith::ArithOp::Sub,
3689            2 => lua_types::arith::ArithOp::Mul,
3690            3 => lua_types::arith::ArithOp::Mod,
3691            4 => lua_types::arith::ArithOp::Pow,
3692            5 => lua_types::arith::ArithOp::Div,
3693            6 => lua_types::arith::ArithOp::Idiv,
3694            7 => lua_types::arith::ArithOp::Band,
3695            8 => lua_types::arith::ArithOp::Bor,
3696            9 => lua_types::arith::ArithOp::Bxor,
3697            10 => lua_types::arith::ArithOp::Shl,
3698            11 => lua_types::arith::ArithOp::Shr,
3699            12 => lua_types::arith::ArithOp::Unm,
3700            13 => lua_types::arith::ArithOp::Bnot,
3701            _ => return Err(LuaError::runtime(format_args!("invalid arith op {}", op))),
3702        };
3703        let mut res = LuaValue::Nil;
3704        if crate::object::raw_arith(self, arith_op, p1, p2, &mut res)? {
3705            Ok(res)
3706        } else {
3707            Err(LuaError::arith_error(p1, p2, "perform arithmetic on"))
3708        }
3709    }
3710    pub fn concat(&mut self, n: i32) -> Result<(), LuaError> {
3711        crate::vm::concat(self, n)
3712    }
3713    pub fn less_than(&mut self, l: &LuaValue, r: &LuaValue) -> Result<bool, LuaError> {
3714        crate::vm::less_than(self, l, r)
3715    }
3716    pub fn less_equal(&mut self, l: &LuaValue, r: &LuaValue) -> Result<bool, LuaError> {
3717        crate::vm::less_equal(self, l, r)
3718    }
3719    pub fn equal_obj(&self, _ctx: Option<&LuaValue>, l: &LuaValue, r: &LuaValue) -> bool {
3720        crate::vm::equal_obj(None, l, r).unwrap_or(false)
3721    }
3722    pub fn equal_obj_with_tm(&mut self, l: &LuaValue, r: &LuaValue) -> Result<bool, LuaError> {
3723        crate::vm::equal_obj(Some(self), l, r)
3724    }
3725    pub fn obj_len(&mut self, v: &LuaValue) -> Result<LuaValue, LuaError> {
3726        match v {
3727            LuaValue::Table(_) => {
3728                // Lua 5.1 `#t` ignores a table `__len` metamethod (table
3729                // `__len` is 5.2+); always use the primitive length under V51.
3730                let consult_len_tm =
3731                    !matches!(self.global().lua_version, lua_types::LuaVersion::V51);
3732                let tm = if consult_len_tm {
3733                    let mt = self.table_metatable(v);
3734                    self.fast_tm_table(mt.as_ref(), TagMethod::Len)
3735                } else {
3736                    LuaValue::Nil
3737                };
3738                if matches!(tm, LuaValue::Nil) {
3739                    let n = self.table_length(v)?;
3740                    return Ok(LuaValue::Int(n));
3741                }
3742                self.push(LuaValue::Nil);
3743                let slot = StackIdx(self.top.0 - 1);
3744                crate::tagmethods::call_tm_res(self, tm, v.clone(), v.clone(), slot)?;
3745                Ok(self.pop())
3746            }
3747            LuaValue::Str(s) => Ok(LuaValue::Int(s.len() as i64)),
3748            other => {
3749                let tm = crate::tagmethods::get_tm_by_obj(
3750                    self,
3751                    other,
3752                    crate::tagmethods::TagMethod::Len,
3753                );
3754                if matches!(tm, LuaValue::Nil) {
3755                    let mut msg = b"attempt to get length of a ".to_vec();
3756                    msg.extend_from_slice(&self.obj_type_name(other));
3757                    msg.extend_from_slice(b" value");
3758                    return Err(crate::debug::prefixed_runtime_pub(self, msg));
3759                }
3760                self.push(LuaValue::Nil);
3761                let slot = StackIdx(self.top.0 - 1);
3762                crate::tagmethods::call_tm_res(self, tm, v.clone(), v.clone(), slot)?;
3763                Ok(self.pop())
3764            }
3765        }
3766    }
3767    pub fn obj_to_string(&mut self, idx: i32) -> Result<GcRef<LuaString>, LuaError> {
3768        let slot: StackIdx = if idx > 0 {
3769            let ci_func = self.current_call_info().func;
3770            ci_func + idx
3771        } else {
3772            debug_assert!(idx != 0, "invalid index");
3773            StackIdx((self.top_idx().0 as i32 + idx) as u32)
3774        };
3775        let val = self.get_at(slot);
3776        match val {
3777            LuaValue::Str(s) => Ok(s),
3778            LuaValue::Int(_) | LuaValue::Float(_) => {
3779                let s = crate::object::num_to_string(self, &val)?;
3780                self.set_at(slot, LuaValue::Str(s.clone()));
3781                Ok(s)
3782            }
3783            _ => Err(LuaError::type_error(&val, "convert to string")),
3784        }
3785    }
3786    pub fn coerce_to_string(&mut self, idx: StackIdx) -> Result<GcRef<LuaString>, LuaError> {
3787        let val = self.get_at(idx);
3788        match val {
3789            LuaValue::Str(s) => Ok(s),
3790            LuaValue::Int(_) | LuaValue::Float(_) => {
3791                let s = crate::object::num_to_string(self, &val)?;
3792                self.set_at(idx, LuaValue::Str(s.clone()));
3793                Ok(s)
3794            }
3795            _ => Err(LuaError::type_error(&val, "convert to string")),
3796        }
3797    }
3798    /// Parses `s` as a Lua number, returning `(value, consumed)` on success.
3799    ///
3800    /// `object::str2num` is number-model-blind and yields an integer whenever
3801    /// the text parses as one. The float-only versions (5.1/5.2) have no integer
3802    /// subtype, so a string coercion there is normalised to a float: otherwise
3803    /// `tonumber("10")` would carry an integer payload that diverges from the
3804    /// reference on any path that inspects the subtype.
3805    pub fn str_to_num(&mut self, s: &[u8]) -> Option<(LuaValue, usize)> {
3806        let mut out = LuaValue::Nil;
3807        let float_only =
3808            self.global().lua_version.number_model() == lua_types::NumberModel::FloatOnly;
3809        let sz = if float_only {
3810            crate::object::str2num_float_only(s, &mut out)
3811        } else {
3812            crate::object::str2num(s, &mut out)
3813        };
3814        if sz == 0 {
3815            return None;
3816        }
3817        Some((out, sz))
3818    }
3819
3820    #[inline(always)]
3821    pub fn fast_get(&mut self, t: &LuaValue, k: &LuaValue) -> Result<Option<LuaValue>, LuaError> {
3822        let LuaValue::Table(tbl) = t else {
3823            return Ok(None);
3824        };
3825        let v = tbl.get(k);
3826        if matches!(v, LuaValue::Nil) {
3827            Ok(None)
3828        } else {
3829            Ok(Some(v))
3830        }
3831    }
3832    #[inline(always)]
3833    pub fn fast_get_int(&mut self, t: &LuaValue, k: i64) -> Result<Option<LuaValue>, LuaError> {
3834        let LuaValue::Table(tbl) = t else {
3835            return Ok(None);
3836        };
3837        let v = tbl.get_int(k);
3838        if matches!(v, LuaValue::Nil) {
3839            Ok(None)
3840        } else {
3841            Ok(Some(v))
3842        }
3843    }
3844    #[inline(always)]
3845    pub fn fast_get_short_str(
3846        &mut self,
3847        t: &LuaValue,
3848        k: &LuaValue,
3849    ) -> Result<Option<LuaValue>, LuaError> {
3850        let LuaValue::Table(tbl) = t else {
3851            return Ok(None);
3852        };
3853        let LuaValue::Str(s) = k else {
3854            return Ok(None);
3855        };
3856        let v = tbl.get_short_str(s);
3857        if matches!(v, LuaValue::Nil) {
3858            Ok(None)
3859        } else {
3860            Ok(Some(v))
3861        }
3862    }
3863    #[inline(always)]
3864    pub fn fast_tm_table(&mut self, t: Option<&GcRef<LuaTable>>, tm: TagMethod) -> LuaValue {
3865        let Some(mt) = t else {
3866            return LuaValue::Nil;
3867        };
3868        debug_assert!((tm as u8) <= TagMethod::Eq as u8);
3869        let ename = self.global().tmname[tm as usize].clone();
3870        mt.get_short_str(&ename)
3871    }
3872    pub fn fast_tm_ud(&mut self, u: &GcRef<LuaUserData>, tm: TagMethod) -> LuaValue {
3873        // metatable then index by the interned `__xxx` name.
3874        let mt = u.metatable();
3875        self.fast_tm_table(mt.as_ref(), tm)
3876    }
3877
3878    pub fn table_get_with_tm(&mut self, t: &LuaValue, k: &LuaValue) -> Result<LuaValue, LuaError> {
3879        // Fast path: when the table has no metatable, `__index` can never
3880        // fire — so we can return the raw slot value (Nil if absent) without
3881        // routing through finish_get's push/pop scaffolding. Halves the
3882        // get-hot-path cost on tables without metamethods, which is the
3883        // common case in table.remove/insert shift loops and most user code.
3884        if let LuaValue::Table(tbl) = t {
3885            if !tbl.has_metatable() {
3886                return Ok(tbl.get(k));
3887            }
3888        }
3889        if let Some(v) = self.fast_get(t, k)? {
3890            return Ok(v);
3891        }
3892        let res = self.top_idx();
3893        self.push(LuaValue::Nil);
3894        crate::vm::finish_get(self, t.clone(), k.clone(), res, true, None, None)?;
3895        let value = self.get_at(res);
3896        self.pop();
3897        Ok(value)
3898    }
3899    /// Set `t[k] = v` with `__newindex` metamethod awareness.
3900    ///
3901    /// Fast path: when the table has no metatable, `__newindex` can never
3902    /// fire, so the existence check via `fast_get` is pure waste —
3903    /// `try_raw_set` handles both "key exists" and "key absent" cases via
3904    /// a single lookup internally. Removing the `fast_get` halves the
3905    /// lookups per set on the metamethod-free path (table.remove/insert
3906    /// hot loops, most user code).
3907    ///
3908    /// The GC backward barrier is invoked before the store (with `&v`)
3909    /// instead of after; the barrier only inspects the value's color, not
3910    /// its location, so the order is semantically equivalent to upstream
3911    /// C-Lua and lets us move `v` straight into `table_raw_set` without
3912    /// the extra `v.clone()` that the post-store ordering forced.
3913    #[inline]
3914    pub fn table_set_with_tm(
3915        &mut self,
3916        t: &LuaValue,
3917        k: LuaValue,
3918        v: LuaValue,
3919    ) -> Result<(), LuaError> {
3920        if let LuaValue::Table(tbl) = t {
3921            if !tbl.has_metatable() {
3922                self.gc_table_barrier_back(tbl, &v);
3923                return self.table_raw_set(t, k, v);
3924            }
3925        }
3926        if self.fast_get(t, &k)?.is_some() {
3927            self.gc_value_barrier_back(t, &v);
3928            return self.table_raw_set(t, k, v);
3929        }
3930        crate::vm::finish_set(self, t.clone(), k, v, true, None, None)
3931    }
3932    #[inline]
3933    pub fn table_raw_set(
3934        &mut self,
3935        t: &LuaValue,
3936        k: LuaValue,
3937        v: LuaValue,
3938    ) -> Result<(), LuaError> {
3939        let LuaValue::Table(tbl) = t else {
3940            return Err(LuaError::type_error(t, "index"));
3941        };
3942        let tbl = tbl.clone();
3943        tbl.raw_set(self, k, v)
3944    }
3945    #[inline]
3946    pub fn table_array_set(
3947        &mut self,
3948        t: &LuaValue,
3949        idx: usize,
3950        v: LuaValue,
3951    ) -> Result<(), LuaError> {
3952        let LuaValue::Table(tbl) = t else {
3953            return Err(LuaError::type_error(t, "index"));
3954        };
3955        let tbl = tbl.clone();
3956        tbl.raw_set_int(self, idx as i64 + 1, v)
3957    }
3958    pub fn table_ensure_array(&mut self, t: &LuaValue, n: usize) -> Result<(), LuaError> {
3959        let LuaValue::Table(tbl) = t else {
3960            return Err(LuaError::type_error(t, "index"));
3961        };
3962        if n > tbl.array_len() {
3963            tbl.resize(self, n, 0)?;
3964        }
3965        Ok(())
3966    }
3967    pub fn table_length(&mut self, t: &LuaValue) -> Result<i64, LuaError> {
3968        let LuaValue::Table(tbl) = t else {
3969            return Err(LuaError::type_error(t, "get length of"));
3970        };
3971        Ok(tbl.getn() as i64)
3972    }
3973    pub fn table_metatable(&mut self, v: &LuaValue) -> Option<GcRef<LuaTable>> {
3974        match v {
3975            LuaValue::Table(t) => t.metatable(),
3976            LuaValue::UserData(u) => u.metatable(),
3977            other => {
3978                let idx = other.base_type() as usize;
3979                self.global().mt[idx].clone()
3980            }
3981        }
3982    }
3983    pub fn table_resize(
3984        &mut self,
3985        t: &GcRef<LuaTable>,
3986        na: usize,
3987        nh: usize,
3988    ) -> Result<(), LuaError> {
3989        self.mark_gc_check_needed();
3990        t.resize(self, na, nh)
3991    }
3992    pub fn table_getn(&self, t: &GcRef<LuaTable>) -> i64 {
3993        // PORT NOTE: C's `luaH_getn` returns a boundary i such that t[i] is
3994        // present and t[i+1] is absent (or 0 if t[1] is absent), exploiting the
3995        // hybrid array+hash layout. Phase B's LuaTable (lua-types/src/value.rs)
3996        // is a flat Vec<(K,V)> with no array part, so we linearly probe integer
3997        // keys starting at 1. The rich array+hash impl in
3998        // crates/lua-vm/src/table.rs lights up in Phase D.
3999        // PERF(port): O(n) linear scan with O(n) lookups → O(n²); Phase D fixes.
4000        let mut i: i64 = 1;
4001        loop {
4002            let v = t.get_int(i);
4003            if matches!(v, LuaValue::Nil) {
4004                return i - 1;
4005            }
4006            i += 1;
4007        }
4008    }
4009
4010    pub fn try_bin_tm(
4011        &mut self,
4012        p1: &LuaValue,
4013        p1_idx: Option<StackIdx>,
4014        p2: &LuaValue,
4015        p2_idx: Option<StackIdx>,
4016        res: StackIdx,
4017        tm: lua_types::tagmethod::TagMethod,
4018    ) -> Result<(), LuaError> {
4019        let event = crate::tagmethods::TagMethod::from_u8(tm as u8);
4020        crate::tagmethods::try_bin_tm(self, p1, p1_idx, p2, p2_idx, res, event)
4021    }
4022    pub fn try_bin_i_tm(
4023        &mut self,
4024        p1: &LuaValue,
4025        p1_idx: Option<StackIdx>,
4026        imm: i64,
4027        flip: bool,
4028        res: StackIdx,
4029        tm: lua_types::tagmethod::TagMethod,
4030    ) -> Result<(), LuaError> {
4031        let event = crate::tagmethods::TagMethod::from_u8(tm as u8);
4032        crate::tagmethods::try_bini_tm(self, p1, p1_idx, imm, flip, res, event)
4033    }
4034    pub fn try_bin_assoc_tm(
4035        &mut self,
4036        p1: &LuaValue,
4037        p1_idx: Option<StackIdx>,
4038        p2: &LuaValue,
4039        p2_idx: Option<StackIdx>,
4040        flip: bool,
4041        res: StackIdx,
4042        tm: lua_types::tagmethod::TagMethod,
4043    ) -> Result<(), LuaError> {
4044        let event = crate::tagmethods::TagMethod::from_u8(tm as u8);
4045        crate::tagmethods::try_bin_assoc_tm(self, p1, p1_idx, p2, p2_idx, flip, res, event)
4046    }
4047    pub fn try_concat_tm(&mut self, _p1: &LuaValue, _p2: &LuaValue) -> Result<(), LuaError> {
4048        crate::tagmethods::try_concat_tm(self)
4049    }
4050    pub fn call_tm(
4051        &mut self,
4052        f: LuaValue,
4053        p1: &LuaValue,
4054        p2: &LuaValue,
4055        p3: &LuaValue,
4056    ) -> Result<(), LuaError> {
4057        crate::tagmethods::call_tm(self, f, p1.clone(), p2.clone(), p3.clone())
4058    }
4059    pub fn call_tm_res(
4060        &mut self,
4061        f: LuaValue,
4062        p1: &LuaValue,
4063        p2: &LuaValue,
4064        res: StackIdx,
4065    ) -> Result<(), LuaError> {
4066        crate::tagmethods::call_tm_res(self, f, p1.clone(), p2.clone(), res)
4067    }
4068    pub fn call_tm_res_bool(
4069        &mut self,
4070        f: LuaValue,
4071        p1: &LuaValue,
4072        p2: &LuaValue,
4073    ) -> Result<bool, LuaError> {
4074        let res = self.top_idx();
4075        self.push(LuaValue::Nil);
4076        crate::tagmethods::call_tm_res(self, f, p1.clone(), p2.clone(), res)?;
4077        let result = self.get_at(res).clone();
4078        self.pop();
4079        Ok(!matches!(result, LuaValue::Nil | LuaValue::Bool(false)))
4080    }
4081    pub fn call_order_tm(
4082        &mut self,
4083        p1: &LuaValue,
4084        p2: &LuaValue,
4085        tm: lua_types::tagmethod::TagMethod,
4086    ) -> Result<bool, LuaError> {
4087        let event = crate::tagmethods::TagMethod::from_u8(tm as u8);
4088        crate::tagmethods::call_order_tm(self, p1, p2, event)
4089    }
4090    pub fn call_order_i_tm(
4091        &mut self,
4092        p1: &LuaValue,
4093        v2: i64,
4094        flip: bool,
4095        isfloat: bool,
4096        tm: lua_types::tagmethod::TagMethod,
4097    ) -> Result<bool, LuaError> {
4098        let event = crate::tagmethods::TagMethod::from_u8(tm as u8);
4099        crate::tagmethods::call_orderi_tm(self, p1, v2 as i32, flip, isfloat, event)
4100    }
4101
4102    #[inline(always)]
4103    pub fn proto_code(
4104        &self,
4105        cl: &GcRef<lua_types::closure::LuaLClosure>,
4106        pc: u32,
4107    ) -> lua_types::opcode::Instruction {
4108        cl.proto.code[pc as usize]
4109    }
4110    #[inline(always)]
4111    pub fn proto_const(&self, cl: &GcRef<lua_types::closure::LuaLClosure>, idx: usize) -> LuaValue {
4112        cl.proto.k[idx].clone()
4113    }
4114    /// Hot-path accessor: returns `Some(i)` only when the constant pool entry
4115    /// at `idx` is an `Int`. Avoids the full `LuaValue` clone that
4116    /// `proto_const` performs.
4117    ///
4118    /// arithmetic opcode macros (`op_arithK`).
4119    #[inline(always)]
4120    pub fn proto_const_int(
4121        &self,
4122        cl: &GcRef<lua_types::closure::LuaLClosure>,
4123        idx: usize,
4124    ) -> Option<i64> {
4125        match &cl.proto.k[idx] {
4126            LuaValue::Int(v) => Some(*v),
4127            _ => None,
4128        }
4129    }
4130    /// Hot-path accessor: returns `Some(f)` for `Float(f)` or `Int(i)` (coerced)
4131    /// constants. Avoids the full `LuaValue` clone. Used by the float fast
4132    /// path of `OP_ADDK`/`OP_SUBK`/`OP_MULK`/`OP_DIVK`/`OP_POWK`.
4133    #[inline(always)]
4134    pub fn proto_const_num(
4135        &self,
4136        cl: &GcRef<lua_types::closure::LuaLClosure>,
4137        idx: usize,
4138    ) -> Option<f64> {
4139        match &cl.proto.k[idx] {
4140            LuaValue::Float(f) => Some(*f),
4141            LuaValue::Int(v) => Some(*v as f64),
4142            _ => None,
4143        }
4144    }
4145    pub fn get_proto_instr(&self, ci: CallInfoIdx, pc: u32) -> lua_types::opcode::Instruction {
4146        let cl = self
4147            .ci_lua_closure(ci)
4148            .expect("get_proto_instr: CallInfo does not hold a Lua closure");
4149        cl.proto.code[pc as usize]
4150    }
4151    /// flag as `bool` (C returns `int` 0/1).
4152    ///
4153    /// The C function reads `L->ci` directly, so the `_idx` argument is unused;
4154    /// the VM passes its locally tracked `ci` for symmetry with `trace_exec`.
4155    pub fn trace_call(&mut self, _idx: CallInfoIdx) -> Result<bool, LuaError> {
4156        Ok(crate::debug::trace_call(self)? != 0)
4157    }
4158    /// returning `bool` for the trap flag. `_idx` is unused for the same reason
4159    /// as `trace_call`; `pc` is the 0-based index of the next instruction.
4160    pub fn trace_exec(&mut self, _idx: CallInfoIdx, pc: u32) -> Result<bool, LuaError> {
4161        Ok(crate::debug::trace_exec(self, pc)? != 0)
4162    }
4163    /// Fires the call hook for the frame at `idx`.
4164    ///
4165    /// On Lua 5.1 a tail call reuses the caller's frame but still fires an
4166    /// ordinary `"call"` hook for the callee (the synthetic call event is on the
4167    /// return side, as a `"tail return"`). 5.2+ instead report the call as a
4168    /// `"tail call"` event, which `do_::hookcall` derives from the `CIST_TAIL`
4169    /// callstatus bit. To get the 5.1 naming without disturbing `CIST_TAIL`
4170    /// (which `debug.getinfo` still reads to report `what == "tail"`), the bit is
4171    /// masked off across the `hookcall` call and restored afterwards, so on 5.1
4172    /// the event is always `LUA_HOOKCALL`. 5.2+ takes the unchanged delegation.
4173    pub fn hook_call(&mut self, idx: CallInfoIdx) -> Result<(), LuaError> {
4174        if self.global().lua_version == lua_types::LuaVersion::V51 {
4175            let had_tail = self.call_info[idx.as_usize()].callstatus & CIST_TAIL;
4176            self.call_info[idx.as_usize()].callstatus &= !CIST_TAIL;
4177            let r = crate::do_::hookcall(self, idx);
4178            self.call_info[idx.as_usize()].callstatus |= had_tail;
4179            return r;
4180        }
4181        crate::do_::hookcall(self, idx)
4182    }
4183    #[inline(always)]
4184    fn gc_step_flags(&self) -> Option<(bool, bool)> {
4185        let g = self.global();
4186        if !g.is_gc_running() {
4187            return None;
4188        }
4189        let should_collect = g.heap.would_collect();
4190        let has_finalizers = g.finalizers.has_to_be_finalized();
4191        if should_collect || has_finalizers {
4192            Some((should_collect, has_finalizers))
4193        } else {
4194            None
4195        }
4196    }
4197
4198    #[inline(always)]
4199    fn should_check_gc(&mut self) -> bool {
4200        if self.gc_check_needed {
4201            return true;
4202        }
4203        if self.global().finalizers.has_to_be_finalized() {
4204            self.gc_check_needed = true;
4205            return true;
4206        }
4207        false
4208    }
4209
4210    #[inline(always)]
4211    pub(crate) fn mark_gc_check_needed(&mut self) {
4212        self.gc_check_needed = true;
4213    }
4214
4215    /// The stack prefix the GC must treat as live: `[0 .. gc_trace_bound())`.
4216    ///
4217    /// C's `traversethread` walks exactly `[stack .. top)` (`lgc.c`). The
4218    /// companion invariant is that `top` covers every live slot at every
4219    /// point a collect can run: C functions keep `top` exact at all times,
4220    /// and lvm.c's Protect sites raise it (`savestate`: `top = ci->top`)
4221    /// before any mid-opcode operation that can collect. Checkpoints whose
4222    /// C original runs under Protect must therefore do the same top fixup
4223    /// at the call site (e.g. `get_varargs`). Widening the bound here to
4224    /// `ci.top` instead was tried and rejected: every suspended frame's
4225    /// reserved slice stays traced forever, and the over-retention drives
4226    /// the generational pacer into permanent re-collection (db.lua's
4227    /// line-hook section went from seconds to timeout).
4228    pub fn gc_trace_bound(&self) -> usize {
4229        (self.top.0 as usize).min(self.stack.len())
4230    }
4231
4232    /// Nil this thread's dead stack slice `[gc_trace_bound() ..
4233    /// stack.len())`. C clears the equivalent slice in `traversethread`'s
4234    /// atomic phase (`lgc.c`: `setnilvalue` from `top` to `stack_last +
4235    /// EXTRA_STACK`). Without the clear, a slot that was above the bound at
4236    /// one collect (its object swept) and drifts back under a raised bound
4237    /// later feeds a dangling GcRef to the marker — #140 bug B.
4238    pub fn clear_dead_stack_tail(&mut self) {
4239        let bound = self.gc_trace_bound();
4240        for slot in &mut self.stack[bound..] {
4241            slot.val = LuaValue::Nil;
4242        }
4243    }
4244
4245    /// Clear the dead stack slice of this thread and of every registered
4246    /// coroutine whose state is borrowable. Threads held by a resume chain
4247    /// or a debug-API guard are rooted via `suspended_parent_stacks`
4248    /// snapshots and skipped here; their tails are cleared at their next
4249    /// reachable collect. Call before any collection runs (C parity:
4250    /// `traversethread`'s atomic clear).
4251    pub fn gc_clear_dead_stack_tails(&mut self) {
4252        self.clear_dead_stack_tail();
4253        let global = self.global.clone();
4254        let g = global.borrow();
4255        for entry in g.threads.values() {
4256            if let Ok(mut t) = entry.state.try_borrow_mut() {
4257                t.clear_dead_stack_tail();
4258            }
4259        }
4260    }
4261
4262    /// `gc_clear_dead_stack_tails` gated on `would_collect` — the one-line
4263    /// companion for call sites that invoke `gc().check_step()` directly.
4264    /// Zero cost when no collection is due.
4265    pub fn gc_pre_collect_clear(&mut self) {
4266        if self.global().heap.would_collect() {
4267            self.gc_clear_dead_stack_tails();
4268        }
4269    }
4270
4271    #[inline(always)]
4272    pub fn gc_check_step(&mut self) {
4273        if !self.allowhook {
4274            return;
4275        }
4276        if !self.should_check_gc() {
4277            return;
4278        }
4279        let Some((should_collect, has_finalizers)) = self.gc_step_flags() else {
4280            self.gc_check_needed = false;
4281            return;
4282        };
4283        if should_collect || has_finalizers {
4284            if should_collect {
4285                self.gc_clear_dead_stack_tails();
4286                self.gc().check_step();
4287            }
4288            crate::api::run_pending_finalizers(self);
4289            self.gc_check_needed = true;
4290        }
4291        let should_keep_checking = {
4292            let g = self.global();
4293            g.heap.would_collect() || g.finalizers.has_to_be_finalized()
4294        };
4295        self.gc_check_needed = should_keep_checking;
4296    }
4297    #[inline(always)]
4298    pub fn gc_cond_step(&mut self) {
4299        if !self.allowhook {
4300            return;
4301        }
4302        if !self.should_check_gc() {
4303            return;
4304        }
4305        let Some((should_collect, has_finalizers)) = self.gc_step_flags() else {
4306            self.gc_check_needed = false;
4307            return;
4308        };
4309        if should_collect || has_finalizers {
4310            if should_collect {
4311                self.gc_clear_dead_stack_tails();
4312                self.gc().check_step();
4313            }
4314            crate::api::run_pending_finalizers(self);
4315            self.gc_check_needed = true;
4316        }
4317        let should_keep_checking = {
4318            let g = self.global();
4319            g.heap.would_collect() || g.finalizers.has_to_be_finalized()
4320        };
4321        self.gc_check_needed = should_keep_checking;
4322    }
4323    pub fn gc_barrier_back(&mut self, t: &dyn std::any::Any, v: &LuaValue) {
4324        self.gc().barrier_back(t, v);
4325    }
4326    #[inline(always)]
4327    pub fn gc_value_barrier_back(&mut self, t: &LuaValue, v: &LuaValue) {
4328        if !v.is_collectable() {
4329            return;
4330        }
4331        if let LuaValue::Table(tbl) = t {
4332            self.gc_table_barrier_back(tbl, v);
4333        } else {
4334            self.gc_barrier_back(t, v);
4335        }
4336    }
4337    #[inline(always)]
4338    pub fn gc_table_barrier_back(&mut self, t: &GcRef<LuaTable>, v: &LuaValue) {
4339        if !v.is_collectable() {
4340            return;
4341        }
4342        self.gc().table_barrier_back(t, v);
4343    }
4344    pub fn gc_barrier_upval(&mut self, uv: &GcRef<UpVal>, v: &LuaValue) {
4345        self.gc().barrier(uv, v);
4346    }
4347    ///
4348    /// Phase E-1: compares `GlobalState::current_thread_id` against
4349    /// `main_thread_id`. Coroutine resume (slice 02b) is what will swap
4350    /// `current_thread_id` in and out; until then the running thread is
4351    /// always the main thread and this returns `true`.
4352    pub fn is_main_thread(&mut self) -> bool {
4353        let g = self.global();
4354        g.current_thread_id == g.main_thread_id
4355    }
4356    pub fn obj_type_name<'v>(&self, v: &'v LuaValue) -> std::borrow::Cow<'static, [u8]> {
4357        let honors_name = self.global().lua_version.honors_name_metafield();
4358        match v {
4359            LuaValue::LightUserData(_) => std::borrow::Cow::Borrowed(b"light userdata"),
4360            LuaValue::Table(t) => {
4361                if honors_name {
4362                    if let Some(mt) = t.metatable() {
4363                        if let LuaValue::Str(s) = mt.get_str_bytes(b"__name") {
4364                            return std::borrow::Cow::Owned(s.as_bytes().to_vec());
4365                        }
4366                    }
4367                }
4368                std::borrow::Cow::Borrowed(crate::tagmethods::type_name(v.base_type()))
4369            }
4370            LuaValue::UserData(u) => {
4371                if honors_name {
4372                    if let Some(mt) = u.metatable() {
4373                        if let LuaValue::Str(s) = mt.get_str_bytes(b"__name") {
4374                            return std::borrow::Cow::Owned(s.as_bytes().to_vec());
4375                        }
4376                    }
4377                }
4378                std::borrow::Cow::Borrowed(crate::tagmethods::type_name(v.base_type()))
4379            }
4380            _ => std::borrow::Cow::Borrowed(crate::tagmethods::type_name(v.base_type())),
4381        }
4382    }
4383
4384    pub fn full_type_name(&mut self, v: &LuaValue) -> Result<Vec<u8>, LuaError> {
4385        crate::tagmethods::obj_type_name(self, v)
4386    }
4387    pub fn emit_warning(&mut self, _msg: &[u8], _to_cont: bool) {
4388        warning(self, _msg, _to_cont)
4389    }
4390}
4391
4392// ─── GcHandle — no-op GC facade ───────────────────────────────────────────────
4393
4394/// A short-lived handle returned by `state.gc()` for GC operations.
4395///
4396/// In Phases A–C all methods are no-ops. Phase D replaces with real GC.
4397pub struct GcHandle<'a> {
4398    _state: &'a mut LuaState,
4399}
4400
4401/// Composite root passed to `Heap::full_collect`. The Phase-A workaround in
4402/// `new_state` leaves `GlobalState.mainthread = None` (to break the
4403/// self-referential Rc cycle pre-D), so the running thread's stack and
4404/// openupval list are not reachable from `GlobalState::trace`. Wrapping both
4405/// references in a single `Trace`-implementing root injects the active
4406/// thread as a second mark source for the duration of the collection.
4407struct CollectRoots<'a> {
4408    global: &'a GlobalState,
4409    thread: &'a LuaState,
4410}
4411
4412#[derive(Clone, Copy)]
4413enum HeapCollectMode {
4414    Full,
4415    Step,
4416    Minor,
4417}
4418
4419impl<'a> lua_gc::Trace for CollectRoots<'a> {
4420    fn trace(&self, m: &mut lua_gc::Marker) {
4421        self.global.trace(m);
4422        self.thread.trace(m);
4423    }
4424}
4425
4426#[derive(Clone, Copy)]
4427enum BarrierKind {
4428    Forward,
4429    Backward,
4430}
4431
4432fn barrier_lua_value<P>(
4433    heap: &lua_gc::Heap,
4434    parent: GcRef<P>,
4435    child: &LuaValue,
4436    generational: bool,
4437    kind: BarrierKind,
4438) where
4439    P: lua_gc::Trace + 'static,
4440{
4441    if !child.is_collectable() {
4442        return;
4443    }
4444    if generational && matches!(kind, BarrierKind::Backward) {
4445        heap.generational_backward_barrier(parent.0);
4446    }
4447    match child {
4448        LuaValue::Str(c) => barrier_gc_child(heap, parent, *c, generational, kind),
4449        LuaValue::Table(c) => barrier_gc_child(heap, parent, *c, generational, kind),
4450        LuaValue::Function(LuaClosure::Lua(c)) => {
4451            barrier_gc_child(heap, parent, *c, generational, kind)
4452        }
4453        LuaValue::Function(LuaClosure::C(c)) => {
4454            barrier_gc_child(heap, parent, *c, generational, kind)
4455        }
4456        LuaValue::UserData(c) => barrier_gc_child(heap, parent, *c, generational, kind),
4457        LuaValue::Thread(c) => barrier_gc_child(heap, parent, *c, generational, kind),
4458        LuaValue::Nil
4459        | LuaValue::Bool(_)
4460        | LuaValue::Int(_)
4461        | LuaValue::Float(_)
4462        | LuaValue::LightUserData(_)
4463        | LuaValue::Function(LuaClosure::LightC(_)) => {}
4464    }
4465}
4466
4467fn barrier_gc_child<P, C>(
4468    heap: &lua_gc::Heap,
4469    parent: GcRef<P>,
4470    child: GcRef<C>,
4471    generational: bool,
4472    kind: BarrierKind,
4473) where
4474    P: lua_gc::Trace + 'static,
4475    C: lua_gc::Trace + 'static,
4476{
4477    if generational && matches!(kind, BarrierKind::Forward) {
4478        heap.generational_forward_barrier(parent.0, child.0);
4479    } else if matches!(kind, BarrierKind::Backward) {
4480        heap.barrier_back(parent.0, child.0);
4481    } else {
4482        heap.barrier(parent.0, child.0);
4483    }
4484}
4485
4486fn barrier_child_any<P>(
4487    heap: &lua_gc::Heap,
4488    parent: GcRef<P>,
4489    child: &dyn std::any::Any,
4490    generational: bool,
4491    kind: BarrierKind,
4492) where
4493    P: lua_gc::Trace + 'static,
4494{
4495    if let Some(v) = child.downcast_ref::<LuaValue>() {
4496        barrier_lua_value(heap, parent, v, generational, kind);
4497    } else if let Some(c) = child.downcast_ref::<GcRef<LuaString>>() {
4498        barrier_gc_child(heap, parent, c.clone(), generational, kind);
4499    } else if let Some(c) = child.downcast_ref::<GcRef<LuaTable>>() {
4500        barrier_gc_child(heap, parent, c.clone(), generational, kind);
4501    } else if let Some(c) = child.downcast_ref::<GcRef<LuaClosureLua>>() {
4502        barrier_gc_child(heap, parent, c.clone(), generational, kind);
4503    } else if let Some(c) = child.downcast_ref::<GcRef<LuaClosureC>>() {
4504        barrier_gc_child(heap, parent, c.clone(), generational, kind);
4505    } else if let Some(c) = child.downcast_ref::<GcRef<LuaUserData>>() {
4506        barrier_gc_child(heap, parent, c.clone(), generational, kind);
4507    } else if let Some(c) = child.downcast_ref::<GcRef<lua_types::value::LuaThread>>() {
4508        barrier_gc_child(heap, parent, c.clone(), generational, kind);
4509    } else if let Some(c) = child.downcast_ref::<GcRef<LuaProto>>() {
4510        barrier_gc_child(heap, parent, c.clone(), generational, kind);
4511    } else if let Some(c) = child.downcast_ref::<GcRef<UpVal>>() {
4512        barrier_gc_child(heap, parent, c.clone(), generational, kind);
4513    }
4514}
4515
4516fn barrier_any(
4517    heap: &lua_gc::Heap,
4518    parent: &dyn std::any::Any,
4519    child: &dyn std::any::Any,
4520    generational: bool,
4521    kind: BarrierKind,
4522) {
4523    if let Some(v) = parent.downcast_ref::<LuaValue>() {
4524        match v {
4525            LuaValue::Str(p) => barrier_child_any(heap, *p, child, generational, kind),
4526            LuaValue::Table(p) => barrier_child_any(heap, *p, child, generational, kind),
4527            LuaValue::Function(LuaClosure::Lua(p)) => {
4528                barrier_child_any(heap, *p, child, generational, kind)
4529            }
4530            LuaValue::Function(LuaClosure::C(p)) => {
4531                barrier_child_any(heap, *p, child, generational, kind)
4532            }
4533            LuaValue::UserData(p) => barrier_child_any(heap, *p, child, generational, kind),
4534            LuaValue::Thread(p) => barrier_child_any(heap, *p, child, generational, kind),
4535            LuaValue::Nil
4536            | LuaValue::Bool(_)
4537            | LuaValue::Int(_)
4538            | LuaValue::Float(_)
4539            | LuaValue::LightUserData(_)
4540            | LuaValue::Function(LuaClosure::LightC(_)) => {}
4541        }
4542    } else if let Some(p) = parent.downcast_ref::<GcRef<LuaString>>() {
4543        barrier_child_any(heap, p.clone(), child, generational, kind);
4544    } else if let Some(p) = parent.downcast_ref::<GcRef<LuaTable>>() {
4545        barrier_child_any(heap, p.clone(), child, generational, kind);
4546    } else if let Some(p) = parent.downcast_ref::<GcRef<LuaClosureLua>>() {
4547        barrier_child_any(heap, p.clone(), child, generational, kind);
4548    } else if let Some(p) = parent.downcast_ref::<GcRef<LuaClosureC>>() {
4549        barrier_child_any(heap, p.clone(), child, generational, kind);
4550    } else if let Some(p) = parent.downcast_ref::<GcRef<LuaUserData>>() {
4551        barrier_child_any(heap, p.clone(), child, generational, kind);
4552    } else if let Some(p) = parent.downcast_ref::<GcRef<lua_types::value::LuaThread>>() {
4553        barrier_child_any(heap, p.clone(), child, generational, kind);
4554    } else if let Some(p) = parent.downcast_ref::<GcRef<LuaProto>>() {
4555        barrier_child_any(heap, p.clone(), child, generational, kind);
4556    } else if let Some(p) = parent.downcast_ref::<GcRef<UpVal>>() {
4557        barrier_child_any(heap, p.clone(), child, generational, kind);
4558    }
4559}
4560
4561/// Fixed-point trace of every registered coroutine whose thread handle was
4562/// reached from a real root.
4563///
4564/// A thread whose `RefCell` is mutably borrowed at collect time CANNOT be
4565/// traced — its stack is silently invisible to the marker. Exactly two
4566/// borrow-holders are legitimate: the currently-running thread (rooted
4567/// directly via `CollectRoots.thread`) and resume-chain parents (rooted via
4568/// the `suspended_parent_stacks` snapshots, one per nesting level). Any
4569/// other borrow held across a collect point un-roots that coroutine for the
4570/// whole cycle — the bug-A root-loss class from issue #140 — so debug builds
4571/// assert the failure count stays within snapshot coverage.
4572fn trace_reachable_threads(
4573    global: &GlobalState,
4574    _current_thread_id: u64,
4575    marker: &mut lua_gc::Marker,
4576) {
4577    use lua_gc::Trace;
4578
4579    #[cfg(debug_assertions)]
4580    let mut uncovered_borrowed: Vec<u64> = Vec::new();
4581
4582    loop {
4583        let visited_before = marker.visited_count();
4584        for (id, entry) in global.threads.iter() {
4585            if thread_entry_marked_alive(marker, *id, entry) {
4586                match entry.state.try_borrow() {
4587                    Ok(thread) => thread.trace(marker),
4588                    Err(_) => {
4589                        #[cfg(debug_assertions)]
4590                        if *id != _current_thread_id && !uncovered_borrowed.contains(id) {
4591                            uncovered_borrowed.push(*id);
4592                        }
4593                    }
4594                }
4595            }
4596        }
4597        marker.drain_gray_queue();
4598        if marker.visited_count() == visited_before {
4599            break;
4600        }
4601    }
4602
4603    remark_legacy_open_upvalues(global, marker);
4604
4605    #[cfg(debug_assertions)]
4606    debug_assert!(
4607        uncovered_borrowed.len() <= global.suspended_parent_stacks.len(),
4608        "GC root loss: {} marked-alive coroutine(s) (ids {:?}) were mutably \
4609         borrowed at collect time with only {} parent snapshot(s) covering \
4610         them — their stacks were NOT traced this cycle, so anything only \
4611         reachable from them will be swept (issue #140 bug-A class). A borrow \
4612         of a coroutine's state must not be held across an allocation \
4613         checkpoint without pushing a parent GC snapshot.",
4614        uncovered_borrowed.len(),
4615        uncovered_borrowed,
4616        global.suspended_parent_stacks.len()
4617    );
4618}
4619
4620/// Legacy (5.1/5.2/5.3) `remarkupvals` (lgc.c). After the main mark phase has
4621/// settled which threads are reachable, an unmarked thread's *touched* open
4622/// upvalues have their pointed-to stack values re-marked exactly once, then the
4623/// thread's touched flag is cleared — mirroring C marking each touched upvalue's
4624/// value and removing the thread from `g->twups`.
4625///
4626/// This re-mark resurrects an object reachable only through a suspended thread's
4627/// cycle for one extra collection. Concretely, a `co <-> closure <-> table`
4628/// cycle with a `__gc` on the table is *not* finalized after the first
4629/// `collectgarbage()` (the touched open upvalue keeps the table marked, and the
4630/// table transitively re-marks the thread); on the next collection the flag is
4631/// already cleared, nothing re-marks the table, and it is separated to be
4632/// finalized. That two-collection delay is exactly what 5.3's `gc.lua` asserts
4633/// (`assert(not collected)` after the first collect).
4634///
4635/// From 5.4 the C condition became `!iswhite(uv)` rather than `touched`, so a
4636/// white open upvalue on an unmarked thread is never re-marked and the same
4637/// cycle is finalized after a single collect. Modern versions therefore skip
4638/// this pass entirely, leaving the baked 5.4/5.5 behavior untouched.
4639fn remark_legacy_open_upvalues(global: &GlobalState, marker: &mut lua_gc::Marker) {
4640    use lua_gc::Trace;
4641
4642    let legacy = matches!(
4643        global.lua_version,
4644        lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53
4645    );
4646    if !legacy {
4647        return;
4648    }
4649
4650    let mut remarked_any = false;
4651    for (id, entry) in global.threads.iter() {
4652        if entry.value.id != *id {
4653            continue;
4654        }
4655        if thread_entry_marked_alive(marker, *id, entry) {
4656            continue;
4657        }
4658        let Ok(thread) = entry.state.try_borrow() else {
4659            continue;
4660        };
4661        if thread.openupval.is_empty() || !thread.legacy_open_upval_touched.get() {
4662            continue;
4663        }
4664        thread.legacy_open_upval_touched.set(false);
4665        for uv in thread.openupval.iter() {
4666            let Some((_tid, idx)) = uv.try_open_payload() else {
4667                continue;
4668            };
4669            let slot = idx.0 as usize;
4670            if slot < thread.stack.len() {
4671                thread.stack[slot].val.trace(marker);
4672                remarked_any = true;
4673            }
4674        }
4675    }
4676
4677    if !remarked_any {
4678        return;
4679    }
4680    marker.drain_gray_queue();
4681
4682    loop {
4683        let visited_before = marker.visited_count();
4684        for (id, entry) in global.threads.iter() {
4685            if thread_entry_marked_alive(marker, *id, entry) {
4686                if let Ok(thread) = entry.state.try_borrow() {
4687                    thread.trace(marker);
4688                }
4689            }
4690        }
4691        marker.drain_gray_queue();
4692        if marker.visited_count() == visited_before {
4693            break;
4694        }
4695    }
4696}
4697
4698fn thread_entry_marked_alive(
4699    marker: &lua_gc::Marker,
4700    id: u64,
4701    entry: &ThreadRegistryEntry,
4702) -> bool {
4703    marker.is_marked_or_old(entry.value.0) && entry.value.id == id
4704}
4705
4706fn lua_value_marked_or_old(marker: &lua_gc::Marker, value: &LuaValue) -> bool {
4707    match value {
4708        LuaValue::Str(v) => marker.is_marked_or_old(v.0),
4709        LuaValue::Table(v) => marker.is_marked_or_old(v.0),
4710        LuaValue::Function(LuaClosure::Lua(v)) => marker.is_marked_or_old(v.0),
4711        LuaValue::Function(LuaClosure::C(v)) => marker.is_marked_or_old(v.0),
4712        LuaValue::UserData(v) => marker.is_marked_or_old(v.0),
4713        LuaValue::Thread(v) => marker.is_marked_or_old(v.0),
4714        LuaValue::Nil
4715        | LuaValue::Bool(_)
4716        | LuaValue::Int(_)
4717        | LuaValue::Float(_)
4718        | LuaValue::LightUserData(_)
4719        | LuaValue::Function(LuaClosure::LightC(_)) => true,
4720    }
4721}
4722
4723fn finalizer_marked_or_old(marker: &lua_gc::Marker, object: &FinalizerObject) -> bool {
4724    match object {
4725        FinalizerObject::Table(t) => marker.is_marked_or_old(t.0),
4726        FinalizerObject::UserData(u) => marker.is_marked_or_old(u.0),
4727    }
4728}
4729
4730fn weak_snapshot_tables<'a>(
4731    snapshot: &'a lua_gc::WeakRegistrySnapshot<GcRef<LuaTable>>,
4732) -> impl Iterator<Item = &'a GcRef<LuaTable>> {
4733    snapshot
4734        .weak_values
4735        .iter()
4736        .chain(snapshot.ephemeron.iter())
4737        .chain(snapshot.all_weak.iter())
4738}
4739
4740fn close_open_upvalues_for_unreachable_threads(global: &GlobalState, marker: &mut lua_gc::Marker) {
4741    use lua_gc::Trace;
4742
4743    let mut closed_values = Vec::<LuaValue>::new();
4744    for (id, entry) in global.threads.iter() {
4745        if entry.value.id != *id {
4746            continue;
4747        }
4748        if thread_entry_marked_alive(marker, *id, entry) {
4749            continue;
4750        }
4751        let Ok(thread) = entry.state.try_borrow() else {
4752            continue;
4753        };
4754        for uv in thread.openupval.iter() {
4755            if !marker.is_visited(uv.identity()) {
4756                continue;
4757            }
4758            let Some((thread_id, idx)) = uv.try_open_payload() else {
4759                continue;
4760            };
4761            if thread_id as u64 != *id {
4762                continue;
4763            }
4764            let value = thread.get_at(idx);
4765            uv.close_with(value.clone());
4766            closed_values.push(value);
4767        }
4768    }
4769    for value in closed_values {
4770        value.trace(marker);
4771    }
4772    marker.drain_gray_queue();
4773}
4774
4775/// Mark-phase half of intern-table cleanup (C: dead strings leave the
4776/// stringtable via `luaS_remove` at free time). The marker and a `&mut`
4777/// global cannot coexist, so this records each DEAD entry's
4778/// `(hash, identity)` pair during the immutable post-mark hook; the
4779/// steady-state output is empty or tiny, replacing the old all-live-ids
4780/// vector (O(table) push + sort + per-entry binary-search retain).
4781fn record_dead_interned_strings(
4782    global: &GlobalState,
4783    marker: &lua_gc::Marker,
4784    dead_pairs: &std::cell::RefCell<Vec<(u32, usize)>>,
4785) {
4786    let mut dead = dead_pairs.borrow_mut();
4787    for s in global.interned_lt.iter() {
4788        let id = s.identity();
4789        if !marker.is_visited(id) {
4790            dead.push((s.hash(), id));
4791        }
4792    }
4793}
4794
4795/// Sweep-phase half: O(dead) bucket removals by cached hash + identity,
4796/// then a single batch-end shrink check.
4797///
4798/// The shrink check runs ONCE per collection here — after every dead entry
4799/// for this cycle has been removed, so only live `GcRef<LuaString>`s remain
4800/// to rehash — and deliberately NOT inside `remove()`. A per-removal shrink
4801/// would rehash O(live) entries on each of the O(dead) removals, turning the
4802/// batch into O(live²); deferring to batch end keeps removal O(dead) and adds
4803/// one bounded load-factor check per GC cycle. This mirrors C, where
4804/// `luaS_remove` only unlinks and the shrink lives in `checkSizes`, called
4805/// once per `singlestep`/`fullgc` cycle.
4806fn remove_dead_interned_strings(global: &mut GlobalState, dead_pairs: Vec<(u32, usize)>) {
4807    for (hash, id) in dead_pairs {
4808        global.interned_lt.remove(hash, id);
4809    }
4810    global.interned_lt.shrink_if_sparse();
4811}
4812
4813impl<'a> GcHandle<'a> {
4814    /// macros.tsv: `luaC_checkGC → state.gc().check_step()`
4815    ///
4816    /// Phase D-2: drives implicit collection when the heap's byte threshold
4817    /// is exceeded. Without this hook, loops that allocate without an
4818    /// explicit `collectgarbage()` call (e.g. `closure.lua`'s
4819    /// `while x[1] do local a = A..A end` GC-driven loop) never settle.
4820    pub fn check_step(&self) {
4821        if !self._state.global().is_gc_running() {
4822            return;
4823        }
4824        if self._state.global().is_gen_mode() {
4825            let should_collect = {
4826                let g = self._state.global();
4827                g.heap.would_collect() || g.gc_debt() > 0
4828            };
4829            if should_collect {
4830                self.generational_step();
4831            }
4832        } else {
4833            self.collect_via_heap(/* force = */ false);
4834        }
4835    }
4836
4837    /// macros.tsv: `luaC_fullgc → state.gc().full_collect()`
4838    pub fn full_collect(&self) {
4839        if self._state.global().is_gen_mode() {
4840            self.fullgen();
4841        } else {
4842            self.collect_via_heap(/* force = */ true);
4843        }
4844    }
4845
4846    fn negative_debt(bytes: usize) -> isize {
4847        -(bytes.min(isize::MAX as usize) as isize)
4848    }
4849
4850    fn set_minor_debt(&self) {
4851        let mut g = self._state.global_mut();
4852        let total = g.total_bytes();
4853        let growth = (total / 100).saturating_mul(g.genminormul as usize);
4854        g.heap
4855            .set_threshold_bytes(total.saturating_add(growth.max(1)));
4856        set_debt(&mut *g, Self::negative_debt(growth));
4857    }
4858
4859    fn set_pause_debt(&self) {
4860        let mut g = self._state.global_mut();
4861        let total = g.total_bytes();
4862        let pause = g.gc_pause_param().max(0) as usize;
4863        let threshold = g.gc_estimate.max(1).saturating_mul(pause) / 100;
4864        let debt = if threshold > total {
4865            Self::negative_debt(threshold - total)
4866        } else {
4867            0
4868        };
4869        let heap_threshold = if threshold > total {
4870            threshold
4871        } else {
4872            total.saturating_add(1)
4873        };
4874        g.heap.set_threshold_bytes(heap_threshold);
4875        set_debt(&mut *g, debt);
4876    }
4877
4878    fn enter_incremental_mode(&self) {
4879        let mut g = self._state.global_mut();
4880        g.heap.reset_all_ages();
4881        g.finalizers.reset_generation_boundaries();
4882        g.gckind = GcKind::Incremental as u8;
4883        g.lastatomic = 0;
4884    }
4885
4886    fn enter_generational_mode(&self) -> usize {
4887        self.collect_via_heap_mode(HeapCollectMode::Full);
4888        let numobjs = {
4889            let mut g = self._state.global_mut();
4890            g.heap.promote_all_to_old();
4891            g.finalizers.promote_all_pending_to_old();
4892            g.heap.allgc_count()
4893        };
4894        let total = self._state.global().total_bytes();
4895        {
4896            let mut g = self._state.global_mut();
4897            g.gckind = GcKind::Generational as u8;
4898            g.lastatomic = 0;
4899            g.gc_estimate = total;
4900        }
4901        self.set_minor_debt();
4902        numobjs
4903    }
4904
4905    fn fullgen(&self) -> usize {
4906        self.enter_incremental_mode();
4907        self.enter_generational_mode()
4908    }
4909
4910    fn stepgenfull(&self, lastatomic: usize) {
4911        if self._state.global().gckind == GcKind::Generational as u8 {
4912            self.enter_incremental_mode();
4913        }
4914        self.collect_via_heap_mode(HeapCollectMode::Full);
4915        let newatomic = self._state.global().heap.allgc_count().max(1);
4916        if newatomic < lastatomic.saturating_add(lastatomic >> 3) {
4917            {
4918                let mut g = self._state.global_mut();
4919                g.heap.promote_all_to_old();
4920                g.finalizers.promote_all_pending_to_old();
4921            }
4922            let total = self._state.global().total_bytes();
4923            {
4924                let mut g = self._state.global_mut();
4925                g.gckind = GcKind::Generational as u8;
4926                g.lastatomic = 0;
4927                g.gc_estimate = total;
4928            }
4929            self.set_minor_debt();
4930        } else {
4931            {
4932                let mut g = self._state.global_mut();
4933                g.heap.reset_all_ages();
4934                g.finalizers.reset_generation_boundaries();
4935            }
4936            let total = self._state.global().total_bytes();
4937            {
4938                let mut g = self._state.global_mut();
4939                g.gckind = GcKind::Incremental as u8;
4940                g.lastatomic = newatomic;
4941                g.gc_estimate = total;
4942            }
4943            self.set_pause_debt();
4944        }
4945    }
4946
4947    /// Shared driver behind both `full_collect` (force-collect) and
4948    /// `check_step` (collect only if heap byte threshold exceeded).
4949    ///
4950    /// Snapshots the weak-tables registry, invokes the heap's collect path
4951    /// with a post-mark weak-prune hook, and rebuilds the registry by
4952    /// retaining only entries whose target was reachable. The same hook
4953    /// works for both modes — the heap short-circuits when force=false and
4954    /// the threshold isn't met.
4955    fn collect_via_heap(&self, force: bool) {
4956        self.collect_via_heap_mode(if force {
4957            HeapCollectMode::Full
4958        } else {
4959            HeapCollectMode::Step
4960        });
4961    }
4962
4963    fn collect_via_heap_mode(&self, mode: HeapCollectMode) {
4964        use lua_gc::Trace;
4965        let state_ref: &LuaState = &*self._state;
4966
4967        // Fast path: when the caller did not force a collection, skip all
4968        // the snapshot work (3 Vec allocations + 3 HashSet allocations) if
4969        // the heap is paused or under threshold — a `step()` in that state
4970        // is a no-op, so the snapshot would be pure waste. Called millions
4971        // of times per recursive workload via `gc_check_step` in `precall`.
4972        if matches!(mode, HeapCollectMode::Step) {
4973            let g = state_ref.global.borrow();
4974            if !g.heap.would_collect() {
4975                return;
4976            }
4977        }
4978
4979        // Snapshot weak tables BEFORE the collect. `identity()` reads only
4980        // the pointer address — safe even on still-dangling weak handles —
4981        // and dedup by identity keeps the iteration linear.
4982        let weak_tables_snapshot: lua_gc::WeakRegistrySnapshot<GcRef<LuaTable>> = {
4983            let mut g = state_ref.global.borrow_mut();
4984            g.weak_tables_registry.live_snapshot_by_kind()
4985        };
4986
4987        // Snapshot pending finalizers. `GlobalState::trace` deliberately
4988        // does NOT root these — that's how the post-mark hook below can
4989        // distinguish "still reachable from program state" from "only kept
4990        // alive by the finalizer registry."
4991        let weak_table_capacity = weak_tables_snapshot.len();
4992        let (pending_snapshot, thread_capacity, _interned_capacity): (
4993            Vec<FinalizerObject>,
4994            usize,
4995            usize,
4996        ) = {
4997            let g = state_ref.global.borrow();
4998            let pending = match mode {
4999                HeapCollectMode::Minor => g.finalizers.pending_minor_snapshot(),
5000                HeapCollectMode::Full | HeapCollectMode::Step => g.finalizers.pending_snapshot(),
5001            };
5002            (pending, g.threads.len(), g.interned_lt.len())
5003        };
5004        let finalizer_capacity = pending_snapshot.len();
5005
5006        let alive_ids: std::cell::RefCell<std::collections::HashSet<usize>> =
5007            std::cell::RefCell::new(std::collections::HashSet::new());
5008        let newly_unreachable: std::cell::RefCell<Vec<FinalizerObject>> =
5009            std::cell::RefCell::new(Vec::new());
5010        let alive_thread_ids: std::cell::RefCell<std::collections::HashSet<u64>> =
5011            std::cell::RefCell::new(std::collections::HashSet::new());
5012        let alive_closure_env_ids: std::cell::RefCell<std::collections::HashSet<usize>> =
5013            std::cell::RefCell::new(std::collections::HashSet::new());
5014        let dead_interned: std::cell::RefCell<Vec<(u32, usize)>> = std::cell::RefCell::new(Vec::new());
5015        let collect_ran = std::cell::Cell::new(false);
5016
5017        {
5018            let global = state_ref.global.borrow();
5019            global.heap.unpause();
5020            let roots = CollectRoots {
5021                global: &*global,
5022                thread: state_ref,
5023            };
5024            let hook = |marker: &mut lua_gc::Marker| {
5025                collect_ran.set(true);
5026                alive_ids.borrow_mut().reserve(weak_table_capacity);
5027                newly_unreachable.borrow_mut().reserve(finalizer_capacity);
5028                alive_thread_ids.borrow_mut().reserve(thread_capacity);
5029                trace_reachable_threads(&*global, global.current_thread_id, marker);
5030                close_open_upvalues_for_unreachable_threads(&*global, marker);
5031                // Lua 5.1 weak-key tables are NOT ephemerons. In 5.1
5032                // `traversetable` (lgc.c) a `__mode='k'` table still marks its
5033                // VALUES strongly (`if (!weakvalue) markvalue(g, gval(n))`), so
5034                // a value that references its own weak key keeps that key alive
5035                // (`a[t]=t` survives). Ephemeron semantics — a value reachable
5036                // only if the key is independently reachable — arrived in 5.2.
5037                let legacy_weak_key_values =
5038                    matches!(global.lua_version, lua_types::LuaVersion::V51);
5039                loop {
5040                    let visited_before = marker.visited_count();
5041                    for t in &weak_tables_snapshot.ephemeron {
5042                        if !marker.is_marked_or_old(t.0) {
5043                            continue;
5044                        }
5045                        if legacy_weak_key_values {
5046                            let mut to_mark = Vec::new();
5047                            t.for_each_entry(|_k, v| to_mark.push(v.clone()));
5048                            for v in &to_mark {
5049                                v.trace(marker);
5050                            }
5051                        } else {
5052                            let to_mark = t.ephemeron_values_to_mark_with_value(&|v| {
5053                                lua_value_marked_or_old(marker, v)
5054                            });
5055                            for v in &to_mark {
5056                                v.trace(marker);
5057                            }
5058                        }
5059                    }
5060                    marker.drain_gray_queue();
5061                    if marker.visited_count() == visited_before {
5062                        break;
5063                    }
5064                }
5065                // Clear dead weak VALUES before finalizer resurrection,
5066                // mirroring C's `clearvalues(g->weak, NULL)` /
5067                // `clearvalues(g->allweak, NULL)` which run in `atomic`
5068                // *before* `separatetobefnz`/`markbeingfnz` (lua-5.3.6
5069                // lgc.c). An object that is only reachable through a
5070                // to-be-finalized table is still white at this point, so a
5071                // weak-value reference to it is dropped — and stays dropped
5072                // even after resurrection re-marks the object — so the
5073                // finalizer observes the already-cleared slot. Keys are left
5074                // untouched here; they are cleared post-resurrection.
5075                //
5076                // C iterates the *entire* weak list regardless of table mark
5077                // state, so a weak-value table reachable only via a soon-to-be
5078                // resurrected object (its `__gc` closure stored weakly is the
5079                // canonical case) still has its dead values dropped here, not
5080                // after resurrection re-marks the table. The mark check only
5081                // gates whether surviving value-strings are re-marked: strings
5082                // are values weak tables never collect, but only when the
5083                // owning table itself is reachable.
5084                for t in weak_tables_snapshot
5085                    .weak_values
5086                    .iter()
5087                    .chain(weak_tables_snapshot.all_weak.iter())
5088                {
5089                    let to_mark = t.prune_weak_dead_with_value(
5090                        &|_| true,
5091                        &|v| lua_value_marked_or_old(marker, v),
5092                    );
5093                    if marker.is_marked_or_old(t.0) {
5094                        for v in &to_mark {
5095                            v.trace(marker);
5096                        }
5097                    }
5098                }
5099                marker.drain_gray_queue();
5100                for pf in &pending_snapshot {
5101                    if !finalizer_marked_or_old(marker, pf) {
5102                        pf.mark(marker);
5103                        newly_unreachable.borrow_mut().push(pf.clone());
5104                    }
5105                }
5106                marker.drain_gray_queue();
5107                loop {
5108                    let visited_before = marker.visited_count();
5109                    for t in &weak_tables_snapshot.ephemeron {
5110                        if !marker.is_marked_or_old(t.0) {
5111                            continue;
5112                        }
5113                        if legacy_weak_key_values {
5114                            let mut to_mark = Vec::new();
5115                            t.for_each_entry(|_k, v| to_mark.push(v.clone()));
5116                            for v in &to_mark {
5117                                v.trace(marker);
5118                            }
5119                        } else {
5120                            let to_mark = t.ephemeron_values_to_mark_with_value(&|v| {
5121                                lua_value_marked_or_old(marker, v)
5122                            });
5123                            for v in &to_mark {
5124                                v.trace(marker);
5125                            }
5126                        }
5127                    }
5128                    marker.drain_gray_queue();
5129                    if marker.visited_count() == visited_before {
5130                        break;
5131                    }
5132                }
5133                // Clear dead weak KEYS post-resurrection, mirroring C's
5134                // `clearkeys(g->ephemeron, NULL)` / `clearkeys(g->allweak,
5135                // NULL)`. A key kept alive only by a resurrected (now-marked)
5136                // object survives; a key pending its own finalization is
5137                // already marked by `markbeingfnz`, so `marked_or_old`
5138                // keeps it visible until its `__gc` runs. Values in these
5139                // originally-tracked tables were already settled in the
5140                // pre-resurrection value pass and must not be re-evaluated
5141                // (resurrection can re-mark a value that was correctly
5142                // cleared), so this pass touches keys only — matching the
5143                // `origweak` skip in C's later `clearvalues(g->weak,
5144                // origweak)`.
5145                for t in weak_tables_snapshot
5146                    .ephemeron
5147                    .iter()
5148                    .chain(weak_tables_snapshot.all_weak.iter())
5149                {
5150                    if !marker.is_marked_or_old(t.0) {
5151                        continue;
5152                    }
5153                    let to_mark = t.prune_weak_dead_with_value(
5154                        &|v| lua_value_marked_or_old(marker, v),
5155                        &|_| true,
5156                    );
5157                    for v in &to_mark {
5158                        v.trace(marker);
5159                    }
5160                }
5161                for t in weak_snapshot_tables(&weak_tables_snapshot) {
5162                    if marker.is_marked_or_old(t.0) {
5163                        alive_ids.borrow_mut().insert(t.identity());
5164                    }
5165                }
5166                marker.drain_gray_queue();
5167                {
5168                    let mut alive = alive_thread_ids.borrow_mut();
5169                    for (id, entry) in global.threads.iter() {
5170                        if thread_entry_marked_alive(marker, *id, entry) {
5171                            alive.insert(*id);
5172                        }
5173                    }
5174                }
5175                {
5176                    let mut alive = alive_closure_env_ids.borrow_mut();
5177                    for id in global.closure_envs.keys() {
5178                        if marker.is_visited(*id) {
5179                            alive.insert(*id);
5180                        }
5181                    }
5182                }
5183                record_dead_interned_strings(&*global, marker, &dead_interned);
5184            };
5185            match mode {
5186                HeapCollectMode::Full => global.heap.full_collect_with_post_mark(&roots, hook),
5187                HeapCollectMode::Step => global.heap.step_with_post_mark(&roots, hook),
5188                HeapCollectMode::Minor => global.heap.minor_collect_with_post_mark(&roots, hook),
5189            }
5190        }
5191
5192        if !collect_ran.get() {
5193            return;
5194        }
5195
5196        // After collect, drop weak-table-registry entries whose target was
5197        // swept. This keeps the registry bounded and avoids retaining weak
5198        // handles whose target can no longer upgrade.
5199        let alive_set = alive_ids.into_inner();
5200        let promote: Vec<FinalizerObject> = newly_unreachable.into_inner();
5201        let alive_thread_ids = alive_thread_ids.into_inner();
5202        let alive_closure_env_ids = alive_closure_env_ids.into_inner();
5203        let dead_interned = dead_interned.into_inner();
5204        let mut g = state_ref.global.borrow_mut();
5205        remove_dead_interned_strings(&mut *g, dead_interned);
5206        g.weak_tables_registry.retain_identities(&alive_set);
5207        let main_thread_id = g.main_thread_id;
5208        g.threads.retain(|id, _| alive_thread_ids.contains(id));
5209        g.cross_thread_upvals
5210            .retain(|(id, _), _| *id == main_thread_id || alive_thread_ids.contains(id));
5211        // Lua 5.1 side maps (empty on 5.2–5.5): drop a coroutine's per-thread
5212        // global table once the coroutine is gone, and a closure's stored
5213        // environment once that closure is no longer visited.
5214        g.thread_globals
5215            .retain(|id, _| alive_thread_ids.contains(id));
5216        g.closure_envs
5217            .retain(|id, _| alive_closure_env_ids.contains(id));
5218        // Move newly-unreachable finalizables from `pending_finalizers` to
5219        // `to_be_finalized`. The latter is rooted by `GlobalState::trace`,
5220        // so these tables remain alive until their `__gc` runs.
5221        let promoted = g.finalizers.promote_pending_to_finalized(promote);
5222        for object in &promoted {
5223            if let Some(ptr) = object.heap_ptr() {
5224                g.heap.move_finobj_to_tobefnz(ptr);
5225            }
5226        }
5227        if matches!(mode, HeapCollectMode::Minor) {
5228            g.finalizers.finish_minor_collection();
5229        }
5230    }
5231
5232    /// Run one generational collection step.
5233    pub fn generational_step(&self) -> bool {
5234        self.generational_step_with_major(true)
5235    }
5236
5237    /// Run a generational step forced to the regular minor path.
5238    ///
5239    /// Used for `collectgarbage("step", 0)`: upstream `genstep` treats
5240    /// `GCdebt <= 0` as an explicit zero-size step and performs a minor
5241    /// collection, unless a previous bad major has already armed `lastatomic`.
5242    pub fn generational_step_minor_only(&self) -> bool {
5243        self.generational_step_with_major(false)
5244    }
5245
5246    fn generational_step_with_major(&self, allow_major: bool) -> bool {
5247        let (lastatomic, majorbase, majorinc, should_major) = {
5248            let g = self._state.global();
5249            let majorbase = if g.gc_estimate == 0 {
5250                g.total_bytes()
5251            } else {
5252                g.gc_estimate
5253            };
5254            let majormul = g.gc_genmajormul_param().max(0) as usize;
5255            let majorinc = (majorbase / 100).saturating_mul(majormul);
5256            let debt_due = g.gc_debt() > 0 || g.heap.would_collect();
5257            let should_major =
5258                allow_major && debt_due && g.total_bytes() > majorbase.saturating_add(majorinc);
5259            (g.lastatomic, majorbase, majorinc, should_major)
5260        };
5261
5262        if lastatomic != 0 {
5263            self.stepgenfull(lastatomic);
5264            debug_assert!(self._state.global().is_gen_mode());
5265            return true;
5266        }
5267
5268        if should_major {
5269            let numobjs = self.fullgen();
5270            let after = self._state.global().total_bytes();
5271            if after < majorbase.saturating_add(majorinc / 2) {
5272                self.set_minor_debt();
5273            } else {
5274                {
5275                    let mut g = self._state.global_mut();
5276                    g.lastatomic = numobjs.max(1);
5277                }
5278                self.set_pause_debt();
5279            }
5280        } else {
5281            self.collect_via_heap_mode(HeapCollectMode::Minor);
5282            self.set_minor_debt();
5283            self._state.global_mut().gc_estimate = majorbase;
5284        }
5285
5286        debug_assert!(self._state.global().is_gen_mode());
5287        true
5288    }
5289
5290    /// Phase-B stub for `luaC_step(L)`.
5291    pub fn step(&self) { /* phase-b no-op */
5292    }
5293
5294    /// Run one budgeted incremental step of the GC.
5295    ///
5296    /// `work_units` is the number of GC work units the step is allowed to
5297    /// perform (one gray trace, one sweep visit, or one phase transition).
5298    /// Returns `true` if the step completed a cycle and the collector is
5299    /// now in the `Pause` state; `false` otherwise.
5300    ///
5301    /// Mirrors `collect_via_heap` for the post-mark weak-table /
5302    /// finalizer-promotion logic, but only the atomic-phase transition will
5303    /// invoke the snapshot-walking hook — propagate and sweep steps reuse
5304    /// the snapshot but never execute it. The snapshot is rebuilt on every
5305    /// call; the cost is `O(weak_tables_registry)` per step.
5306    pub fn incremental_step(&self, work_units: isize) -> bool {
5307        self.incremental_step_to_state(work_units, None)
5308    }
5309
5310    /// TestC/debug helper: run the incremental collector until a specific heap
5311    /// state is entered, preserving the same weak-table/finalizer post-mark
5312    /// hooks as [`Self::incremental_step`]. This is intentionally not used for
5313    /// normal pacing; it exists so official tests can inspect mid-cycle colors.
5314    pub fn run_until_gc_state_for_test(&self, target: lua_gc::GcState) -> bool {
5315        self.incremental_step_to_state(isize::MAX / 4, Some(target));
5316        self._state.global().heap.gc_state() == target
5317    }
5318
5319    fn incremental_step_to_state(
5320        &self,
5321        work_units: isize,
5322        target: Option<lua_gc::GcState>,
5323    ) -> bool {
5324        use lua_gc::{StepBudget, StepOutcome, Trace};
5325        let state_ref: &LuaState = &*self._state;
5326
5327        let weak_tables_snapshot: lua_gc::WeakRegistrySnapshot<GcRef<LuaTable>> = {
5328            let mut g = state_ref.global.borrow_mut();
5329            g.weak_tables_registry.live_snapshot_by_kind()
5330        };
5331
5332        let weak_table_capacity = weak_tables_snapshot.len();
5333        let (pending_snapshot, thread_capacity, _interned_capacity): (
5334            Vec<FinalizerObject>,
5335            usize,
5336            usize,
5337        ) = {
5338            let g = state_ref.global.borrow();
5339            (
5340                g.finalizers.pending_snapshot(),
5341                g.threads.len(),
5342                g.interned_lt.len(),
5343            )
5344        };
5345        let finalizer_capacity = pending_snapshot.len();
5346
5347        let alive_ids: std::cell::RefCell<std::collections::HashSet<usize>> =
5348            std::cell::RefCell::new(std::collections::HashSet::new());
5349        let newly_unreachable: std::cell::RefCell<Vec<FinalizerObject>> =
5350            std::cell::RefCell::new(Vec::new());
5351        let alive_thread_ids: std::cell::RefCell<std::collections::HashSet<u64>> =
5352            std::cell::RefCell::new(std::collections::HashSet::new());
5353        let alive_closure_env_ids: std::cell::RefCell<std::collections::HashSet<usize>> =
5354            std::cell::RefCell::new(std::collections::HashSet::new());
5355        let dead_interned: std::cell::RefCell<Vec<(u32, usize)>> = std::cell::RefCell::new(Vec::new());
5356        let atomic_ran = std::cell::Cell::new(false);
5357
5358        let stop_target = {
5359            let g = state_ref.global.borrow();
5360            match (target, g.heap.gc_state()) {
5361                (Some(target), _) => Some(target),
5362                (None, lua_gc::GcState::CallFin) => None,
5363                (None, _) => Some(lua_gc::GcState::CallFin),
5364            }
5365        };
5366
5367        let outcome = {
5368            let global = state_ref.global.borrow();
5369            global.heap.unpause();
5370            let roots = CollectRoots {
5371                global: &*global,
5372                thread: state_ref,
5373            };
5374            let hook = |marker: &mut lua_gc::Marker| {
5375                atomic_ran.set(true);
5376                alive_ids.borrow_mut().reserve(weak_table_capacity);
5377                newly_unreachable.borrow_mut().reserve(finalizer_capacity);
5378                alive_thread_ids.borrow_mut().reserve(thread_capacity);
5379                trace_reachable_threads(&*global, global.current_thread_id, marker);
5380                close_open_upvalues_for_unreachable_threads(&*global, marker);
5381                // Lua 5.1 weak-key tables are NOT ephemerons; see the
5382                // full-collect hook above. `__mode='k'` still marks its values
5383                // strongly so a value referencing its own weak key survives.
5384                let legacy_weak_key_values =
5385                    matches!(global.lua_version, lua_types::LuaVersion::V51);
5386                loop {
5387                    let visited_before = marker.visited_count();
5388                    for t in &weak_tables_snapshot.ephemeron {
5389                        let t_id = t.identity();
5390                        if !marker.is_visited(t_id) {
5391                            continue;
5392                        }
5393                        if legacy_weak_key_values {
5394                            let mut to_mark = Vec::new();
5395                            t.for_each_entry(|_k, v| to_mark.push(v.clone()));
5396                            for v in &to_mark {
5397                                v.trace(marker);
5398                            }
5399                        } else {
5400                            let to_mark = t.ephemeron_values_to_mark(&|id| marker.is_visited(id));
5401                            for v in &to_mark {
5402                                v.trace(marker);
5403                            }
5404                        }
5405                    }
5406                    marker.drain_gray_queue();
5407                    if marker.visited_count() == visited_before {
5408                        break;
5409                    }
5410                }
5411                // Clear dead weak VALUES before finalizer resurrection,
5412                // mirroring C's `clearvalues(g->weak, NULL)` /
5413                // `clearvalues(g->allweak, NULL)` which run *before*
5414                // `markbeingfnz` in `atomic` (lua-5.3.6 lgc.c). See the
5415                // full-collect hook above for the rationale; this is the
5416                // incremental atomic and is the path the in-mode gc.lua
5417                // weak-table-plus-finalizer test exercises. C iterates the
5418                // entire weak list regardless of table mark state, so a
5419                // weak-value table reachable only via a soon-to-be-resurrected
5420                // object (canonically a `__gc` closure stored weakly) still
5421                // has its dead values dropped here. The visited check only
5422                // gates re-marking surviving value-strings. Keys untouched.
5423                for t in weak_tables_snapshot
5424                    .weak_values
5425                    .iter()
5426                    .chain(weak_tables_snapshot.all_weak.iter())
5427                {
5428                    let to_mark =
5429                        t.prune_weak_dead_with(&|_| true, &|id| marker.is_visited(id));
5430                    if marker.is_visited(t.identity()) {
5431                        for v in &to_mark {
5432                            v.trace(marker);
5433                        }
5434                    }
5435                }
5436                marker.drain_gray_queue();
5437                for pf in &pending_snapshot {
5438                    if !marker.is_visited(pf.identity()) {
5439                        pf.mark(marker);
5440                        newly_unreachable.borrow_mut().push(pf.clone());
5441                    }
5442                }
5443                marker.drain_gray_queue();
5444                loop {
5445                    let visited_before = marker.visited_count();
5446                    for t in &weak_tables_snapshot.ephemeron {
5447                        let t_id = t.identity();
5448                        if !marker.is_visited(t_id) {
5449                            continue;
5450                        }
5451                        if legacy_weak_key_values {
5452                            let mut to_mark = Vec::new();
5453                            t.for_each_entry(|_k, v| to_mark.push(v.clone()));
5454                            for v in &to_mark {
5455                                v.trace(marker);
5456                            }
5457                        } else {
5458                            let to_mark = t.ephemeron_values_to_mark(&|id| marker.is_visited(id));
5459                            for v in &to_mark {
5460                                v.trace(marker);
5461                            }
5462                        }
5463                    }
5464                    marker.drain_gray_queue();
5465                    if marker.visited_count() == visited_before {
5466                        break;
5467                    }
5468                }
5469                // Clear dead weak KEYS post-resurrection, mirroring C's
5470                // `clearkeys(g->ephemeron, NULL)` / `clearkeys(g->allweak,
5471                // NULL)`. Values in these originally-tracked tables were
5472                // already settled in the pre-resurrection pass and must not
5473                // be re-evaluated, so this pass is keys-only (the `origweak`
5474                // skip in C's later `clearvalues(g->weak, origweak)`).
5475                for t in weak_tables_snapshot
5476                    .ephemeron
5477                    .iter()
5478                    .chain(weak_tables_snapshot.all_weak.iter())
5479                {
5480                    if !marker.is_visited(t.identity()) {
5481                        continue;
5482                    }
5483                    let to_mark =
5484                        t.prune_weak_dead_with(&|id| marker.is_visited(id), &|_| true);
5485                    for v in &to_mark {
5486                        v.trace(marker);
5487                    }
5488                }
5489                for t in weak_snapshot_tables(&weak_tables_snapshot) {
5490                    if marker.is_visited(t.identity()) {
5491                        alive_ids.borrow_mut().insert(t.identity());
5492                    }
5493                }
5494                marker.drain_gray_queue();
5495                {
5496                    let mut alive = alive_thread_ids.borrow_mut();
5497                    for (id, entry) in global.threads.iter() {
5498                        if thread_entry_marked_alive(marker, *id, entry) {
5499                            alive.insert(*id);
5500                        }
5501                    }
5502                }
5503                {
5504                    let mut alive = alive_closure_env_ids.borrow_mut();
5505                    for id in global.closure_envs.keys() {
5506                        if marker.is_visited(*id) {
5507                            alive.insert(*id);
5508                        }
5509                    }
5510                }
5511                record_dead_interned_strings(&*global, marker, &dead_interned);
5512            };
5513            let budget = StepBudget::from_work(work_units);
5514            if let Some(target) = stop_target {
5515                global
5516                    .heap
5517                    .incremental_run_until_state_with_post_mark(&roots, target, work_units, hook)
5518            } else {
5519                global
5520                    .heap
5521                    .incremental_step_with_post_mark(&roots, budget, hook)
5522            }
5523        };
5524
5525        if atomic_ran.get() {
5526            let alive_set = alive_ids.into_inner();
5527            let promote: Vec<FinalizerObject> = newly_unreachable.into_inner();
5528            let alive_thread_ids = alive_thread_ids.into_inner();
5529            let alive_closure_env_ids = alive_closure_env_ids.into_inner();
5530            let dead_interned = dead_interned.into_inner();
5531            let mut g = state_ref.global.borrow_mut();
5532            remove_dead_interned_strings(&mut *g, dead_interned);
5533            g.weak_tables_registry.retain_identities(&alive_set);
5534            let main_thread_id = g.main_thread_id;
5535            g.threads.retain(|id, _| alive_thread_ids.contains(id));
5536            g.cross_thread_upvals
5537                .retain(|(id, _), _| *id == main_thread_id || alive_thread_ids.contains(id));
5538            // Lua 5.1 side maps (empty on 5.2–5.5): drop a coroutine's
5539            // per-thread global table once the coroutine is gone, and a
5540            // closure's stored environment once that closure is unvisited.
5541            g.thread_globals
5542                .retain(|id, _| alive_thread_ids.contains(id));
5543            g.closure_envs
5544                .retain(|id, _| alive_closure_env_ids.contains(id));
5545            let promoted = g.finalizers.promote_pending_to_finalized(promote);
5546            for object in &promoted {
5547                if let Some(ptr) = object.heap_ptr() {
5548                    g.heap.move_finobj_to_tobefnz(ptr);
5549                }
5550            }
5551        }
5552
5553        let mut paused = matches!(outcome, StepOutcome::Paused);
5554        if target.is_none()
5555            && self._state.global().heap.gc_state() == lua_gc::GcState::CallFin
5556            && !self._state.global().finalizers.has_to_be_finalized()
5557        {
5558            paused = self._state.global().heap.finish_callfin_phase();
5559        }
5560
5561        paused
5562    }
5563
5564    /// Run only the weak-table atomic cleanup used by legacy generational
5565    /// callers that need mark/prune behavior without sweeping.
5566    ///
5567    /// Explicit generational steps now use [`Self::generational_step`], which
5568    /// performs a young sweep. This helper remains for call sites that only
5569    /// need the weak-table atomic pass.
5570    pub fn prune_weak_tables_mark_only(&self) {
5571        use lua_gc::Trace;
5572        let state_ref: &LuaState = &*self._state;
5573
5574        let weak_tables_snapshot: lua_gc::WeakRegistrySnapshot<GcRef<LuaTable>> = {
5575            let mut g = state_ref.global.borrow_mut();
5576            g.weak_tables_registry.live_snapshot_by_kind()
5577        };
5578        let _interned_capacity = {
5579            let g = state_ref.global.borrow();
5580            g.interned_lt.len()
5581        };
5582
5583        let dead_interned: std::cell::RefCell<Vec<(u32, usize)>> = std::cell::RefCell::new(Vec::new());
5584
5585        {
5586            let global = state_ref.global.borrow();
5587            global.heap.unpause();
5588            let roots = CollectRoots {
5589                global: &*global,
5590                thread: state_ref,
5591            };
5592            let hook = |marker: &mut lua_gc::Marker| {
5593                trace_reachable_threads(&*global, global.current_thread_id, marker);
5594                loop {
5595                    let visited_before = marker.visited_count();
5596                    for t in &weak_tables_snapshot.ephemeron {
5597                        let t_id = t.identity();
5598                        if !marker.is_visited(t_id) {
5599                            continue;
5600                        }
5601                        let to_mark = t.ephemeron_values_to_mark(&|id| marker.is_visited(id));
5602                        for v in &to_mark {
5603                            v.trace(marker);
5604                        }
5605                    }
5606                    marker.drain_gray_queue();
5607                    if marker.visited_count() == visited_before {
5608                        break;
5609                    }
5610                }
5611                for t in weak_snapshot_tables(&weak_tables_snapshot) {
5612                    if marker.is_visited(t.identity()) {
5613                        let to_mark = t.prune_weak_dead(&|id| marker.is_visited(id));
5614                        for v in &to_mark {
5615                            v.trace(marker);
5616                        }
5617                    }
5618                }
5619                marker.drain_gray_queue();
5620                record_dead_interned_strings(&*global, marker, &dead_interned);
5621            };
5622            global.heap.mark_only_with_post_mark(&roots, hook);
5623        }
5624
5625        let dead_interned = dead_interned.into_inner();
5626        let mut g = state_ref.global.borrow_mut();
5627        remove_dead_interned_strings(&mut *g, dead_interned);
5628    }
5629
5630    /// Set the GC kind (incremental/generational).
5631    pub fn change_mode(&self, mode: GcKind) {
5632        let old = self._state.global().gckind;
5633        if old == mode as u8 {
5634            self._state.global_mut().lastatomic = 0;
5635            return;
5636        }
5637        match mode {
5638            GcKind::Generational => {
5639                self.enter_generational_mode();
5640            }
5641            GcKind::Incremental => {
5642                self.enter_incremental_mode();
5643            }
5644        }
5645    }
5646
5647    /// Phase-B stub for `luaC_fix(L, o)` — pin an object so GC won't collect it.
5648    pub fn fix_object<T: lua_gc::Trace + 'static>(&self, _o: &GcRef<T>) { /* phase-b no-op */
5649    }
5650
5651    /// Free all collectable objects (called during state teardown).
5652    ///
5653    /// PORT NOTE: In Phases A–C, Rc drop chains handle deallocation automatically.
5654    pub fn free_all_objects(&self) {
5655        // PORT NOTE: Phase A–C no-op; Rc::drop handles deallocation
5656    }
5657
5658    /// GC write barrier for a TValue.
5659    ///
5660    /// macros.tsv: `luaC_barrier → state.gc().barrier(p, v)`
5661    pub fn barrier(&self, p: &dyn std::any::Any, v: &LuaValue) {
5662        let g = self._state.global();
5663        barrier_any(&g.heap, p, v, g.is_gen_mode(), BarrierKind::Forward);
5664    }
5665
5666    /// Backward write barrier.
5667    ///
5668    /// macros.tsv: `luaC_barrierback → state.gc().barrier_back(p, v)`
5669    pub fn barrier_back(&self, p: &dyn std::any::Any, v: &LuaValue) {
5670        let g = self._state.global();
5671        barrier_any(&g.heap, p, v, g.is_gen_mode(), BarrierKind::Backward);
5672    }
5673
5674    /// Typed table backward barrier for table mutation hot paths.
5675    pub fn table_barrier_back(&self, p: &GcRef<LuaTable>, v: &LuaValue) {
5676        let g = self._state.global();
5677        barrier_lua_value(&g.heap, *p, v, g.is_gen_mode(), BarrierKind::Backward);
5678    }
5679
5680    /// Object write barrier.
5681    ///
5682    /// macros.tsv: `luaC_objbarrier → state.gc().obj_barrier(p, o)`
5683    pub fn obj_barrier(&self, p: &dyn std::any::Any, o: &dyn std::any::Any) {
5684        let g = self._state.global();
5685        barrier_any(&g.heap, p, o, g.is_gen_mode(), BarrierKind::Forward);
5686    }
5687
5688    /// Backward object write barrier.
5689    ///
5690    pub fn obj_barrier_back(&self, p: &dyn std::any::Any, o: &dyn std::any::Any) {
5691        let g = self._state.global();
5692        barrier_any(&g.heap, p, o, g.is_gen_mode(), BarrierKind::Backward);
5693    }
5694}
5695
5696// ─── Functions from lstate.c ──────────────────────────────────────────────────
5697
5698//
5699// PORT NOTE: `luai_makeseed` in C mixed ASLR entropy (pointer addresses of a
5700// heap var, stack var, and code symbol) with the current time via `luaS_hash`.
5701// In Rust, raw pointer addresses require `unsafe` which is forbidden outside
5702// lua-gc/lua-coro. Native builds use time-only entropy for now; bare WASM uses
5703// a fixed seed so state creation never touches a stubbed host clock.
5704fn make_seed() -> u32 {
5705    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
5706    {
5707        return crate::string::hash_bytes(b"lua-rs-wasm-seed", 0x9e37_79b9);
5708    }
5709
5710    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
5711    {
5712        use std::time::{SystemTime, UNIX_EPOCH};
5713        let t = SystemTime::now()
5714            .duration_since(UNIX_EPOCH)
5715            .map(|d| d.as_secs() as u32)
5716            .unwrap_or(0);
5717
5718        // TODO(port): mix in ASLR entropy (pointer to heap / stack / code).
5719        // Requires a short `unsafe` block to cast references to usize.
5720        // The entropy improvement is important for hash DoS resistance (CVE-class).
5721        // Phase B should add this via a platform-specific helper in lua-gc or via
5722        // the `getrandom` crate if it is added as a dependency.
5723
5724        // For Phase A, just hash the time bytes against itself.
5725        crate::string::hash_bytes(&t.to_le_bytes(), t)
5726    }
5727}
5728
5729/// Adjust the compatibility `GCdebt` value against the collector-owned live
5730/// byte count.
5731///
5732///
5733/// ```c
5734///
5735/// //   l_mem tb = gettotalbytes(g);
5736/// //   lua_assert(tb > 0);
5737/// //   if (debt < tb - MAX_LMEM)
5738/// //     debt = tb - MAX_LMEM;
5739/// //   g->GCdebt = debt;
5740/// // }
5741/// ```
5742pub(crate) fn set_debt(g: &mut GlobalState, mut debt: isize) {
5743    let tb = g.total_bytes() as isize;
5744    debug_assert!(tb > 0);
5745    // macros.tsv: MAX_LMEM → isize::MAX
5746    if debt < tb.saturating_sub(isize::MAX) {
5747        debt = tb - isize::MAX;
5748    }
5749    g.gc_debt = debt;
5750}
5751
5752/// Deprecated no-op that returns `LUAI_MAXCCALLS`.
5753///
5754///
5755/// ```c
5756///
5757/// //   UNUSED(L); UNUSED(limit);
5758/// //   return LUAI_MAXCCALLS;  /* warning?? */
5759/// // }
5760/// ```
5761pub fn set_c_stack_limit(_state: &mut LuaState, _limit: u32) -> i32 {
5762    let _ = (_state, _limit);
5763    LUAI_MAXCCALLS as i32
5764}
5765
5766/// Allocate a fresh `CallInfo` beyond the current frame and return its index.
5767///
5768///
5769/// ```c
5770///
5771/// //   CallInfo *ci;
5772/// //   lua_assert(L->ci->next == NULL);
5773/// //   ci = luaM_new(L, CallInfo);
5774/// //   L->ci->next = ci;
5775/// //   ci->previous = L->ci;
5776/// //   ci->next = NULL;
5777/// //   ci->u.l.trap = 0;
5778/// //   L->nci++;
5779/// //   return ci;
5780/// // }
5781/// ```
5782pub(crate) fn extend_ci(state: &mut LuaState) -> CallInfoIdx {
5783    debug_assert!(
5784        state.call_info[state.ci.0 as usize].next.is_none(),
5785        "extend_ci: current ci already has a cached next frame"
5786    );
5787
5788    let current_idx = state.ci;
5789    // macros.tsv: luaM_new → Box::new(T::default()) — here we push onto the Vec
5790    let new_idx = CallInfoIdx(state.call_info.len() as u32);
5791
5792    state.call_info.push(CallInfo {
5793        previous: Some(current_idx),
5794        next: None,
5795        u: CallInfoFrame::lua_default(),
5796        ..CallInfo::default()
5797    });
5798
5799    state.call_info[current_idx.0 as usize].next = Some(new_idx);
5800
5801    state.nci += 1;
5802
5803    new_idx
5804}
5805
5806/// Free all cached (unused) `CallInfo` frames beyond the current frame.
5807///
5808///
5809/// ```c
5810///
5811/// //   CallInfo *ci = L->ci;
5812/// //   CallInfo *next = ci->next;
5813/// //   ci->next = NULL;
5814/// //   while ((ci = next) != NULL) {
5815/// //     next = ci->next;
5816/// //     luaM_free(L, ci);
5817/// //     L->nci--;
5818/// //   }
5819/// // }
5820/// ```
5821///
5822/// PORT NOTE: In C, each `CallInfo` is an independent heap allocation freed by
5823/// `luaM_free`.  In Rust, all `CallInfo` entries live in `state.call_info: Vec<CallInfo>`.
5824/// We walk the link chain to count removals (updating `nci`), then truncate the Vec.
5825/// This is safe as long as all free entries have indices greater than `state.ci`.
5826fn free_ci(state: &mut LuaState) {
5827    let ci_idx = state.ci.0 as usize;
5828
5829    let mut next_opt = state.call_info[ci_idx].next.take();
5830
5831    while let Some(idx) = next_opt {
5832        next_opt = state.call_info[idx.0 as usize].next;
5833        state.nci = state.nci.saturating_sub(1);
5834    }
5835
5836    // Truncate: drop all entries beyond the current ci.
5837    // TODO(port): verify invariant that all cached frames have contiguous indices > state.ci
5838    state.call_info.truncate(ci_idx + 1);
5839}
5840
5841/// Free approximately half of the cached `CallInfo` frames beyond the current frame.
5842///
5843///
5844/// ```c
5845///
5846/// //   CallInfo *ci = L->ci->next;
5847/// //   CallInfo *next;
5848/// //   if (ci == NULL) return;
5849/// //   while ((next = ci->next) != NULL) {
5850/// //     CallInfo *next2 = next->next;
5851/// //     ci->next = next2;
5852/// //     L->nci--;
5853/// //     luaM_free(L, next);
5854/// //     if (next2 == NULL) break;
5855/// //     else { next2->previous = ci; ci = next2; }
5856/// //   }
5857/// // }
5858/// ```
5859///
5860/// PORT NOTE: The C code removes every other node from the free-list chain by
5861/// pointer manipulation.  In Rust, removing elements from the middle of a `Vec`
5862/// shifts subsequent elements and invalidates `CallInfoIdx` values that point
5863/// past the removal site.  For Phase A, we approximate by halving the free count
5864/// via truncation.  TODO(port): Phase B should implement a proper free-list
5865/// pool (e.g., a slab) that allows O(1) element removal without index
5866/// invalidation.
5867pub(crate) fn shrink_ci(state: &mut LuaState) {
5868    let ci_idx = state.ci.0 as usize;
5869
5870    if state.call_info[ci_idx].next.is_none() {
5871        return;
5872    }
5873
5874    let free_count = state.call_info.len().saturating_sub(ci_idx + 1);
5875    if free_count <= 1 {
5876        return;
5877    }
5878
5879    // Remove every other cached frame (halve the free list).
5880    // PERF(port): truncation is O(n) copy for the drop; a slab allocator
5881    // would be O(1) — profile in Phase B.
5882    let keep = free_count / 2;
5883    let removed = free_count - keep;
5884    let new_len = ci_idx + 1 + keep;
5885    state.call_info.truncate(new_len);
5886    state.nci = state.nci.saturating_sub(removed as u32);
5887
5888    // Terminate the now-last cached frame.
5889    if let Some(last) = state.call_info.last_mut() {
5890        last.next = None;
5891    }
5892}
5893
5894/// Check whether the C-call depth has reached its limit and raise an error if so.
5895///
5896///
5897/// ```c
5898///
5899/// //   if (getCcalls(L) == LUAI_MAXCCALLS)
5900/// //     luaG_runerror(L, "C stack overflow");
5901/// //   else if (getCcalls(L) >= (LUAI_MAXCCALLS / 10 * 11))
5902/// //     luaD_throw(L, LUA_ERRERR);
5903/// // }
5904/// ```
5905pub(crate) fn check_c_stack(state: &mut LuaState) -> Result<(), LuaError> {
5906    // macros.tsv: getCcalls → state.c_calls()
5907    // error_sites.tsv: luaG_runerror → return Err(LuaError::runtime(format_args!(...)))
5908    if state.c_calls() == LUAI_MAXCCALLS {
5909        return Err(LuaError::runtime(format_args!("C stack overflow")));
5910    }
5911    // error_sites.tsv: luaD_throw(L, LUA_ERRERR) → return Err(LuaError::with_status(LuaStatus::ErrErr))
5912    if state.c_calls() >= (LUAI_MAXCCALLS / 10 * 11) {
5913        return Err(LuaError::with_status(LuaStatus::ErrErr));
5914    }
5915    Ok(())
5916}
5917
5918/// Increment the C-call depth counter, checking for overflow.
5919///
5920///
5921/// ```c
5922///
5923/// //   L->n_ccalls++;
5924/// //   if (l_unlikely(getCcalls(L) >= LUAI_MAXCCALLS))
5925/// //     luaE_checkcstack(L);
5926/// // }
5927/// ```
5928pub fn inc_c_stack(state: &mut LuaState) -> Result<(), LuaError> {
5929    state.n_ccalls += 1;
5930    // macros.tsv: l_unlikely → x (drop branch hint); getCcalls → state.c_calls()
5931    if state.c_calls() >= LUAI_MAXCCALLS {
5932        check_c_stack(state)?;
5933    }
5934    Ok(())
5935}
5936
5937//
5938// PORT NOTE: In C, `L` is a separate thread used only for memory allocation
5939// (via `luaM_newvector`).  In Rust we don't have a custom allocator; all
5940// allocation goes through the global Rust allocator.  The function takes only
5941// the new thread (`thread`) and ignores the caller.
5942fn stack_init(thread: &mut LuaState) {
5943    // macros.tsv: luaM_newvector → vec![T::default(); n]
5944    let total_slots = BASIC_STACK_SIZE + EXTRA_STACK;
5945    thread.stack = vec![StackValue::default(); total_slots];
5946
5947    // types.tsv: lua_State.tbclist → Vec<StackIdx>
5948    // PORT NOTE: In C, tbclist.p = stack.p is a sentinel meaning "no tbc vars".
5949    // In Rust the Vec is empty when there are no tbc variables.
5950    thread.tbclist = Vec::new();
5951
5952    //      setnilvalue(s2v(L1->stack.p + i));  /* erase new stack */
5953    // macros.tsv: setnilvalue → *o = LuaValue::Nil
5954    // Already initialized to LuaValue::Nil via StackValue::default().
5955
5956    thread.top = StackIdx(0);
5957
5958    thread.stack_last = StackIdx(BASIC_STACK_SIZE as u32);
5959
5960    let base_ci = CallInfo {
5961        func: StackIdx(0),
5962        top: StackIdx(1 + LUA_MINSTACK as u32),
5963        previous: None,
5964        next: None,
5965        callstatus: CIST_C,
5966        call_metamethods: 0,
5967        tailcalls: 0,
5968        nresults: 0,
5969        u: CallInfoFrame::c_default(),
5970        u2: CallInfoExtra::default(),
5971    };
5972
5973    if thread.call_info.is_empty() {
5974        thread.call_info.push(base_ci);
5975    } else {
5976        thread.call_info[0] = base_ci;
5977        thread.call_info.truncate(1);
5978    }
5979
5980    thread.stack[0] = StackValue {
5981        val: LuaValue::Nil,
5982    };
5983
5984    thread.top = StackIdx(1);
5985
5986    thread.ci = CallInfoIdx(0);
5987}
5988
5989fn free_stack(state: &mut LuaState) {
5990    if state.stack.is_empty() {
5991        return;
5992    }
5993    state.ci = CallInfoIdx(0);
5994    free_ci(state);
5995    debug_assert_eq!(state.nci, 0, "nci should be 0 after free_ci");
5996    // macros.tsv: luaM_freearray → (Rust's Drop handles deallocation; drop the call)
5997    state.stack.clear();
5998    state.stack.shrink_to_fit();
5999}
6000
6001fn init_registry(state: &mut LuaState) -> Result<(), LuaError> {
6002    // macros.tsv: luaH_new → state.new_table()
6003    let registry = state.new_table();
6004
6005    // macros.tsv: sethvalue → *o = LuaValue::Table(x.clone())
6006    state.global_mut().l_registry = LuaValue::Table(registry.clone());
6007
6008    // macros.tsv: luaH_resize → t.resize(state, na, nh)?
6009    // TODO(port): registry is a GcRef<LuaTable> (Rc); calling methods requires borrow_mut()
6010    // For Phase A, use RefCell interior mutability on LuaTable, or accept the limitation.
6011    // Using Rc::get_mut is not available because of possible aliasing.
6012    // TODO(port): LuaTable resize requires &mut access through Rc — needs RefCell<LuaTable>
6013    //   or a redesign in Phase B.
6014
6015    // macros.tsv: setthvalue → *o = LuaValue::Thread(x.clone())
6016    // TODO(port): cannot create GcRef<LuaState> to self (self-referential Rc).
6017    // In Phase E this would be resolved once coroutine threads are GcRef-tracked.
6018    // For Phase A: leave registry[LUA_RIDX_MAINTHREAD-1] as Nil and add a TODO.
6019    // TODO(port): set registry[LUA_RIDX_MAINTHREAD - 1] = LuaValue::Thread(main_thread_gcref)
6020
6021    // PORT NOTE (phase-b-reconcile): The lua-types LuaTable placeholder is
6022    // storage-less, so we can't actually persist the globals table inside
6023    // the registry via array_set. Store it in a direct GlobalState field
6024    // and patch get_global_table to read it from there. Symmetric for the
6025    // _LOADED module cache. Once the LuaTable placeholder reconciles, the
6026    // canonical registry storage takes over and these fields disappear.
6027    let globals = state.new_table();
6028    state.global_mut().globals = LuaValue::Table(globals);
6029    let loaded = state.new_table();
6030    state.global_mut().loaded = LuaValue::Table(loaded);
6031
6032    Ok(())
6033}
6034
6035fn lua_open(state: &mut LuaState) -> Result<(), LuaError> {
6036    stack_init(state);
6037    init_registry(state)?;
6038    crate::string::init(state)?;
6039    crate::tagmethods::init(state)?;
6040    // TODO(port): luaX_init lives in the lua-lex crate; cross-crate call needed in Phase B
6041    state.global_mut().gcstp = 0;
6042    state.global().heap.unpause();
6043    // macros.tsv: setnilvalue → *o = LuaValue::Nil
6044    // PORT NOTE: setting nilvalue = Nil signals completestate() → is_complete() = true
6045    state.global_mut().nilvalue = LuaValue::Nil;
6046    // macros.tsv: luai_userstateopen → (extension hook, no-op default; drop)
6047    Ok(())
6048}
6049
6050fn preinit_thread(thread: &mut LuaState, global: Rc<RefCell<GlobalState>>) {
6051    thread.global = global;
6052    thread.stack = Vec::new();
6053    thread.call_info = Vec::new();
6054    // PORT NOTE: We initialize ci to 0 but call_info is empty; stack_init() must be
6055    // called before any use of call_info.
6056    thread.ci = CallInfoIdx(0);
6057    thread.nci = 0;
6058    // PORT NOTE: In C, L->twups = L is a self-reference sentinel meaning "no open upvals".
6059    // In Rust, GlobalState.twups is a Vec<GcRef<LuaState>>; absence from that Vec is the
6060    // sentinel.  The per-thread `twups` field is removed (types.tsv: lua_State.twups → removed).
6061    thread.n_ccalls = 0;
6062    thread.hook = None;
6063    thread.hookmask = 0;
6064    thread.basehookcount = 0;
6065    thread.allowhook = true;
6066    // macros.tsv: resethookcount → state.reset_hook_count()
6067    thread.hookcount = thread.basehookcount;
6068
6069    // Sandbox inheritance: a coroutine joins the runtime-wide instruction/memory
6070    // budget so metering spans every thread, not just the main one. The budget
6071    // itself lives in `GlobalState` (shared); the new thread only needs the
6072    // count-hook mask armed so the dispatch loop traps and charges it.
6073    {
6074        let (active, interval) = {
6075            let g = thread.global.borrow();
6076            (g.sandbox_active(), g.sandbox.interval.get())
6077        };
6078        if active {
6079            thread.hookmask = SANDBOX_COUNT_MASK;
6080            thread.basehookcount = interval;
6081            thread.hookcount = interval;
6082        }
6083    }
6084    thread.openupval = Vec::new();
6085    thread.status = LuaStatus::Ok as u8;
6086    thread.errfunc = 0;
6087    thread.oldpc = 0;
6088    thread.gc_check_needed = true;
6089}
6090
6091fn close_state(state: &mut LuaState) {
6092    let is_complete = state.global().is_complete();
6093
6094    if !is_complete {
6095        // macros.tsv: luaC_freeallobjects via GcHandle
6096        state.gc().free_all_objects();
6097    } else {
6098        state.ci = CallInfoIdx(0);
6099        // TODO(port): crate::do_::close_protected(state, StackIdx(1), LuaStatus::Ok)
6100        // Ignoring result here because we are in teardown (same as C behavior).
6101        state.gc().free_all_objects();
6102        // macros.tsv: luai_userstateclose → (extension hook; drop)
6103    }
6104
6105    // macros.tsv: luaM_freearray → (Rust's Drop handles deallocation; drop the call)
6106    state.global_mut().strt = StringPool::default();
6107
6108    free_stack(state);
6109
6110    // PORT NOTE: C-specific memory accounting assertion; not applicable in Rust.
6111
6112    // PORT NOTE: Custom allocator freed LG here. Rust's allocator (via Drop) handles
6113    // deallocation of GlobalState and LuaState automatically.
6114}
6115
6116/// Create a new coroutine thread sharing the same GlobalState as the caller.
6117///
6118/// Pushes the new thread onto the caller's stack and returns `Ok(())`.
6119///
6120///
6121/// ```c
6122///
6123/// //   global_State *g = G(L);
6124/// //   GCObject *o;
6125/// //   lua_State *L1;
6126/// //   lua_lock(L); luaC_checkGC(L);
6127/// //   o = luaC_newobjdt(L, LUA_TTHREAD, sizeof(LX), offsetof(LX, l));
6128/// //   L1 = gco2th(o);
6129/// //   setthvalue2s(L, L->top.p, L1); api_incr_top(L);
6130/// //   preinit_thread(L1, g);
6131/// //   ... (copy hook settings, extra space, stack_init) ...
6132/// //   lua_unlock(L); return L1;
6133/// // }
6134/// ```
6135/// Allocate a fresh coroutine `LuaState`, register it under a new
6136/// `ThreadId`, and push the resulting `LuaValue::Thread(value)` onto
6137/// `state`'s stack.
6138///
6139/// If `initial_body` is `Some(f)`, `f` is also pushed onto the new
6140/// thread's stack so that `coroutine.status` reports `"suspended"`
6141/// rather than `"dead"`. The full cross-thread `xmove` from caller to
6142/// coroutine arrives in slice 02b; `co_create` uses `initial_body` to
6143/// stage the body without needing a real `xmove`.
6144pub fn new_thread(state: &mut LuaState, initial_body: Option<LuaValue>) -> Result<(), LuaError> {
6145    state.gc_pre_collect_clear();
6146    state.gc().check_step();
6147
6148    // PORT NOTE: In C, the new thread is GC-allocated as part of the allgc list.
6149    // In Rust (Phase A), we create a plain LuaState; Phase D will wire GC registration.
6150    // TODO(port): allocate via state.gc().new_obj(LuaType::Thread, ...) in Phase D
6151
6152    let global_rc = state.global_rc();
6153    let hookmask = state.hookmask;
6154    let basehookcount = state.basehookcount;
6155
6156    let reserved_id = {
6157        let mut g = state.global_mut();
6158        let id = g.next_thread_id;
6159        g.next_thread_id += 1;
6160        id
6161    };
6162
6163    // Lua 5.1: a coroutine inherits its creator's global table (`l_gt`) at
6164    // creation, after which the two are independent. Seed the new thread's
6165    // `thread_globals` slot with the running thread's current `l_gt` so the
6166    // coroutine's `setfenv(0, t)` and freshly-loaded chunks resolve through
6167    // its own slot, never the main thread's `globals`. Inert on 5.2–5.5.
6168    if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
6169        let creator_id = state.global().current_thread_id;
6170        let creator_lgt = state.v51_thread_lgt(creator_id);
6171        state
6172            .global_mut()
6173            .thread_globals
6174            .insert(reserved_id, creator_lgt);
6175    }
6176
6177    let mut new_thread = LuaState {
6178        status: LuaStatus::Ok as u8,
6179        allowhook: true,
6180        nci: 0,
6181        top: StackIdx(0),
6182        stack_last: StackIdx(0),
6183        stack: Vec::new(),
6184        ci: CallInfoIdx(0),
6185        call_info: Vec::new(),
6186        openupval: Vec::new(),
6187        legacy_open_upval_touched: std::cell::Cell::new(false),
6188        tbclist: Vec::new(),
6189        global: global_rc.clone(),
6190        hook: None,
6191        hookmask: 0,
6192        basehookcount: 0,
6193        hookcount: 0,
6194        errfunc: 0,
6195        n_ccalls: 0,
6196        oldpc: 0,
6197        marked: 0,
6198        cached_thread_id: reserved_id,
6199        gc_check_needed: false,
6200    };
6201
6202    preinit_thread(&mut new_thread, global_rc);
6203
6204    new_thread.hookmask = hookmask;
6205    new_thread.basehookcount = basehookcount;
6206    // TODO(port): lua_Hook is Box<dyn FnMut(...)>; not Clone.
6207    // Sharing a hook between threads would require Arc<Mutex<...>> (Phase E debug).
6208    new_thread.reset_hook_count();
6209
6210    // macros.tsv: lua_getextraspace → state.extra_space_mut() → &mut [u8]
6211    // TODO(port): LuaState.extra_space field not yet defined; Phase B
6212
6213    // macros.tsv: luai_userstatethread → (extension hook; drop)
6214
6215    stack_init(&mut new_thread);
6216
6217    if let Some(body) = initial_body {
6218        new_thread.push(body);
6219    }
6220
6221    let thread_ref: Rc<RefCell<LuaState>> = Rc::new(RefCell::new(new_thread));
6222
6223    let value = {
6224        let mut g = state.global_mut();
6225        let id = reserved_id;
6226        let value = GcRef::new(lua_types::value::LuaThread::new(id));
6227        g.threads.insert(
6228            id,
6229            ThreadRegistryEntry {
6230                state: thread_ref,
6231                value: value.clone(),
6232            },
6233        );
6234        value
6235    };
6236
6237    state.push(LuaValue::Thread(value));
6238
6239    Ok(())
6240}
6241
6242/// Reset a thread to its base state, closing all to-be-closed variables.
6243///
6244/// Returns the final status code as an `i32` (mirrors the C API).
6245///
6246///
6247/// ```c
6248///
6249/// //   CallInfo *ci = L->ci = &L->base_ci;
6250/// //   setnilvalue(s2v(L->stack.p));
6251/// //   ci->func.p = L->stack.p;
6252/// //   ci->callstatus = CIST_C;
6253/// //   if (status == LUA_YIELD) status = LUA_OK;
6254/// //   L->status = LUA_OK;  /* so it can run __close metamethods */
6255/// //   status = luaD_closeprotected(L, 1, status);
6256/// //   if (status != LUA_OK) luaD_seterrorobj(L, status, L->stack.p + 1);
6257/// //   else L->top.p = L->stack.p + 1;
6258/// //   ci->top.p = L->top.p + LUA_MINSTACK;
6259/// //   luaD_reallocstack(L, cast_int(ci->top.p - L->stack.p), 0);
6260/// //   return status;
6261/// // }
6262/// ```
6263pub fn reset_thread(state: &mut LuaState, status: i32) -> i32 {
6264    state.ci = CallInfoIdx(0);
6265    let ci_idx = 0usize;
6266
6267    // macros.tsv: setnilvalue → *o = LuaValue::Nil; s2v → state.stack_at(idx)
6268    if !state.stack.is_empty() {
6269        state.stack[0].val = LuaValue::Nil;
6270    }
6271
6272    state.call_info[ci_idx].func = StackIdx(0);
6273    state.call_info[ci_idx].call_metamethods = 0;
6274    state.call_info[ci_idx].callstatus = CIST_C;
6275
6276    let mut status = if status == LuaStatus::Yield as i32 {
6277        LuaStatus::Ok as i32
6278    } else {
6279        status
6280    };
6281
6282    state.status = LuaStatus::Ok as u8;
6283
6284    let close_status = crate::do_::close_protected(state, StackIdx(1), LuaStatus::from_raw(status));
6285    status = close_status as i32;
6286
6287    if status != LuaStatus::Ok as i32 {
6288        crate::do_::set_error_obj(state, LuaStatus::from_raw(status), StackIdx(1));
6289    } else {
6290        state.top = StackIdx(1);
6291    }
6292
6293    let new_ci_top = StackIdx(state.top.0 + LUA_MINSTACK as u32);
6294    state.call_info[ci_idx].top = new_ci_top;
6295
6296    // TODO(port): crate::do_::realloc_stack(state, new_ci_top.0 as i32, 0) — ldo.c → do_.rs
6297    // For Phase A, grow the stack if needed to at least new_ci_top slots.
6298    let needed = new_ci_top.0 as usize;
6299    if state.stack.len() < needed {
6300        state.stack.resize(needed, StackValue::default());
6301    }
6302
6303    status
6304}
6305
6306/// Close a coroutine thread from the perspective of another thread.
6307///
6308///
6309/// ```c
6310///
6311/// //   int status;
6312/// //   lua_lock(L);
6313/// //   L->n_ccalls = (from) ? getCcalls(from) : 0;
6314/// //   status = luaE_resetthread(L, L->status);
6315/// //   lua_unlock(L);
6316/// //   return status;
6317/// // }
6318/// ```
6319pub fn close_thread(state: &mut LuaState, from: Option<&LuaState>) -> i32 {
6320    // macros.tsv: getCcalls → state.c_calls()
6321    state.n_ccalls = match from {
6322        Some(f) => f.c_calls(),
6323        None => 0,
6324    };
6325    let current_status = state.status as i32;
6326    let result = reset_thread(state, current_status);
6327    result
6328}
6329
6330/// Deprecated wrapper for `close_thread(L, NULL)`.
6331///
6332///
6333/// ```c
6334///
6335/// //   return lua_closethread(L, NULL);
6336/// // }
6337/// ```
6338pub fn reset_thread_api(state: &mut LuaState) -> i32 {
6339    close_thread(state, None)
6340}
6341
6342/// Create a new independent Lua state.  Returns `None` only on OOM.
6343///
6344///
6345/// PORT NOTE: The C API takes a custom allocator `(f, ud)`.  The Rust-native API
6346/// uses the global Rust allocator; those parameters are dropped.  Equivalent to
6347/// `LuaState::new()` at the call site.
6348///
6349/// ```c
6350///
6351/// //   int i;
6352/// //   lua_State *L;
6353/// //   global_State *g;
6354/// //   LG *l = cast(LG *, (*f)(ud, NULL, LUA_TTHREAD, sizeof(LG)));
6355/// //   if (l == NULL) return NULL;
6356/// //   L = &l->l.l; g = &l->g;
6357/// //   L->tt = LUA_VTHREAD;
6358/// //   g->currentwhite = bitmask(WHITE0BIT);
6359/// //   L->marked = luaC_white(g);
6360/// //   preinit_thread(L, g);
6361/// //   g->allgc = obj2gco(L);
6362/// //   L->next = NULL;
6363/// //   incnny(L);
6364/// //   g->frealloc = f; g->ud = ud; g->warnf = NULL; g->ud_warn = NULL;
6365/// //   g->mainthread = L; g->seed = luai_makeseed(L);
6366/// //   g->gcstp = GCSTPGC;
6367/// //   ... (zero-init all GC list pointers and tunables) ...
6368/// //   setivalue(&g->nilvalue, 0);  /* signal: state not yet built */
6369/// //   ... (setgcparam tunables) ...
6370/// //   for (i=0; i < LUA_NUMTAGS; i++) g->mt[i] = NULL;
6371/// //   if (luaD_rawrunprotected(L, f_luaopen, NULL) != LUA_OK) {
6372/// //     close_state(L); L = NULL;
6373/// //   }
6374/// //   return L;
6375/// // }
6376/// ```
6377pub fn new_state() -> Option<LuaState> {
6378    // In Rust, allocation failure panics by default; we use Result internally.
6379
6380    // Build a dummy LuaString for memerrmsg and strcache initialization.
6381    // This is a chicken-and-egg problem: GlobalState.memerrmsg needs to be initialized
6382    // before luaS_init, but luaS_init creates the memerrmsg.
6383    // The heap is created ahead of the GlobalState literal so these two
6384    // pre-state allocations (the placeholder string and the main thread
6385    // value) can live on its heap-owned uncollected list instead of falling
6386    // through GcRef::new's detached arm — this was the last per-VM
6387    // process-lifetime leak, and removing it lets LUA_RS_GC_STRICT_GUARD
6388    // police the detached arm with zero sanctioned exceptions. Moving the
6389    // heap into the literal below is safe: the owner-list heads are Cells
6390    // whose GcBox pointees are stable heap allocations.
6391    let heap = lua_gc::Heap::new();
6392    let placeholder_str = GcRef(heap.allocate_uncollected(LuaString::placeholder()));
6393    let main_thread_value = GcRef(heap.allocate_uncollected(lua_types::value::LuaThread::new(0)));
6394
6395    // macros.tsv: bitmask → (1u32 << b); WHITE0BIT = 0 → 1u8
6396    let initial_white = 1u8 << WHITE0BIT;
6397
6398    // macros.tsv: setivalue → *o = LuaValue::Int(x)
6399    // PORT NOTE: non-nil nilvalue signals "state not yet complete"; see is_complete().
6400
6401    let global = GlobalState {
6402        parser_hook: None,
6403        cli_argv: None,
6404        cli_preload: None,
6405        lua_version: lua_types::LuaVersion::default(),
6406        file_loader_hook: None,
6407        file_open_hook: None,
6408        stdout_hook: None,
6409        stderr_hook: None,
6410        stdin_hook: None,
6411        env_hook: None,
6412        unix_time_hook: None,
6413        cpu_clock_hook: None,
6414        local_offset_hook: None,
6415        entropy_hook: None,
6416        temp_name_hook: None,
6417        popen_hook: None,
6418        file_remove_hook: None,
6419        file_rename_hook: None,
6420        os_execute_hook: None,
6421        dynlib_load_hook: None,
6422        dynlib_symbol_hook: None,
6423        dynlib_unload_hook: None,
6424        sandbox: SandboxLimits::default(),
6425        gc_debt: 0,
6426        gc_estimate: 0,
6427        lastatomic: 0,
6428        strt: StringPool::default(),
6429        l_registry: LuaValue::Nil,
6430        external_roots: ExternalRootSet::default(),
6431        globals: LuaValue::Nil,
6432        loaded: LuaValue::Nil,
6433        nilvalue: LuaValue::Int(0),
6434        seed: make_seed(),
6435        currentwhite: initial_white,
6436        gcstate: GCS_PAUSE,
6437        // macros.tsv: KGC_INC → GcKind::Incremental
6438        gckind: GcKind::Incremental as u8,
6439        gcstopem: false,
6440        genminormul: LUAI_GENMINORMUL,
6441        // macros.tsv: setgcparam → p = v / 4
6442        genmajormul: (LUAI_GENMAJORMUL / 4) as u8,
6443        gcstp: GCSTPGC,
6444        gcemergency: false,
6445        gcpause: (LUAI_GCPAUSE / 4) as u8,
6446        gcstepmul: (LUAI_GCMUL / 4) as u8,
6447        gcstepsize: LUAI_GCSTEPSIZE,
6448        // Lua 5.5 collectgarbage("param") defaults, observed on lua5.5.0:
6449        // [minormul, majorminor, minormajor, pause, stepmul, stepsize].
6450        gc55_params: [20, 50, 68, 250, 200, 9600],
6451        sweepgc_cursor: 0,
6452        weak_tables_registry: lua_gc::WeakRegistry::default(),
6453        finalizers: lua_gc::FinalizerRegistry::default(),
6454        gc_finalizer_error: None,
6455        twups: Vec::new(),
6456        panic: None,
6457        mainthread: None,
6458        threads: std::collections::HashMap::new(),
6459        main_thread_value,
6460        current_thread_id: 0,
6461        closing_thread_id: None,
6462        main_thread_id: 0,
6463        next_thread_id: 1,
6464        thread_globals: std::collections::HashMap::new(),
6465        closure_envs: std::collections::HashMap::new(),
6466        memerrmsg: placeholder_str.clone(),
6467        tmname: Vec::new(),
6468        mt: std::array::from_fn(|_| None),
6469        strcache: std::array::from_fn(|_| std::array::from_fn(|_| placeholder_str.clone())),
6470        interned_lt: InternedStringMap::default(),
6471        warnf: None,
6472        warn_mode: WarnMode::Off,
6473        test_warn_enabled: false,
6474        test_warn_on: false,
6475        test_warn_mode: TestWarnMode::Normal,
6476        test_warn_last_to_cont: false,
6477        test_warn_buffer: Vec::new(),
6478        c_functions: Vec::new(),
6479        heap,
6480        cross_thread_upvals: std::collections::HashMap::new(),
6481        suspended_parent_stacks: Vec::new(),
6482        suspended_parent_open_upvals: Vec::new(),
6483        snapshot_stack_pool: Vec::new(),
6484        snapshot_upval_pool: Vec::new(),
6485        resume_value_pool: Vec::new(),
6486        resume_upval_slot_pool: Vec::new(),
6487        resume_flush_pool: Vec::new(),
6488    };
6489
6490    let global_rc = Rc::new(RefCell::new(global));
6491
6492    // issue #249: from here through the end of lua_open() below (registry,
6493    // string pool, tagmethod tables), route allocations onto the heap's
6494    // bootstrap ("uncollected but heap-owned") list rather than the normal
6495    // collectable list. The object graph isn't self-consistent for root
6496    // tracing yet, and lua_open() clears `paused` partway through, so an
6497    // allocation-triggered step during setup must not sweep these objects.
6498    // Heap::drop_all() still frees them when the Lua instance drops — they
6499    // just never leak past that, unlike the pre-D-1c `Gc::new_uncollected`
6500    // fallback. RAII scope: the lua_open error return below cannot leave the
6501    // heap stuck in bootstrap mode. Callers that continue bootstrapping
6502    // (stdlib install) open a nested window of their own; callers that use
6503    // new_state() directly (low-level GC tests) get a heap that allocates
6504    // normally once this scope drops at return.
6505    let _bootstrap_scope = global_rc.borrow().heap.bootstrap_scope();
6506    let _bootstrap_guard = lua_gc::HeapGuard::push(&global_rc.borrow().heap);
6507
6508    // macros.tsv: luaC_white → g.current_white()
6509    let initial_marked = initial_white;
6510
6511    let mut main_thread = LuaState {
6512        status: LuaStatus::Ok as u8,
6513        allowhook: true,
6514        nci: 0,
6515        top: StackIdx(0),
6516        stack_last: StackIdx(0),
6517        stack: Vec::new(),
6518        ci: CallInfoIdx(0),
6519        call_info: Vec::new(),
6520        openupval: Vec::new(),
6521        legacy_open_upval_touched: std::cell::Cell::new(false),
6522        tbclist: Vec::new(),
6523        global: global_rc.clone(),
6524        hook: None,
6525        hookmask: 0,
6526        basehookcount: 0,
6527        hookcount: 0,
6528        errfunc: 0,
6529        n_ccalls: 0,
6530        oldpc: 0,
6531        marked: initial_marked,
6532        cached_thread_id: 0,
6533        gc_check_needed: false,
6534    };
6535
6536    preinit_thread(&mut main_thread, global_rc.clone());
6537
6538    // macros.tsv: incnny → state.inc_nny() → L->n_ccalls += 0x10000
6539    main_thread.inc_nny();
6540
6541    // TODO(port): self-referential Rc cycle; Phase D GC handles cycles.
6542    // For Phase A: skip setting mainthread to avoid the cycle.
6543
6544    // TODO(port): Phase D — register main_thread in allgc as a GcRef
6545
6546    //      close_state(L); L = NULL; }
6547    // error_sites.tsv: luaD_rawrunprotected → state.run_protected(|s| f(s, ud))
6548    // PORT NOTE: We call lua_open directly since we're not using the protected-call
6549    // machinery yet (ldo.c is not ported). Errors from lua_open propagate as Err.
6550    match lua_open(&mut main_thread) {
6551        Ok(()) => {}
6552        Err(_) => {
6553            close_state(&mut main_thread);
6554            return None;
6555        }
6556    }
6557
6558    Some(main_thread)
6559}
6560
6561/// Close the Lua state and free all resources.
6562///
6563///
6564/// PORT NOTE: In C, `lua_close` gets the main thread via `G(L)->mainthread`
6565/// and closes that regardless of which thread is passed.  In Rust, the caller
6566/// should hold the main `LuaState` and drop it (which triggers `close_state`
6567/// via this function or `Drop`).
6568///
6569/// ```c
6570///
6571/// //   lua_lock(L);
6572/// //   L = G(L)->mainthread;  /* only the main thread can be closed */
6573/// //   close_state(L);
6574/// // }
6575/// ```
6576pub fn close(mut state: LuaState) {
6577    // PORT NOTE: In Rust, callers must pass the main LuaState directly (or obtain it
6578    // from GlobalState.mainthread).  We do not traverse to the main thread here;
6579    // the caller owns the root state.
6580    // TODO(port): assert that `state` is indeed the main thread before closing
6581    close_state(&mut state);
6582}
6583
6584/// Forward a warning message through the configured warning sink.
6585///
6586///
6587/// ```c
6588///
6589/// //   lua_WarnFunction wf = G(L)->warnf;
6590/// //   if (wf != NULL) wf(G(L)->ud_warn, msg, tocont);
6591/// // }
6592/// ```
6593pub(crate) fn warning(state: &mut LuaState, msg: &[u8], to_cont: bool) {
6594    let test_warn_enabled = state.global().test_warn_enabled;
6595    if test_warn_enabled {
6596        test_warn(state, msg, to_cont);
6597        return;
6598    }
6599
6600    // types.tsv: global_State.warnf → Option<Box<dyn FnMut(&[u8], bool)>>
6601    // types.tsv: global_State.ud_warn → (removed; folded into the closure)
6602    // PORT NOTE: We must drop the RefMut borrow before calling the closure to avoid
6603    // a potential re-entrant borrow_mut() if the closure calls back into Lua.
6604    // We check for the presence of warnf while holding a borrow, then call it.
6605    // TODO(port): if the warning function needs to call back into state (e.g. to push
6606    // a Lua error), this will panic at runtime due to RefCell re-entry. Phase B should
6607    // design a safe re-entrance pattern (e.g. take + restore the warnf closure).
6608    let has_warnf = state.global().warnf.is_some();
6609    if has_warnf {
6610        // Take the warnf closure out to avoid re-entrant borrow.
6611        let mut warnf = state.global_mut().warnf.take();
6612        if let Some(ref mut f) = warnf {
6613            f(msg, to_cont);
6614        }
6615        // Restore the closure.
6616        state.global_mut().warnf = warnf;
6617        return;
6618    }
6619    default_warn(state, msg, to_cont);
6620}
6621
6622fn test_warn(state: &mut LuaState, msg: &[u8], to_cont: bool) {
6623    let is_control = {
6624        let g = state.global();
6625        !g.test_warn_last_to_cont && !to_cont && msg.first() == Some(&b'@')
6626    };
6627    if is_control {
6628        let mut g = state.global_mut();
6629        match &msg[1..] {
6630            b"off" => g.test_warn_on = false,
6631            b"on" => g.test_warn_on = true,
6632            b"normal" => g.test_warn_mode = TestWarnMode::Normal,
6633            b"allow" => g.test_warn_mode = TestWarnMode::Allow,
6634            b"store" => g.test_warn_mode = TestWarnMode::Store,
6635            _ => {}
6636        }
6637        return;
6638    }
6639
6640    let finished = {
6641        let mut g = state.global_mut();
6642        g.test_warn_last_to_cont = to_cont;
6643        g.test_warn_buffer.extend_from_slice(msg);
6644        if to_cont {
6645            None
6646        } else {
6647            Some((
6648                std::mem::take(&mut g.test_warn_buffer),
6649                g.test_warn_mode,
6650                g.test_warn_on,
6651            ))
6652        }
6653    };
6654
6655    let Some((message, mode, warn_on)) = finished else {
6656        return;
6657    };
6658    match mode {
6659        TestWarnMode::Normal => {
6660            if warn_on && message.first() == Some(&b'#') {
6661                write_warning_message(&message);
6662            }
6663        }
6664        TestWarnMode::Allow => {
6665            if warn_on {
6666                write_warning_message(&message);
6667            }
6668        }
6669        TestWarnMode::Store => {
6670            if let Ok(s) = state.intern_str(&message) {
6671                state.push(LuaValue::Str(s));
6672                let _ = crate::api::set_global(state, b"_WARN");
6673            }
6674        }
6675    }
6676}
6677
6678fn write_warning_message(message: &[u8]) {
6679    use std::io::Write;
6680    let stderr = std::io::stderr();
6681    let mut h = stderr.lock();
6682    let _ = h.write_all(b"Lua warning: ");
6683    let _ = h.write_all(message);
6684    let _ = h.write_all(b"\n");
6685}
6686
6687/// The default warning handler: a faithful port of the `warnfoff` /
6688/// `warnfon` / `warnfcont` chain in upstream `lauxlib.c`. State is held in
6689/// `GlobalState::warn_mode` (C threads it via `lua_setwarnf`); output goes to
6690/// stderr (`lua_writestringerror`).
6691fn default_warn(state: &mut LuaState, msg: &[u8], to_cont: bool) {
6692    use std::io::Write;
6693    // checkcontrol: a leading-`@` non-continuation message is a control word.
6694    if !to_cont && msg.first() == Some(&b'@') {
6695        match &msg[1..] {
6696            b"off" => state.global_mut().warn_mode = WarnMode::Off,
6697            b"on" => state.global_mut().warn_mode = WarnMode::On,
6698            _ => {}
6699        }
6700        return;
6701    }
6702    let mode = state.global().warn_mode;
6703    match mode {
6704        WarnMode::Off => {}
6705        WarnMode::On | WarnMode::Cont => {
6706            let stderr = std::io::stderr();
6707            let mut h = stderr.lock();
6708            if mode == WarnMode::On {
6709                let _ = h.write_all(b"Lua warning: ");
6710            }
6711            let _ = h.write_all(msg);
6712            if to_cont {
6713                state.global_mut().warn_mode = WarnMode::Cont;
6714            } else {
6715                let _ = h.write_all(b"\n");
6716                state.global_mut().warn_mode = WarnMode::On;
6717            }
6718        }
6719    }
6720}
6721
6722#[cfg(test)]
6723mod tests {
6724    use super::*;
6725
6726    fn test_noop_cclosure(_: &mut LuaState) -> Result<usize, LuaError> {
6727        Ok(0)
6728    }
6729
6730    #[test]
6731    fn external_root_keys_reject_stale_slot_after_reuse() {
6732        let mut roots = ExternalRootSet::default();
6733
6734        let first = roots.insert(LuaValue::Int(1));
6735        assert_eq!(roots.len(), 1);
6736        assert_eq!(roots.get(first), Some(&LuaValue::Int(1)));
6737
6738        assert_eq!(roots.remove(first), Some(LuaValue::Int(1)));
6739        assert!(roots.get(first).is_none());
6740        assert!(roots.remove(first).is_none());
6741        assert_eq!(roots.len(), 0);
6742        assert_eq!(roots.vacant_len(), 1);
6743        assert!(roots.replace(first, LuaValue::Int(9)).is_none());
6744        assert!(roots.is_empty());
6745
6746        let second = roots.insert(LuaValue::Int(2));
6747        assert_eq!(first.index, second.index);
6748        assert_ne!(first, second);
6749        assert!(roots.get(first).is_none());
6750        assert_eq!(roots.get(second), Some(&LuaValue::Int(2)));
6751        assert!(roots.replace(first, LuaValue::Int(3)).is_none());
6752    }
6753
6754    #[test]
6755    fn external_roots_keep_heap_value_alive_until_unrooted() {
6756        let mut state = new_state().expect("state should initialize");
6757        let _heap_guard = {
6758            let g = state.global();
6759            lua_gc::HeapGuard::push(&g.heap)
6760        };
6761
6762        let table = state.new_table();
6763        assert_eq!(state.global().heap.allgc_count(), 1);
6764
6765        let key = state.external_root_value(LuaValue::Table(table));
6766        state.gc().full_collect();
6767        assert_eq!(state.global().heap.allgc_count(), 1);
6768        assert_eq!(state.global().external_roots.len(), 1);
6769
6770        assert!(state.external_unroot_value(key).is_some());
6771        state.gc().full_collect();
6772        assert_eq!(state.global().heap.allgc_count(), 0);
6773        assert!(state.global().external_roots.is_empty());
6774    }
6775
6776    #[test]
6777    fn interned_string_table_grows_then_shrinks_on_collection() {
6778        let mut state = new_state().expect("state should initialize");
6779        let _heap_guard = {
6780            let g = state.global();
6781            lua_gc::HeapGuard::push(&g.heap)
6782        };
6783
6784        let initial_buckets = state.global().interned_lt.bucket_count();
6785        assert_eq!(
6786            initial_buckets, 64,
6787            "the intern table starts at C's MINSTRTABSIZE-equivalent of 64"
6788        );
6789        let baseline_live = state.global().interned_lt.len();
6790
6791        let mut anchors: Vec<GcRef<LuaString>> = Vec::with_capacity(2000);
6792        for i in 0..2000usize {
6793            let key = format!("intern-shrink-probe-{i:08}");
6794            let s = state
6795                .intern_str(key.as_bytes())
6796                .expect("short string interns");
6797            anchors.push(s);
6798        }
6799
6800        let grown_buckets = state.global().interned_lt.bucket_count();
6801        assert_eq!(
6802            state.global().interned_lt.len(),
6803            baseline_live + 2000,
6804            "all 2000 distinct short strings are interned and live alongside \
6805             the runtime's own rooted strings"
6806        );
6807        assert!(
6808            grown_buckets >= 2048,
6809            "interning 2000 strings must force several bucket doublings past \
6810             the initial 64 (saw {grown_buckets})"
6811        );
6812
6813        drop(anchors);
6814        state.gc().full_collect();
6815
6816        let shrunk_buckets = state.global().interned_lt.bucket_count();
6817        let surviving_live = state.global().interned_lt.len();
6818        assert!(
6819            surviving_live <= baseline_live,
6820            "every probe string is unreachable and must be swept out of the \
6821             intern table, leaving at most the runtime's own strings (baseline \
6822             {baseline_live}, surviving {surviving_live})"
6823        );
6824        assert!(
6825            surviving_live < 2000,
6826            "the 2000 probe strings must not survive collection (surviving \
6827             {surviving_live})"
6828        );
6829        assert_eq!(
6830            shrunk_buckets, 64,
6831            "the batch-end shrink check must reclaim the stale buckets back to \
6832             the 64-bucket floor (saw {shrunk_buckets}, grew to {grown_buckets})"
6833        );
6834        assert!(
6835            shrunk_buckets < grown_buckets,
6836            "shrink must strictly reduce the bucket array"
6837        );
6838    }
6839
6840    #[test]
6841    fn table_buffer_accounting_refunds_on_sweep() {
6842        let mut state = new_state().expect("state should initialize");
6843        let _heap_guard = {
6844            let g = state.global();
6845            lua_gc::HeapGuard::push(&g.heap)
6846        };
6847
6848        let table = state.new_table();
6849        let key = state.external_root_value(LuaValue::Table(table));
6850        let header_bytes = state.global().heap.bytes_used();
6851        assert!(header_bytes > 0);
6852
6853        for i in 1..=128 {
6854            table
6855                .raw_set_int(&mut state, i, LuaValue::Int(i))
6856                .expect("integer table insert should succeed");
6857        }
6858        let grown_bytes = state.global().heap.bytes_used();
6859        assert!(
6860            grown_bytes > header_bytes,
6861            "table array/hash buffer growth must be charged to the GC heap"
6862        );
6863
6864        state.gc().full_collect();
6865        assert_eq!(
6866            state.global().heap.bytes_used(),
6867            grown_bytes,
6868            "rooted table buffer bytes should remain charged after collection"
6869        );
6870
6871        assert!(state.external_unroot_value(key).is_some());
6872        state.gc().full_collect();
6873        assert_eq!(state.global().heap.bytes_used(), 0);
6874        assert_eq!(state.global().heap.allgc_count(), 0);
6875    }
6876
6877    #[test]
6878    fn userdata_buffer_accounting_refunds_on_sweep() {
6879        let mut state = new_state().expect("state should initialize");
6880        let _heap_guard = {
6881            let g = state.global();
6882            lua_gc::HeapGuard::push(&g.heap)
6883        };
6884
6885        let payload_len = 4096;
6886        let userdata = state
6887            .new_userdata_typed(b"accounting", payload_len, 3)
6888            .expect("userdata allocation should succeed");
6889        state.pop_n(1);
6890        let key = state.external_root_value(LuaValue::UserData(userdata));
6891        let allocated_bytes = state.global().heap.bytes_used();
6892        assert!(
6893            allocated_bytes > payload_len,
6894            "userdata payload bytes must be charged to the GC heap"
6895        );
6896
6897        state.gc().full_collect();
6898        assert_eq!(
6899            state.global().heap.bytes_used(),
6900            allocated_bytes,
6901            "rooted userdata payload bytes should remain charged after collection"
6902        );
6903
6904        assert!(state.external_unroot_value(key).is_some());
6905        state.gc().full_collect();
6906        assert_eq!(state.global().heap.bytes_used(), 0);
6907        assert_eq!(state.global().heap.allgc_count(), 0);
6908    }
6909
6910    #[test]
6911    fn cclosure_upvalue_accounting_refunds_on_sweep() {
6912        let mut state = new_state().expect("state should initialize");
6913        let _heap_guard = {
6914            let g = state.global();
6915            lua_gc::HeapGuard::push(&g.heap)
6916        };
6917
6918        let nupvalues = 64;
6919        for i in 0..nupvalues {
6920            state.push(LuaValue::Int(i as i64));
6921        }
6922        crate::api::push_cclosure(&mut state, test_noop_cclosure, nupvalues as i32)
6923            .expect("C closure creation should succeed");
6924        let LuaValue::Function(LuaClosure::C(ccl)) = state.get_at(state.top_idx() - 1) else {
6925            panic!("expected heavy C closure");
6926        };
6927        let expected_payload = ccl.buffer_bytes();
6928        let key = state.external_root_value(LuaValue::Function(LuaClosure::C(ccl)));
6929        state.pop_n(1);
6930        let allocated_bytes = state.global().heap.bytes_used();
6931        assert!(
6932            allocated_bytes >= expected_payload,
6933            "C closure upvalue vector bytes must be charged to the GC heap"
6934        );
6935
6936        state.gc().full_collect();
6937        assert_eq!(
6938            state.global().heap.bytes_used(),
6939            allocated_bytes,
6940            "rooted C closure payload bytes should remain charged after collection"
6941        );
6942
6943        assert!(state.external_unroot_value(key).is_some());
6944        state.gc().full_collect();
6945        assert_eq!(state.global().heap.bytes_used(), 0);
6946        assert_eq!(state.global().heap.allgc_count(), 0);
6947    }
6948
6949    #[test]
6950    fn proto_and_lclosure_accounting_refunds_on_sweep() {
6951        let mut state = new_state().expect("state should initialize");
6952        let _heap_guard = {
6953            let g = state.global();
6954            lua_gc::HeapGuard::push(&g.heap)
6955        };
6956
6957        let mut proto = LuaProto::placeholder();
6958        proto.code = vec![lua_types::opcode::Instruction(0); 2048];
6959        proto.lineinfo = vec![0; 2048];
6960        proto.k = vec![LuaValue::Int(1); 512];
6961        let expected_proto_payload = proto.buffer_bytes();
6962        let proto = GcRef::new(proto);
6963        proto.account_buffer(expected_proto_payload as isize);
6964
6965        let closure = state.new_lclosure(proto, 16);
6966        let expected_closure_payload = closure.buffer_bytes();
6967        let key = state.external_root_value(LuaValue::Function(LuaClosure::Lua(closure)));
6968        let allocated_bytes = state.global().heap.bytes_used();
6969        assert!(
6970            allocated_bytes >= expected_proto_payload + expected_closure_payload,
6971            "proto and Lua closure vector bytes must be charged to the GC heap"
6972        );
6973
6974        state.gc().full_collect();
6975        assert_eq!(
6976            state.global().heap.bytes_used(),
6977            allocated_bytes,
6978            "rooted proto and Lua closure payload bytes should remain charged after collection"
6979        );
6980
6981        assert!(state.external_unroot_value(key).is_some());
6982        state.gc().full_collect();
6983        assert_eq!(state.global().heap.bytes_used(), 0);
6984        assert_eq!(state.global().heap.allgc_count(), 0);
6985    }
6986
6987    #[test]
6988    fn string_buffer_accounting_refunds_on_sweep() {
6989        let mut state = new_state().expect("state should initialize");
6990        let _heap_guard = {
6991            let g = state.global();
6992            lua_gc::HeapGuard::push(&g.heap)
6993        };
6994
6995        let payload = vec![b'x'; crate::string::MAX_SHORT_LEN + 4096];
6996        let string = state
6997            .intern_str(&payload)
6998            .expect("long string should allocate");
6999        let key = state.external_root_value(LuaValue::Str(string));
7000        let allocated_bytes = state.global().heap.bytes_used();
7001        assert!(
7002            allocated_bytes > payload.len(),
7003            "long string backing bytes must be charged to the GC heap"
7004        );
7005
7006        state.gc().full_collect();
7007        assert_eq!(
7008            state.global().heap.bytes_used(),
7009            allocated_bytes,
7010            "rooted string buffer bytes should remain charged after collection"
7011        );
7012
7013        assert!(state.external_unroot_value(key).is_some());
7014        state.gc().full_collect();
7015        assert_eq!(state.global().heap.bytes_used(), 0);
7016        assert_eq!(state.global().heap.allgc_count(), 0);
7017    }
7018
7019    #[test]
7020    fn interned_short_string_cache_does_not_root_unreferenced_string() {
7021        let mut state = new_state().expect("state should initialize");
7022        let _heap_guard = {
7023            let g = state.global();
7024            lua_gc::HeapGuard::push(&g.heap)
7025        };
7026
7027        let payload = b"weak-cache-probe-a";
7028        let string = state
7029            .intern_str(payload)
7030            .expect("short string should intern");
7031        let id = string.identity();
7032        assert!(state.global().interned_lt.contains_key(&payload[..]));
7033        assert_eq!(
7034            state.global().heap.register_allocation_token(id),
7035            state.global().heap.register_allocation_token(id),
7036            "token registration is get-or-insert while the string is provably live"
7037        );
7038        assert!(state.global().heap.allocation_token(id).is_some());
7039
7040        state.gc().full_collect();
7041        assert!(!state.global().interned_lt.contains_key(&payload[..]));
7042        assert_eq!(state.global().heap.allocation_token(id), None);
7043    }
7044
7045    #[test]
7046    fn interned_short_string_cache_keeps_reachable_string_until_unrooted() {
7047        let mut state = new_state().expect("state should initialize");
7048        let _heap_guard = {
7049            let g = state.global();
7050            lua_gc::HeapGuard::push(&g.heap)
7051        };
7052
7053        let payload = b"weak-cache-probe-b";
7054        let string = state
7055            .intern_str(payload)
7056            .expect("short string should intern");
7057        let id = string.identity();
7058        state.global().heap.register_allocation_token(id);
7059        let key = state.external_root_value(LuaValue::Str(string));
7060
7061        state.gc().full_collect();
7062        assert!(state.global().interned_lt.contains_key(&payload[..]));
7063        assert!(state.global().heap.allocation_token(id).is_some());
7064
7065        assert!(state.external_unroot_value(key).is_some());
7066        state.gc().full_collect();
7067        assert!(!state.global().interned_lt.contains_key(&payload[..]));
7068        assert_eq!(state.global().heap.allocation_token(id), None);
7069    }
7070
7071    #[test]
7072    fn gc_phase_predicates_follow_heap_state() {
7073        let mut state = new_state().expect("state should initialize");
7074        let _heap_guard = {
7075            let g = state.global();
7076            lua_gc::HeapGuard::push(&g.heap)
7077        };
7078
7079        {
7080            let mut g = state.global_mut();
7081            g.gckind = GcKind::Incremental as u8;
7082            g.lastatomic = 0;
7083            assert!(!g.is_gen_mode());
7084            g.lastatomic = 1;
7085            assert!(g.is_gen_mode());
7086            g.lastatomic = 0;
7087        }
7088
7089        let mut roots = Vec::new();
7090        for _ in 0..16 {
7091            let table = state.new_table();
7092            roots.push(state.external_root_value(LuaValue::Table(table)));
7093        }
7094
7095        let mut saw_keep = false;
7096        let mut saw_sweep = false;
7097        for _ in 0..128 {
7098            state.gc().incremental_step(1);
7099            let g = state.global();
7100            let heap_state = g.heap.gc_state();
7101            assert_eq!(g.keep_invariant(), heap_state.is_invariant());
7102            assert_eq!(g.is_sweep_phase(), heap_state.is_sweep());
7103            saw_keep |= g.keep_invariant();
7104            saw_sweep |= g.is_sweep_phase();
7105            if heap_state.is_pause() && saw_keep && saw_sweep {
7106                break;
7107            }
7108        }
7109
7110        assert!(
7111            saw_keep,
7112            "incremental cycle should expose an invariant phase"
7113        );
7114        assert!(saw_sweep, "incremental cycle should expose a sweep phase");
7115
7116        for key in roots {
7117            assert!(state.external_unroot_value(key).is_some());
7118        }
7119        state.gc().full_collect();
7120    }
7121
7122    #[test]
7123    fn gc_barrier_keeps_new_child_stored_in_black_parent() {
7124        let mut state = new_state().expect("state should initialize");
7125        let _heap_guard = {
7126            let g = state.global();
7127            lua_gc::HeapGuard::push(&g.heap)
7128        };
7129
7130        let parent = state.new_table();
7131        let parent_key = state.external_root_value(LuaValue::Table(parent));
7132        state.gc().incremental_step(1);
7133        assert!(
7134            state.global().keep_invariant(),
7135            "test setup should leave the parent marked during an active cycle"
7136        );
7137
7138        let child = state.new_table();
7139        let parent_value = LuaValue::Table(parent);
7140        let child_value = LuaValue::Table(child);
7141        parent
7142            .raw_set_int(&mut state, 1, child_value)
7143            .expect("table store should succeed");
7144        state.gc_barrier_back(&parent_value, &child_value);
7145
7146        for _ in 0..128 {
7147            if state.gc().incremental_step(1) {
7148                break;
7149            }
7150        }
7151
7152        assert_eq!(state.global().heap.allgc_count(), 2);
7153        assert_eq!(
7154            parent.get_int(1).as_table().map(|t| t.identity()),
7155            Some(child.identity())
7156        );
7157
7158        assert!(state.external_unroot_value(parent_key).is_some());
7159        state.gc().full_collect();
7160        assert_eq!(state.global().heap.allgc_count(), 0);
7161    }
7162
7163    #[test]
7164    fn generational_mode_promotes_and_barriers_age_objects() {
7165        let mut state = new_state().expect("state should initialize");
7166        let _heap_guard = {
7167            let g = state.global();
7168            lua_gc::HeapGuard::push(&g.heap)
7169        };
7170
7171        let parent = state.new_table();
7172        let parent_key = state.external_root_value(LuaValue::Table(parent));
7173
7174        state.gc().change_mode(GcKind::Generational);
7175        assert_eq!(parent.0.age(), lua_gc::GcAge::Old);
7176        assert_eq!(parent.0.color(), lua_gc::Color::Black);
7177        let majorbase = state.global().gc_estimate;
7178        assert!(majorbase > 0);
7179        assert!(state.global().gc_debt() <= 0);
7180
7181        let child = state.new_table();
7182        let parent_value = LuaValue::Table(parent);
7183        let child_value = LuaValue::Table(child);
7184        parent
7185            .raw_set_int(&mut state, 1, child_value.clone())
7186            .expect("table store should succeed");
7187        state.gc_barrier_back(&parent_value, &child_value);
7188        assert_eq!(parent.0.age(), lua_gc::GcAge::Touched1);
7189        assert_eq!(parent.0.color(), lua_gc::Color::Gray);
7190        assert_eq!(child.0.age(), lua_gc::GcAge::New);
7191
7192        let metatable = state.new_table();
7193        parent.set_metatable(Some(metatable));
7194        state.gc().obj_barrier(&parent, &metatable);
7195        assert_eq!(metatable.0.age(), lua_gc::GcAge::Old0);
7196
7197        assert!(state.gc().generational_step_minor_only());
7198        assert_eq!(parent.0.age(), lua_gc::GcAge::Touched2);
7199        assert_eq!(child.0.age(), lua_gc::GcAge::Survival);
7200        assert_eq!(metatable.0.age(), lua_gc::GcAge::Old1);
7201        assert_eq!(state.global().gc_estimate, majorbase);
7202        assert!(state.global().gc_debt() <= 0);
7203
7204        state.gc().change_mode(GcKind::Incremental);
7205        assert_eq!(parent.0.age(), lua_gc::GcAge::New);
7206        assert_eq!(child.0.age(), lua_gc::GcAge::New);
7207        assert_eq!(metatable.0.age(), lua_gc::GcAge::New);
7208
7209        assert!(state.external_unroot_value(parent_key).is_some());
7210        state.gc().full_collect();
7211    }
7212
7213    #[test]
7214    fn generational_upvalue_write_barrier_marks_young_child_old0() {
7215        let mut state = new_state().expect("state should initialize");
7216        let _heap_guard = {
7217            let g = state.global();
7218            lua_gc::HeapGuard::push(&g.heap)
7219        };
7220
7221        let proto = state.new_proto();
7222        let closure = state.new_lclosure(proto, 1);
7223        let closure_key = state.external_root_value(LuaValue::Function(LuaClosure::Lua(closure)));
7224        state.gc().change_mode(GcKind::Generational);
7225        let uv = closure.upval(0);
7226        assert_eq!(uv.0.age(), lua_gc::GcAge::Old);
7227
7228        let child = state.new_table();
7229        state
7230            .upvalue_set(&closure, 0, LuaValue::Table(child))
7231            .expect("closed upvalue write should succeed");
7232        assert_eq!(child.0.age(), lua_gc::GcAge::Old0);
7233
7234        assert!(state.external_unroot_value(closure_key).is_some());
7235        state.gc().full_collect();
7236    }
7237
7238    #[test]
7239    fn cclosure_setupvalue_replaces_upvalue() {
7240        let mut state = new_state().expect("state should initialize");
7241        let _heap_guard = {
7242            let g = state.global();
7243            lua_gc::HeapGuard::push(&g.heap)
7244        };
7245
7246        let first = state.new_table();
7247        state.push(LuaValue::Table(first));
7248        crate::api::push_cclosure(&mut state, test_noop_cclosure, 1)
7249            .expect("C closure creation should succeed");
7250        let LuaValue::Function(LuaClosure::C(ccl)) = state.get_at(state.top_idx() - 1) else {
7251            panic!("expected heavy C closure");
7252        };
7253
7254        let second = state.new_table();
7255        state.push(LuaValue::Table(second));
7256        let name =
7257            crate::api::setup_value(&mut state, -2, 1).expect("C closure upvalue should exist");
7258
7259        assert!(name.is_empty());
7260        let upvalues = ccl.upvalues.borrow();
7261        let LuaValue::Table(actual) = upvalues[0].clone() else {
7262            panic!("expected table upvalue");
7263        };
7264        assert_eq!(actual.identity(), second.identity());
7265    }
7266
7267    #[test]
7268    fn generational_cclosure_setupvalue_barrier_marks_young_child_old0() {
7269        let mut state = new_state().expect("state should initialize");
7270        let _heap_guard = {
7271            let g = state.global();
7272            lua_gc::HeapGuard::push(&g.heap)
7273        };
7274
7275        state.push(LuaValue::Nil);
7276        crate::api::push_cclosure(&mut state, test_noop_cclosure, 1)
7277            .expect("C closure creation should succeed");
7278        let LuaValue::Function(LuaClosure::C(ccl)) = state.get_at(state.top_idx() - 1) else {
7279            panic!("expected heavy C closure");
7280        };
7281        let closure_key = state.external_root_value(LuaValue::Function(LuaClosure::C(ccl)));
7282
7283        state.gc().change_mode(GcKind::Generational);
7284        assert_eq!(ccl.0.age(), lua_gc::GcAge::Old);
7285
7286        let child = state.new_table();
7287        state.push(LuaValue::Table(child));
7288        crate::api::setup_value(&mut state, -2, 1).expect("C closure upvalue should exist");
7289
7290        assert_eq!(child.0.age(), lua_gc::GcAge::Old0);
7291
7292        assert!(state.external_unroot_value(closure_key).is_some());
7293        state.gc().full_collect();
7294    }
7295
7296    #[test]
7297    fn generational_closure_upvalue_slot_barrier_marks_new_upval_old0() {
7298        let mut state = new_state().expect("state should initialize");
7299        let _heap_guard = {
7300            let g = state.global();
7301            lua_gc::HeapGuard::push(&g.heap)
7302        };
7303
7304        let proto = state.new_proto();
7305        let closure = state.new_lclosure(proto, 1);
7306        let closure_key = state.external_root_value(LuaValue::Function(LuaClosure::Lua(closure)));
7307        state.gc().change_mode(GcKind::Generational);
7308        assert_eq!(closure.0.age(), lua_gc::GcAge::Old);
7309
7310        let replacement = state.new_upval_closed(LuaValue::Nil);
7311        closure.set_upval(0, replacement);
7312        state.gc().obj_barrier(&closure, &replacement);
7313        assert_eq!(replacement.0.age(), lua_gc::GcAge::Old0);
7314
7315        assert!(state.external_unroot_value(closure_key).is_some());
7316        state.gc().full_collect();
7317    }
7318
7319    #[test]
7320    fn cross_thread_upvalue_mirror_traces_values_as_roots() {
7321        let mut state = new_state().expect("state should initialize");
7322        let _heap_guard = {
7323            let g = state.global();
7324            lua_gc::HeapGuard::push(&g.heap)
7325        };
7326
7327        let mirrored = state.new_table();
7328        state
7329            .global_mut()
7330            .cross_thread_upvals
7331            .insert((999, StackIdx(0)), LuaValue::Table(mirrored));
7332
7333        state.gc().full_collect();
7334        assert_eq!(state.global().heap.allgc_count(), 1);
7335
7336        state.global_mut().cross_thread_upvals.clear();
7337        state.gc().full_collect();
7338        assert_eq!(state.global().heap.allgc_count(), 0);
7339    }
7340
7341    #[test]
7342    fn generational_full_collect_promotes_new_survivors_to_old() {
7343        let mut state = new_state().expect("state should initialize");
7344        let _heap_guard = {
7345            let g = state.global();
7346            lua_gc::HeapGuard::push(&g.heap)
7347        };
7348
7349        state.gc().change_mode(GcKind::Generational);
7350        let table = state.new_table();
7351        let table_key = state.external_root_value(LuaValue::Table(table));
7352        assert_eq!(table.0.age(), lua_gc::GcAge::New);
7353
7354        state.gc().full_collect();
7355        assert_eq!(table.0.age(), lua_gc::GcAge::Old);
7356        assert_eq!(table.0.color(), lua_gc::Color::Black);
7357
7358        assert!(state.external_unroot_value(table_key).is_some());
7359        state.gc().full_collect();
7360    }
7361
7362    #[test]
7363    fn gc_packed_params_return_user_visible_values() {
7364        let mut state = new_state().expect("state should initialize");
7365        assert_eq!(
7366            crate::api::gc(&mut state, crate::api::GcArgs::SetPause { value: 200 }),
7367            200
7368        );
7369        assert_eq!(state.global().gc_pause_param(), 200);
7370        assert_eq!(
7371            crate::api::gc(&mut state, crate::api::GcArgs::SetStepMul { value: 200 }),
7372            100
7373        );
7374        assert_eq!(state.global().gc_stepmul_param(), 200);
7375
7376        crate::api::gc(
7377            &mut state,
7378            crate::api::GcArgs::Gen {
7379                minormul: 0,
7380                majormul: 200,
7381            },
7382        );
7383        assert_eq!(state.global().gc_genmajormul_param(), 200);
7384    }
7385
7386    #[test]
7387    fn generational_step_runs_bad_major_when_growth_exceeds_genmajormul() {
7388        let mut state = new_state().expect("state should initialize");
7389        let _heap_guard = {
7390            let g = state.global();
7391            lua_gc::HeapGuard::push(&g.heap)
7392        };
7393
7394        let root = state.new_table();
7395        let root_key = state.external_root_value(LuaValue::Table(root));
7396        state.gc().change_mode(GcKind::Generational);
7397
7398        let root_value = LuaValue::Table(root);
7399        for i in 1..=64 {
7400            let child = state.new_table();
7401            let child_value = LuaValue::Table(child);
7402            root.raw_set_int(&mut state, i, child_value.clone())
7403                .expect("table store should succeed");
7404            state.gc_barrier_back(&root_value, &child_value);
7405        }
7406
7407        {
7408            let mut g = state.global_mut();
7409            g.gc_estimate = 1;
7410            set_debt(&mut *g, 1);
7411        }
7412
7413        assert!(state.gc().generational_step());
7414        let g = state.global();
7415        assert!(g.is_gen_mode());
7416        assert!(
7417            g.lastatomic > 0,
7418            "bad major collection should arm stepgenfull"
7419        );
7420        assert!(g.gc_estimate > 1);
7421        assert!(g.gc_debt() <= 0);
7422        assert_eq!(root.0.age(), lua_gc::GcAge::Old);
7423        drop(g);
7424
7425        assert!(state.external_unroot_value(root_key).is_some());
7426        state.gc().full_collect();
7427    }
7428
7429    #[test]
7430    fn generational_implicit_step_runs_major_when_heap_threshold_exceeded() {
7431        let mut state = new_state().expect("state should initialize");
7432        let _heap_guard = {
7433            let g = state.global();
7434            lua_gc::HeapGuard::push(&g.heap)
7435        };
7436
7437        let root = state.new_table();
7438        let root_key = state.external_root_value(LuaValue::Table(root));
7439        state.gc().change_mode(GcKind::Generational);
7440
7441        let root_value = LuaValue::Table(root);
7442        for i in 1..=64 {
7443            let child = state.new_table();
7444            let child_value = LuaValue::Table(child);
7445            root.raw_set_int(&mut state, i, child_value.clone())
7446                .expect("table store should succeed");
7447            state.gc_barrier_back(&root_value, &child_value);
7448        }
7449
7450        {
7451            let mut g = state.global_mut();
7452            g.gc_estimate = 1;
7453            set_debt(&mut *g, -1);
7454            g.heap.set_threshold_bytes(1);
7455        }
7456
7457        assert!(state.gc().generational_step());
7458        let g = state.global();
7459        assert!(g.is_gen_mode());
7460        assert!(
7461            g.lastatomic > 0,
7462            "implicit threshold-triggered growth should arm a bad major"
7463        );
7464        assert!(g.gc_debt() <= 0);
7465        drop(g);
7466
7467        assert!(state.external_unroot_value(root_key).is_some());
7468        state.gc().full_collect();
7469    }
7470
7471    #[test]
7472    fn generational_stepgenfull_returns_to_gen_after_good_collection() {
7473        let mut state = new_state().expect("state should initialize");
7474        let _heap_guard = {
7475            let g = state.global();
7476            lua_gc::HeapGuard::push(&g.heap)
7477        };
7478
7479        let root = state.new_table();
7480        let root_key = state.external_root_value(LuaValue::Table(root));
7481        state.gc().change_mode(GcKind::Generational);
7482        {
7483            let mut g = state.global_mut();
7484            g.lastatomic = 1024;
7485        }
7486
7487        assert!(state.gc().generational_step());
7488        let g = state.global();
7489        assert_eq!(g.gckind, GcKind::Generational as u8);
7490        assert_eq!(g.lastatomic, 0);
7491        assert!(g.gc_debt() <= 0);
7492        assert_eq!(root.0.age(), lua_gc::GcAge::Old);
7493        assert_eq!(root.0.color(), lua_gc::Color::Black);
7494        drop(g);
7495
7496        assert!(state.external_unroot_value(root_key).is_some());
7497        state.gc().full_collect();
7498    }
7499
7500    #[test]
7501    fn generational_step_zero_reports_false_without_positive_debt() {
7502        let mut state = new_state().expect("state should initialize");
7503        let _heap_guard = {
7504            let g = state.global();
7505            lua_gc::HeapGuard::push(&g.heap)
7506        };
7507
7508        state.gc().change_mode(GcKind::Generational);
7509        assert_eq!(
7510            crate::api::gc(&mut state, crate::api::GcArgs::Step { data: 0 }),
7511            0
7512        );
7513        assert_eq!(
7514            crate::api::gc(&mut state, crate::api::GcArgs::Step { data: 1 }),
7515            1
7516        );
7517    }
7518}
7519
7520// ──────────────────────────────────────────────────────────────────────────────
7521// PORT STATUS
7522//   source:        src/lstate.c  (445 lines, 25 functions)
7523//                  src/lstate.h  (408 lines; struct definitions merged)
7524//   target_crate:  lua-vm
7525//   confidence:    medium
7526//   todos:         44
7527//   port_notes:    34
7528//   unsafe_blocks: 0   (must be 0 outside explicit unsafe-budget crates)
7529//   t2c2:          CallInfoFrame flattened from a 2-variant enum to a
7530//                  branch-free struct (all fields always present, still 32 B,
7531//                  CallInfo still 72 B, zero new unsafe). The hook `trap` flag
7532//                  moved out of the Lua variant into callstatus bit CIST_TRAP
7533//                  (bit 14). Variant-guarded accessors became plain field
7534//                  reads/writes with debug_assert! frame-kind tripwires.
7535//   notes:         Logic faithfully follows lstate.c. Key structural changes:
7536//                  (1) LX/LG C layout wrappers dropped; GlobalState is Rc<RefCell<>>.
7537//                  (2) CallInfo linked list → Vec<CallInfo> with CallInfoIdx indices;
7538//                      shrink_ci uses truncation rather than node-by-node removal.
7539//                  (3) lua_State.twups self-reference → membership in GlobalState.twups Vec.
7540//                  (4) errorJmp/setjmp → removed; errors use Result<T, LuaError>.
7541//                  (5) Custom allocator (lua_Alloc) → dropped; Rust's allocator handles it.
7542//                  (6) make_seed: ASLR pointer entropy requires unsafe; time-only for Phase A.
7543//                  (7) Perf: LuaState.cached_thread_id stores the thread's own id once at
7544//                      construction; upvalue_get/_set compare against this u64 field
7545//                      instead of borrowing global.current_thread_id on every read.
7546//                      Invariant survives coroutine resume because each thread caches its
7547//                      OWN id, not the global's id (see field doc on cached_thread_id).
7548//                  (8) Perf: LuaTableRefExt::{raw_set, raw_set_int, get, get_int,
7549//                      get_short_str, metatable, as_ptr} and table_{raw,set_with_tm,
7550//                      array_set} carry #[inline] so the per-set dispatch chain
7551//                      collapses into set_i_value / vm.rs OP_SETI callers. The
7552//                      historical reject_invalid_table_key precheck moved into
7553//                      LuaTable::try_raw_set (lua-types) and was dropped at this
7554//                      layer; raw_set now takes the key by value, eliminating a
7555//                      LuaValue clone per set. gc_barrier_back is invoked
7556//                      before the store in table_set_with_tm (semantically
7557//                      equivalent: the barrier only inspects the value's color,
7558//                      not its location), letting v be moved directly into
7559//                      table_raw_set without an intermediate clone.
7560//                  Key TODOs: luaT_init and luaX_init cross-crate calls (Phase B);
7561//                  init_registry table mutations through Rc (needs RefCell<LuaTable>);
7562//                  luaD_closeprotected/seterrorobj/reallocstack in reset_thread (ldo.c);
7563//                  GcRef<LuaState> self-reference for mainthread (Phase D);
7564//                  LuaString::placeholder() helper needed for GlobalState init;
7565//                  LuaValue and LuaTable should move to object.rs once that lands.
7566// ──────────────────────────────────────────────────────────────────────────────