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