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