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    if live_ids.len() == global.interned_lt.len() {
3832        return;
3833    }
3834    live_ids.sort_unstable();
3835    live_ids.dedup();
3836    global
3837        .interned_lt
3838        .retain(|_, s| live_ids.binary_search(&s.identity()).is_ok());
3839}
3840
3841impl<'a> GcHandle<'a> {
3842    /// macros.tsv: `luaC_checkGC → state.gc().check_step()`
3843    ///
3844    /// Phase D-2: drives implicit collection when the heap's byte threshold
3845    /// is exceeded. Without this hook, loops that allocate without an
3846    /// explicit `collectgarbage()` call (e.g. `closure.lua`'s
3847    /// `while x[1] do local a = A..A end` GC-driven loop) never settle.
3848    pub fn check_step(&self) {
3849        if !self._state.global().is_gc_running() {
3850            return;
3851        }
3852        if self._state.global().is_gen_mode() {
3853            let should_collect = {
3854                let g = self._state.global();
3855                g.heap.would_collect() || g.gc_debt() > 0
3856            };
3857            if should_collect {
3858                self.generational_step();
3859            }
3860        } else {
3861            self.collect_via_heap(/* force = */ false);
3862        }
3863    }
3864
3865    /// macros.tsv: `luaC_fullgc → state.gc().full_collect()`
3866    pub fn full_collect(&self) {
3867        if self._state.global().is_gen_mode() {
3868            self.fullgen();
3869        } else {
3870            self.collect_via_heap(/* force = */ true);
3871        }
3872    }
3873
3874    fn negative_debt(bytes: usize) -> isize {
3875        -(bytes.min(isize::MAX as usize) as isize)
3876    }
3877
3878    fn set_minor_debt(&self) {
3879        let mut g = self._state.global_mut();
3880        let total = g.total_bytes();
3881        let growth = (total / 100).saturating_mul(g.genminormul as usize);
3882        g.heap
3883            .set_threshold_bytes(total.saturating_add(growth.max(1)));
3884        set_debt(&mut *g, Self::negative_debt(growth));
3885    }
3886
3887    fn set_pause_debt(&self) {
3888        let mut g = self._state.global_mut();
3889        let total = g.total_bytes();
3890        let pause = g.gc_pause_param().max(0) as usize;
3891        let threshold = g.gc_estimate.max(1).saturating_mul(pause) / 100;
3892        let debt = if threshold > total {
3893            Self::negative_debt(threshold - total)
3894        } else {
3895            0
3896        };
3897        let heap_threshold = if threshold > total {
3898            threshold
3899        } else {
3900            total.saturating_add(1)
3901        };
3902        g.heap.set_threshold_bytes(heap_threshold);
3903        set_debt(&mut *g, debt);
3904    }
3905
3906    fn enter_incremental_mode(&self) {
3907        let mut g = self._state.global_mut();
3908        g.heap.reset_all_ages();
3909        g.finalizers.reset_generation_boundaries();
3910        g.gckind = GcKind::Incremental as u8;
3911        g.lastatomic = 0;
3912    }
3913
3914    fn enter_generational_mode(&self) -> usize {
3915        self.collect_via_heap_mode(HeapCollectMode::Full);
3916        let numobjs = {
3917            let mut g = self._state.global_mut();
3918            g.heap.promote_all_to_old();
3919            g.finalizers.promote_all_pending_to_old();
3920            g.heap.allgc_count()
3921        };
3922        let total = self._state.global().total_bytes();
3923        {
3924            let mut g = self._state.global_mut();
3925            g.gckind = GcKind::Generational as u8;
3926            g.lastatomic = 0;
3927            g.gc_estimate = total;
3928        }
3929        self.set_minor_debt();
3930        numobjs
3931    }
3932
3933    fn fullgen(&self) -> usize {
3934        self.enter_incremental_mode();
3935        self.enter_generational_mode()
3936    }
3937
3938    fn stepgenfull(&self, lastatomic: usize) {
3939        if self._state.global().gckind == GcKind::Generational as u8 {
3940            self.enter_incremental_mode();
3941        }
3942        self.collect_via_heap_mode(HeapCollectMode::Full);
3943        let newatomic = self._state.global().heap.allgc_count().max(1);
3944        if newatomic < lastatomic.saturating_add(lastatomic >> 3) {
3945            {
3946                let mut g = self._state.global_mut();
3947                g.heap.promote_all_to_old();
3948                g.finalizers.promote_all_pending_to_old();
3949            }
3950            let total = self._state.global().total_bytes();
3951            {
3952                let mut g = self._state.global_mut();
3953                g.gckind = GcKind::Generational as u8;
3954                g.lastatomic = 0;
3955                g.gc_estimate = total;
3956            }
3957            self.set_minor_debt();
3958        } else {
3959            {
3960                let mut g = self._state.global_mut();
3961                g.heap.reset_all_ages();
3962                g.finalizers.reset_generation_boundaries();
3963            }
3964            let total = self._state.global().total_bytes();
3965            {
3966                let mut g = self._state.global_mut();
3967                g.gckind = GcKind::Incremental as u8;
3968                g.lastatomic = newatomic;
3969                g.gc_estimate = total;
3970            }
3971            self.set_pause_debt();
3972        }
3973    }
3974
3975    /// Shared driver behind both `full_collect` (force-collect) and
3976    /// `check_step` (collect only if heap byte threshold exceeded).
3977    ///
3978    /// Snapshots the weak-tables registry, invokes the heap's collect path
3979    /// with a post-mark weak-prune hook, and rebuilds the registry by
3980    /// retaining only entries whose target was reachable. The same hook
3981    /// works for both modes — the heap short-circuits when force=false and
3982    /// the threshold isn't met.
3983    fn collect_via_heap(&self, force: bool) {
3984        self.collect_via_heap_mode(if force {
3985            HeapCollectMode::Full
3986        } else {
3987            HeapCollectMode::Step
3988        });
3989    }
3990
3991    fn collect_via_heap_mode(&self, mode: HeapCollectMode) {
3992        use lua_gc::Trace;
3993        let state_ref: &LuaState = &*self._state;
3994
3995        // Fast path: when the caller did not force a collection, skip all
3996        // the snapshot work (3 Vec allocations + 3 HashSet allocations) if
3997        // the heap is paused or under threshold — a `step()` in that state
3998        // is a no-op, so the snapshot would be pure waste. Called millions
3999        // of times per recursive workload via `gc_check_step` in `precall`.
4000        if matches!(mode, HeapCollectMode::Step) {
4001            let g = state_ref.global.borrow();
4002            if !g.heap.would_collect() {
4003                return;
4004            }
4005        }
4006
4007        // Snapshot weak tables BEFORE the collect. `identity()` reads only
4008        // the pointer address — safe even on still-dangling weak handles —
4009        // and dedup by identity keeps the iteration linear.
4010        let weak_tables_snapshot: lua_gc::WeakRegistrySnapshot<GcRef<LuaTable>> = {
4011            let mut g = state_ref.global.borrow_mut();
4012            g.weak_tables_registry.live_snapshot_by_kind()
4013        };
4014
4015        // Snapshot pending finalizers. `GlobalState::trace` deliberately
4016        // does NOT root these — that's how the post-mark hook below can
4017        // distinguish "still reachable from program state" from "only kept
4018        // alive by the finalizer registry."
4019        let weak_table_capacity = weak_tables_snapshot.len();
4020        let (pending_snapshot, thread_capacity, interned_capacity): (
4021            Vec<FinalizerObject>,
4022            usize,
4023            usize,
4024        ) = {
4025            let g = state_ref.global.borrow();
4026            let pending = match mode {
4027                HeapCollectMode::Minor => g.finalizers.pending_minor_snapshot(),
4028                HeapCollectMode::Full | HeapCollectMode::Step => g.finalizers.pending_snapshot(),
4029            };
4030            (pending, g.threads.len(), g.interned_lt.len())
4031        };
4032        let finalizer_capacity = pending_snapshot.len();
4033
4034        let alive_ids: std::cell::RefCell<std::collections::HashSet<usize>> =
4035            std::cell::RefCell::new(std::collections::HashSet::new());
4036        let newly_unreachable: std::cell::RefCell<Vec<FinalizerObject>> =
4037            std::cell::RefCell::new(Vec::new());
4038        let finalizing_ids: std::cell::RefCell<std::collections::HashSet<usize>> =
4039            std::cell::RefCell::new(std::collections::HashSet::new());
4040        let alive_thread_ids: std::cell::RefCell<std::collections::HashSet<u64>> =
4041            std::cell::RefCell::new(std::collections::HashSet::new());
4042        let live_interned_ids: std::cell::RefCell<Vec<usize>> =
4043            std::cell::RefCell::new(Vec::new());
4044        let collect_ran = std::cell::Cell::new(false);
4045
4046        {
4047            let global = state_ref.global.borrow();
4048            global.heap.unpause();
4049            let roots = CollectRoots { global: &*global, thread: state_ref };
4050            let hook = |marker: &mut lua_gc::Marker| {
4051                collect_ran.set(true);
4052                alive_ids.borrow_mut().reserve(weak_table_capacity);
4053                newly_unreachable.borrow_mut().reserve(finalizer_capacity);
4054                finalizing_ids.borrow_mut().reserve(finalizer_capacity);
4055                alive_thread_ids.borrow_mut().reserve(thread_capacity);
4056                live_interned_ids.borrow_mut().reserve(interned_capacity);
4057                trace_reachable_threads(&*global, global.current_thread_id, marker);
4058                close_open_upvalues_for_unreachable_threads(&*global, marker);
4059                loop {
4060                    let visited_before = marker.visited_count();
4061                    for t in &weak_tables_snapshot.ephemeron {
4062                        if !marker.is_marked_or_old(t.0) {
4063                            continue;
4064                        }
4065                        let to_mark = t.ephemeron_values_to_mark_with_value(
4066                            &|v| lua_value_marked_or_old(marker, v),
4067                        );
4068                        for v in &to_mark {
4069                            v.trace(marker);
4070                        }
4071                    }
4072                    marker.drain_gray_queue();
4073                    if marker.visited_count() == visited_before {
4074                        break;
4075                    }
4076                }
4077                for pf in &pending_snapshot {
4078                    if !finalizer_marked_or_old(marker, pf) {
4079                        pf.mark(marker);
4080                        finalizing_ids.borrow_mut().insert(pf.identity());
4081                        newly_unreachable.borrow_mut().push(pf.clone());
4082                    }
4083                }
4084                marker.drain_gray_queue();
4085                loop {
4086                    let visited_before = marker.visited_count();
4087                    for t in &weak_tables_snapshot.ephemeron {
4088                        if !marker.is_marked_or_old(t.0) {
4089                            continue;
4090                        }
4091                        let to_mark = t.ephemeron_values_to_mark_with_value(
4092                            &|v| lua_value_marked_or_old(marker, v),
4093                        );
4094                        for v in &to_mark {
4095                            v.trace(marker);
4096                        }
4097                    }
4098                    marker.drain_gray_queue();
4099                    if marker.visited_count() == visited_before {
4100                        break;
4101                    }
4102                }
4103                for t in weak_snapshot_tables(&weak_tables_snapshot) {
4104                    let id = t.identity();
4105                    if marker.is_marked_or_old(t.0) {
4106                        let to_mark = {
4107                            let finalizing = finalizing_ids.borrow();
4108                            t.prune_weak_dead_with_value(
4109                                &|v| lua_value_marked_or_old(marker, v),
4110                                &|v| {
4111                                    lua_value_marked_or_old(marker, v)
4112                                        && lua_value_identity(v)
4113                                            .map_or(true, |id| !finalizing.contains(&id))
4114                                },
4115                            )
4116                        };
4117                        for v in &to_mark {
4118                            v.trace(marker);
4119                        }
4120                        alive_ids.borrow_mut().insert(id);
4121                    }
4122                }
4123                marker.drain_gray_queue();
4124                {
4125                    let mut alive = alive_thread_ids.borrow_mut();
4126                    for (id, entry) in global.threads.iter() {
4127                        if thread_entry_marked_alive(marker, *id, entry) {
4128                            alive.insert(*id);
4129                        }
4130                    }
4131                }
4132                record_live_interned_strings(&*global, marker, &live_interned_ids);
4133            };
4134            match mode {
4135                HeapCollectMode::Full => global.heap.full_collect_with_post_mark(&roots, hook),
4136                HeapCollectMode::Step => global.heap.step_with_post_mark(&roots, hook),
4137                HeapCollectMode::Minor => global.heap.minor_collect_with_post_mark(&roots, hook),
4138            }
4139        }
4140
4141        if !collect_ran.get() {
4142            return;
4143        }
4144
4145        // After collect, drop weak-table-registry entries whose target was
4146        // swept. This keeps the registry bounded and avoids retaining weak
4147        // handles whose target can no longer upgrade.
4148        let alive_set = alive_ids.into_inner();
4149        let promote: Vec<FinalizerObject> = newly_unreachable.into_inner();
4150        let alive_thread_ids = alive_thread_ids.into_inner();
4151        let live_interned_ids = live_interned_ids.into_inner();
4152        let mut g = state_ref.global.borrow_mut();
4153        retain_live_interned_strings(&mut *g, live_interned_ids);
4154        g.weak_tables_registry.retain_identities(&alive_set);
4155        let main_thread_id = g.main_thread_id;
4156        g.threads.retain(|id, _| alive_thread_ids.contains(id));
4157        g.cross_thread_upvals
4158            .retain(|(id, _), _| *id == main_thread_id || alive_thread_ids.contains(id));
4159        // Move newly-unreachable finalizables from `pending_finalizers` to
4160        // `to_be_finalized`. The latter is rooted by `GlobalState::trace`,
4161        // so these tables remain alive until their `__gc` runs.
4162        let promoted = g.finalizers.promote_pending_to_finalized(promote);
4163        for object in &promoted {
4164            if let Some(ptr) = object.heap_ptr() {
4165                g.heap.move_finobj_to_tobefnz(ptr);
4166            }
4167        }
4168        if matches!(mode, HeapCollectMode::Minor) {
4169            g.finalizers.finish_minor_collection();
4170        }
4171    }
4172
4173    /// Run one generational collection step.
4174    pub fn generational_step(&self) -> bool {
4175        self.generational_step_with_major(true)
4176    }
4177
4178    /// Run a generational step forced to the regular minor path.
4179    ///
4180    /// Used for `collectgarbage("step", 0)`: upstream `genstep` treats
4181    /// `GCdebt <= 0` as an explicit zero-size step and performs a minor
4182    /// collection, unless a previous bad major has already armed `lastatomic`.
4183    pub fn generational_step_minor_only(&self) -> bool {
4184        self.generational_step_with_major(false)
4185    }
4186
4187    fn generational_step_with_major(&self, allow_major: bool) -> bool {
4188        let (lastatomic, majorbase, majorinc, should_major) = {
4189            let g = self._state.global();
4190            let majorbase = if g.gc_estimate == 0 {
4191                g.total_bytes()
4192            } else {
4193                g.gc_estimate
4194            };
4195            let majormul = g.gc_genmajormul_param().max(0) as usize;
4196            let majorinc = (majorbase / 100).saturating_mul(majormul);
4197            let debt_due = g.gc_debt() > 0 || g.heap.would_collect();
4198            let should_major = allow_major
4199                && debt_due
4200                && g.total_bytes() > majorbase.saturating_add(majorinc);
4201            (g.lastatomic, majorbase, majorinc, should_major)
4202        };
4203
4204        if lastatomic != 0 {
4205            self.stepgenfull(lastatomic);
4206            debug_assert!(self._state.global().is_gen_mode());
4207            return true;
4208        }
4209
4210        if should_major {
4211            let numobjs = self.fullgen();
4212            let after = self._state.global().total_bytes();
4213            if after < majorbase.saturating_add(majorinc / 2) {
4214                self.set_minor_debt();
4215            } else {
4216                {
4217                    let mut g = self._state.global_mut();
4218                    g.lastatomic = numobjs.max(1);
4219                }
4220                self.set_pause_debt();
4221            }
4222        } else {
4223            self.collect_via_heap_mode(HeapCollectMode::Minor);
4224            self.set_minor_debt();
4225            self._state.global_mut().gc_estimate = majorbase;
4226        }
4227
4228        debug_assert!(self._state.global().is_gen_mode());
4229        true
4230    }
4231
4232    /// Phase-B stub for `luaC_step(L)`.
4233    pub fn step(&self) { /* phase-b no-op */ }
4234
4235    /// Run one budgeted incremental step of the GC.
4236    ///
4237    /// `work_units` is the number of GC work units the step is allowed to
4238    /// perform (one gray trace, one sweep visit, or one phase transition).
4239    /// Returns `true` if the step completed a cycle and the collector is
4240    /// now in the `Pause` state; `false` otherwise.
4241    ///
4242    /// Mirrors `collect_via_heap` for the post-mark weak-table /
4243    /// finalizer-promotion logic, but only the atomic-phase transition will
4244    /// invoke the snapshot-walking hook — propagate and sweep steps reuse
4245    /// the snapshot but never execute it. The snapshot is rebuilt on every
4246    /// call; the cost is `O(weak_tables_registry)` per step.
4247    pub fn incremental_step(&self, work_units: isize) -> bool {
4248        self.incremental_step_to_state(work_units, None)
4249    }
4250
4251    /// TestC/debug helper: run the incremental collector until a specific heap
4252    /// state is entered, preserving the same weak-table/finalizer post-mark
4253    /// hooks as [`Self::incremental_step`]. This is intentionally not used for
4254    /// normal pacing; it exists so official tests can inspect mid-cycle colors.
4255    pub fn run_until_gc_state_for_test(&self, target: lua_gc::GcState) -> bool {
4256        self.incremental_step_to_state(isize::MAX / 4, Some(target));
4257        self._state.global().heap.gc_state() == target
4258    }
4259
4260    fn incremental_step_to_state(
4261        &self,
4262        work_units: isize,
4263        target: Option<lua_gc::GcState>,
4264    ) -> bool {
4265        use lua_gc::{StepBudget, StepOutcome, Trace};
4266        let state_ref: &LuaState = &*self._state;
4267
4268        let weak_tables_snapshot: lua_gc::WeakRegistrySnapshot<GcRef<LuaTable>> = {
4269            let mut g = state_ref.global.borrow_mut();
4270            g.weak_tables_registry.live_snapshot_by_kind()
4271        };
4272
4273        let weak_table_capacity = weak_tables_snapshot.len();
4274        let (pending_snapshot, thread_capacity, interned_capacity): (
4275            Vec<FinalizerObject>,
4276            usize,
4277            usize,
4278        ) = {
4279            let g = state_ref.global.borrow();
4280            (g.finalizers.pending_snapshot(), g.threads.len(), g.interned_lt.len())
4281        };
4282        let finalizer_capacity = pending_snapshot.len();
4283
4284        let alive_ids: std::cell::RefCell<std::collections::HashSet<usize>> =
4285            std::cell::RefCell::new(std::collections::HashSet::new());
4286        let newly_unreachable: std::cell::RefCell<Vec<FinalizerObject>> =
4287            std::cell::RefCell::new(Vec::new());
4288        let finalizing_ids: std::cell::RefCell<std::collections::HashSet<usize>> =
4289            std::cell::RefCell::new(std::collections::HashSet::new());
4290        let alive_thread_ids: std::cell::RefCell<std::collections::HashSet<u64>> =
4291            std::cell::RefCell::new(std::collections::HashSet::new());
4292        let live_interned_ids: std::cell::RefCell<Vec<usize>> =
4293            std::cell::RefCell::new(Vec::new());
4294        let atomic_ran = std::cell::Cell::new(false);
4295
4296        let stop_target = {
4297            let g = state_ref.global.borrow();
4298            match (target, g.heap.gc_state()) {
4299                (Some(target), _) => Some(target),
4300                (None, lua_gc::GcState::CallFin) => None,
4301                (None, _) => Some(lua_gc::GcState::CallFin),
4302            }
4303        };
4304
4305        let outcome = {
4306            let global = state_ref.global.borrow();
4307            global.heap.unpause();
4308            let roots = CollectRoots { global: &*global, thread: state_ref };
4309            let hook = |marker: &mut lua_gc::Marker| {
4310                atomic_ran.set(true);
4311                alive_ids.borrow_mut().reserve(weak_table_capacity);
4312                newly_unreachable.borrow_mut().reserve(finalizer_capacity);
4313                finalizing_ids.borrow_mut().reserve(finalizer_capacity);
4314                alive_thread_ids.borrow_mut().reserve(thread_capacity);
4315                live_interned_ids.borrow_mut().reserve(interned_capacity);
4316                trace_reachable_threads(&*global, global.current_thread_id, marker);
4317                close_open_upvalues_for_unreachable_threads(&*global, marker);
4318                loop {
4319                    let visited_before = marker.visited_count();
4320                    for t in &weak_tables_snapshot.ephemeron {
4321                        let t_id = t.identity();
4322                        if !marker.is_visited(t_id) {
4323                            continue;
4324                        }
4325                        let to_mark = t.ephemeron_values_to_mark(
4326                            &|id| marker.is_visited(id),
4327                        );
4328                        for v in &to_mark {
4329                            v.trace(marker);
4330                        }
4331                    }
4332                    marker.drain_gray_queue();
4333                    if marker.visited_count() == visited_before {
4334                        break;
4335                    }
4336                }
4337                for pf in &pending_snapshot {
4338                    if !marker.is_visited(pf.identity()) {
4339                        pf.mark(marker);
4340                        finalizing_ids.borrow_mut().insert(pf.identity());
4341                        newly_unreachable.borrow_mut().push(pf.clone());
4342                    }
4343                }
4344                marker.drain_gray_queue();
4345                loop {
4346                    let visited_before = marker.visited_count();
4347                    for t in &weak_tables_snapshot.ephemeron {
4348                        let t_id = t.identity();
4349                        if !marker.is_visited(t_id) {
4350                            continue;
4351                        }
4352                        let to_mark = t.ephemeron_values_to_mark(
4353                            &|id| marker.is_visited(id),
4354                        );
4355                        for v in &to_mark {
4356                            v.trace(marker);
4357                        }
4358                    }
4359                    marker.drain_gray_queue();
4360                    if marker.visited_count() == visited_before {
4361                        break;
4362                    }
4363                }
4364                for t in weak_snapshot_tables(&weak_tables_snapshot) {
4365                    let id = t.identity();
4366                    if marker.is_visited(id) {
4367                        let to_mark = {
4368                            let finalizing = finalizing_ids.borrow();
4369                            t.prune_weak_dead_with(
4370                                &|id| marker.is_visited(id),
4371                                &|id| marker.is_visited(id) && !finalizing.contains(&id),
4372                            )
4373                        };
4374                        for v in &to_mark {
4375                            v.trace(marker);
4376                        }
4377                        alive_ids.borrow_mut().insert(id);
4378                    }
4379                }
4380                marker.drain_gray_queue();
4381                {
4382                    let mut alive = alive_thread_ids.borrow_mut();
4383                    for (id, entry) in global.threads.iter() {
4384                        if thread_entry_marked_alive(marker, *id, entry) {
4385                            alive.insert(*id);
4386                        }
4387                    }
4388                }
4389                record_live_interned_strings(&*global, marker, &live_interned_ids);
4390            };
4391            let budget = StepBudget::from_work(work_units);
4392            if let Some(target) = stop_target {
4393                global.heap.incremental_run_until_state_with_post_mark(
4394                    &roots,
4395                    target,
4396                    work_units,
4397                    hook,
4398                )
4399            } else {
4400                global.heap.incremental_step_with_post_mark(&roots, budget, hook)
4401            }
4402        };
4403
4404        if atomic_ran.get() {
4405            let alive_set = alive_ids.into_inner();
4406            let promote: Vec<FinalizerObject> = newly_unreachable.into_inner();
4407            let alive_thread_ids = alive_thread_ids.into_inner();
4408            let live_interned_ids = live_interned_ids.into_inner();
4409            let mut g = state_ref.global.borrow_mut();
4410            retain_live_interned_strings(&mut *g, live_interned_ids);
4411            g.weak_tables_registry.retain_identities(&alive_set);
4412            let main_thread_id = g.main_thread_id;
4413            g.threads.retain(|id, _| alive_thread_ids.contains(id));
4414            g.cross_thread_upvals
4415                .retain(|(id, _), _| *id == main_thread_id || alive_thread_ids.contains(id));
4416            let promoted = g.finalizers.promote_pending_to_finalized(promote);
4417            for object in &promoted {
4418                if let Some(ptr) = object.heap_ptr() {
4419                    g.heap.move_finobj_to_tobefnz(ptr);
4420                }
4421            }
4422        }
4423
4424        let mut paused = matches!(outcome, StepOutcome::Paused);
4425        if target.is_none()
4426            && self._state.global().heap.gc_state() == lua_gc::GcState::CallFin
4427            && !self._state.global().finalizers.has_to_be_finalized()
4428        {
4429            paused = self._state.global().heap.finish_callfin_phase();
4430        }
4431
4432        paused
4433    }
4434
4435    /// Run only the weak-table atomic cleanup used by legacy generational
4436    /// callers that need mark/prune behavior without sweeping.
4437    ///
4438    /// Explicit generational steps now use [`Self::generational_step`], which
4439    /// performs a young sweep. This helper remains for call sites that only
4440    /// need the weak-table atomic pass.
4441    pub fn prune_weak_tables_mark_only(&self) {
4442        use lua_gc::Trace;
4443        let state_ref: &LuaState = &*self._state;
4444
4445        let weak_tables_snapshot: lua_gc::WeakRegistrySnapshot<GcRef<LuaTable>> = {
4446            let mut g = state_ref.global.borrow_mut();
4447            g.weak_tables_registry.live_snapshot_by_kind()
4448        };
4449        let interned_capacity = {
4450            let g = state_ref.global.borrow();
4451            g.interned_lt.len()
4452        };
4453
4454        let live_interned_ids: std::cell::RefCell<Vec<usize>> =
4455            std::cell::RefCell::new(Vec::new());
4456
4457        {
4458            let global = state_ref.global.borrow();
4459            global.heap.unpause();
4460            let roots = CollectRoots { global: &*global, thread: state_ref };
4461            let hook = |marker: &mut lua_gc::Marker| {
4462                live_interned_ids.borrow_mut().reserve(interned_capacity);
4463                trace_reachable_threads(&*global, global.current_thread_id, marker);
4464                loop {
4465                    let visited_before = marker.visited_count();
4466                    for t in &weak_tables_snapshot.ephemeron {
4467                        let t_id = t.identity();
4468                        if !marker.is_visited(t_id) {
4469                            continue;
4470                        }
4471                        let to_mark = t.ephemeron_values_to_mark(
4472                            &|id| marker.is_visited(id),
4473                        );
4474                        for v in &to_mark {
4475                            v.trace(marker);
4476                        }
4477                    }
4478                    marker.drain_gray_queue();
4479                    if marker.visited_count() == visited_before {
4480                        break;
4481                    }
4482                }
4483                for t in weak_snapshot_tables(&weak_tables_snapshot) {
4484                    if marker.is_visited(t.identity()) {
4485                        let to_mark = t.prune_weak_dead(&|id| marker.is_visited(id));
4486                        for v in &to_mark {
4487                            v.trace(marker);
4488                        }
4489                    }
4490                }
4491                marker.drain_gray_queue();
4492                record_live_interned_strings(&*global, marker, &live_interned_ids);
4493            };
4494            global.heap.mark_only_with_post_mark(&roots, hook);
4495        }
4496
4497        let live_interned_ids = live_interned_ids.into_inner();
4498        let mut g = state_ref.global.borrow_mut();
4499        retain_live_interned_strings(&mut *g, live_interned_ids);
4500    }
4501
4502    /// Set the GC kind (incremental/generational).
4503    pub fn change_mode(&self, mode: GcKind) {
4504        let old = self._state.global().gckind;
4505        if old == mode as u8 {
4506            self._state.global_mut().lastatomic = 0;
4507            return;
4508        }
4509        match mode {
4510            GcKind::Generational => {
4511                self.enter_generational_mode();
4512            }
4513            GcKind::Incremental => {
4514                self.enter_incremental_mode();
4515            }
4516        }
4517    }
4518
4519    /// Phase-B stub for `luaC_fix(L, o)` — pin an object so GC won't collect it.
4520    pub fn fix_object<T: lua_gc::Trace + 'static>(&self, _o: &GcRef<T>) { /* phase-b no-op */ }
4521
4522    /// Free all collectable objects (called during state teardown).
4523    ///
4524    /// PORT NOTE: In Phases A–C, Rc drop chains handle deallocation automatically.
4525    pub fn free_all_objects(&self) {
4526        // PORT NOTE: Phase A–C no-op; Rc::drop handles deallocation
4527    }
4528
4529    /// GC write barrier for a TValue.
4530    ///
4531    /// macros.tsv: `luaC_barrier → state.gc().barrier(p, v)`
4532    pub fn barrier(&self, p: &dyn std::any::Any, v: &LuaValue) {
4533        let g = self._state.global();
4534        barrier_any(&g.heap, p, v, g.is_gen_mode(), BarrierKind::Forward);
4535    }
4536
4537    /// Backward write barrier.
4538    ///
4539    /// macros.tsv: `luaC_barrierback → state.gc().barrier_back(p, v)`
4540    pub fn barrier_back(&self, p: &dyn std::any::Any, v: &LuaValue) {
4541        let g = self._state.global();
4542        barrier_any(&g.heap, p, v, g.is_gen_mode(), BarrierKind::Backward);
4543    }
4544
4545    /// Typed table backward barrier for table mutation hot paths.
4546    pub fn table_barrier_back(&self, p: &GcRef<LuaTable>, v: &LuaValue) {
4547        let g = self._state.global();
4548        barrier_lua_value(&g.heap, *p, v, g.is_gen_mode(), BarrierKind::Backward);
4549    }
4550
4551    /// Object write barrier.
4552    ///
4553    /// macros.tsv: `luaC_objbarrier → state.gc().obj_barrier(p, o)`
4554    pub fn obj_barrier(&self, p: &dyn std::any::Any, o: &dyn std::any::Any) {
4555        let g = self._state.global();
4556        barrier_any(&g.heap, p, o, g.is_gen_mode(), BarrierKind::Forward);
4557    }
4558
4559    /// Backward object write barrier.
4560    ///
4561    pub fn obj_barrier_back(&self, p: &dyn std::any::Any, o: &dyn std::any::Any) {
4562        let g = self._state.global();
4563        barrier_any(&g.heap, p, o, g.is_gen_mode(), BarrierKind::Backward);
4564    }
4565}
4566
4567// ─── Functions from lstate.c ──────────────────────────────────────────────────
4568
4569//
4570// PORT NOTE: `luai_makeseed` in C mixed ASLR entropy (pointer addresses of a
4571// heap var, stack var, and code symbol) with the current time via `luaS_hash`.
4572// In Rust, raw pointer addresses require `unsafe` which is forbidden outside
4573// lua-gc/lua-coro. Native builds use time-only entropy for now; bare WASM uses
4574// a fixed seed so state creation never touches a stubbed host clock.
4575fn make_seed() -> u32 {
4576    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
4577    {
4578        return crate::string::hash_bytes(b"lua-rs-wasm-seed", 0x9e37_79b9);
4579    }
4580
4581    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
4582    {
4583        use std::time::{SystemTime, UNIX_EPOCH};
4584        let t = SystemTime::now()
4585            .duration_since(UNIX_EPOCH)
4586            .map(|d| d.as_secs() as u32)
4587            .unwrap_or(0);
4588
4589        // TODO(port): mix in ASLR entropy (pointer to heap / stack / code).
4590        // Requires a short `unsafe` block to cast references to usize.
4591        // The entropy improvement is important for hash DoS resistance (CVE-class).
4592        // Phase B should add this via a platform-specific helper in lua-gc or via
4593        // the `getrandom` crate if it is added as a dependency.
4594
4595        // For Phase A, just hash the time bytes against itself.
4596        crate::string::hash_bytes(&t.to_le_bytes(), t)
4597    }
4598}
4599
4600/// Adjust the compatibility `GCdebt` value against the collector-owned live
4601/// byte count.
4602///
4603///
4604/// ```c
4605///
4606/// //   l_mem tb = gettotalbytes(g);
4607/// //   lua_assert(tb > 0);
4608/// //   if (debt < tb - MAX_LMEM)
4609/// //     debt = tb - MAX_LMEM;
4610/// //   g->GCdebt = debt;
4611/// // }
4612/// ```
4613pub(crate) fn set_debt(g: &mut GlobalState, mut debt: isize) {
4614    let tb = g.total_bytes() as isize;
4615    debug_assert!(tb > 0);
4616    // macros.tsv: MAX_LMEM → isize::MAX
4617    if debt < tb.saturating_sub(isize::MAX) {
4618        debt = tb - isize::MAX;
4619    }
4620    g.gc_debt = debt;
4621}
4622
4623/// Deprecated no-op that returns `LUAI_MAXCCALLS`.
4624///
4625///
4626/// ```c
4627///
4628/// //   UNUSED(L); UNUSED(limit);
4629/// //   return LUAI_MAXCCALLS;  /* warning?? */
4630/// // }
4631/// ```
4632pub fn set_c_stack_limit(_state: &mut LuaState, _limit: u32) -> i32 {
4633    let _ = (_state, _limit);
4634    LUAI_MAXCCALLS as i32
4635}
4636
4637/// Allocate a fresh `CallInfo` beyond the current frame and return its index.
4638///
4639///
4640/// ```c
4641///
4642/// //   CallInfo *ci;
4643/// //   lua_assert(L->ci->next == NULL);
4644/// //   ci = luaM_new(L, CallInfo);
4645/// //   L->ci->next = ci;
4646/// //   ci->previous = L->ci;
4647/// //   ci->next = NULL;
4648/// //   ci->u.l.trap = 0;
4649/// //   L->nci++;
4650/// //   return ci;
4651/// // }
4652/// ```
4653pub(crate) fn extend_ci(state: &mut LuaState) -> CallInfoIdx {
4654    debug_assert!(
4655        state.call_info[state.ci.0 as usize].next.is_none(),
4656        "extend_ci: current ci already has a cached next frame"
4657    );
4658
4659    let current_idx = state.ci;
4660    // macros.tsv: luaM_new → Box::new(T::default()) — here we push onto the Vec
4661    let new_idx = CallInfoIdx(state.call_info.len() as u32);
4662
4663    state.call_info.push(CallInfo {
4664        previous: Some(current_idx),
4665        next: None,
4666        u: CallInfoFrame::lua_default(),
4667        ..CallInfo::default()
4668    });
4669
4670    state.call_info[current_idx.0 as usize].next = Some(new_idx);
4671
4672    state.nci += 1;
4673
4674    new_idx
4675}
4676
4677/// Free all cached (unused) `CallInfo` frames beyond the current frame.
4678///
4679///
4680/// ```c
4681///
4682/// //   CallInfo *ci = L->ci;
4683/// //   CallInfo *next = ci->next;
4684/// //   ci->next = NULL;
4685/// //   while ((ci = next) != NULL) {
4686/// //     next = ci->next;
4687/// //     luaM_free(L, ci);
4688/// //     L->nci--;
4689/// //   }
4690/// // }
4691/// ```
4692///
4693/// PORT NOTE: In C, each `CallInfo` is an independent heap allocation freed by
4694/// `luaM_free`.  In Rust, all `CallInfo` entries live in `state.call_info: Vec<CallInfo>`.
4695/// We walk the link chain to count removals (updating `nci`), then truncate the Vec.
4696/// This is safe as long as all free entries have indices greater than `state.ci`.
4697fn free_ci(state: &mut LuaState) {
4698    let ci_idx = state.ci.0 as usize;
4699
4700    let mut next_opt = state.call_info[ci_idx].next.take();
4701
4702    while let Some(idx) = next_opt {
4703        next_opt = state.call_info[idx.0 as usize].next;
4704        state.nci = state.nci.saturating_sub(1);
4705    }
4706
4707    // Truncate: drop all entries beyond the current ci.
4708    // TODO(port): verify invariant that all cached frames have contiguous indices > state.ci
4709    state.call_info.truncate(ci_idx + 1);
4710}
4711
4712/// Free approximately half of the cached `CallInfo` frames beyond the current frame.
4713///
4714///
4715/// ```c
4716///
4717/// //   CallInfo *ci = L->ci->next;
4718/// //   CallInfo *next;
4719/// //   if (ci == NULL) return;
4720/// //   while ((next = ci->next) != NULL) {
4721/// //     CallInfo *next2 = next->next;
4722/// //     ci->next = next2;
4723/// //     L->nci--;
4724/// //     luaM_free(L, next);
4725/// //     if (next2 == NULL) break;
4726/// //     else { next2->previous = ci; ci = next2; }
4727/// //   }
4728/// // }
4729/// ```
4730///
4731/// PORT NOTE: The C code removes every other node from the free-list chain by
4732/// pointer manipulation.  In Rust, removing elements from the middle of a `Vec`
4733/// shifts subsequent elements and invalidates `CallInfoIdx` values that point
4734/// past the removal site.  For Phase A, we approximate by halving the free count
4735/// via truncation.  TODO(port): Phase B should implement a proper free-list
4736/// pool (e.g., a slab) that allows O(1) element removal without index
4737/// invalidation.
4738pub(crate) fn shrink_ci(state: &mut LuaState) {
4739    let ci_idx = state.ci.0 as usize;
4740
4741    if state.call_info[ci_idx].next.is_none() {
4742        return;
4743    }
4744
4745    let free_count = state.call_info.len().saturating_sub(ci_idx + 1);
4746    if free_count <= 1 {
4747        return;
4748    }
4749
4750    // Remove every other cached frame (halve the free list).
4751    // PERF(port): truncation is O(n) copy for the drop; a slab allocator
4752    // would be O(1) — profile in Phase B.
4753    let keep = free_count / 2;
4754    let removed = free_count - keep;
4755    let new_len = ci_idx + 1 + keep;
4756    state.call_info.truncate(new_len);
4757    state.nci = state.nci.saturating_sub(removed as u32);
4758
4759    // Terminate the now-last cached frame.
4760    if let Some(last) = state.call_info.last_mut() {
4761        last.next = None;
4762    }
4763}
4764
4765/// Check whether the C-call depth has reached its limit and raise an error if so.
4766///
4767///
4768/// ```c
4769///
4770/// //   if (getCcalls(L) == LUAI_MAXCCALLS)
4771/// //     luaG_runerror(L, "C stack overflow");
4772/// //   else if (getCcalls(L) >= (LUAI_MAXCCALLS / 10 * 11))
4773/// //     luaD_throw(L, LUA_ERRERR);
4774/// // }
4775/// ```
4776pub(crate) fn check_c_stack(state: &mut LuaState) -> Result<(), LuaError> {
4777    // macros.tsv: getCcalls → state.c_calls()
4778    // error_sites.tsv: luaG_runerror → return Err(LuaError::runtime(format_args!(...)))
4779    if state.c_calls() == LUAI_MAXCCALLS {
4780        return Err(LuaError::runtime(format_args!("C stack overflow")));
4781    }
4782    // error_sites.tsv: luaD_throw(L, LUA_ERRERR) → return Err(LuaError::with_status(LuaStatus::ErrErr))
4783    if state.c_calls() >= (LUAI_MAXCCALLS / 10 * 11) {
4784        // TODO(port): LuaError::with_status takes a LuaStatus enum, not a raw i32.
4785        // The exact constructor shape depends on lua-types/error.rs in Phase B.
4786        return Err(LuaError::runtime(format_args!(
4787            "error while handling stack overflow (C stack overflow)"
4788        )));
4789    }
4790    Ok(())
4791}
4792
4793/// Increment the C-call depth counter, checking for overflow.
4794///
4795///
4796/// ```c
4797///
4798/// //   L->n_ccalls++;
4799/// //   if (l_unlikely(getCcalls(L) >= LUAI_MAXCCALLS))
4800/// //     luaE_checkcstack(L);
4801/// // }
4802/// ```
4803pub fn inc_c_stack(state: &mut LuaState) -> Result<(), LuaError> {
4804    state.n_ccalls += 1;
4805    // macros.tsv: l_unlikely → x (drop branch hint); getCcalls → state.c_calls()
4806    if state.c_calls() >= LUAI_MAXCCALLS {
4807        check_c_stack(state)?;
4808    }
4809    Ok(())
4810}
4811
4812//
4813// PORT NOTE: In C, `L` is a separate thread used only for memory allocation
4814// (via `luaM_newvector`).  In Rust we don't have a custom allocator; all
4815// allocation goes through the global Rust allocator.  The function takes only
4816// the new thread (`thread`) and ignores the caller.
4817fn stack_init(thread: &mut LuaState) {
4818    // macros.tsv: luaM_newvector → vec![T::default(); n]
4819    let total_slots = BASIC_STACK_SIZE + EXTRA_STACK;
4820    thread.stack = vec![StackValue::default(); total_slots];
4821
4822    // types.tsv: lua_State.tbclist → Vec<StackIdx>
4823    // PORT NOTE: In C, tbclist.p = stack.p is a sentinel meaning "no tbc vars".
4824    // In Rust the Vec is empty when there are no tbc variables.
4825    thread.tbclist = Vec::new();
4826
4827    //      setnilvalue(s2v(L1->stack.p + i));  /* erase new stack */
4828    // macros.tsv: setnilvalue → *o = LuaValue::Nil
4829    // Already initialized to LuaValue::Nil via StackValue::default().
4830
4831    thread.top = StackIdx(0);
4832
4833    thread.stack_last = StackIdx(BASIC_STACK_SIZE as u32);
4834
4835
4836    let base_ci = CallInfo {
4837        func: StackIdx(0),
4838        top: StackIdx(1 + LUA_MINSTACK as u32),
4839        previous: None,
4840        next: None,
4841        callstatus: CIST_C,
4842        nresults: 0,
4843        u: CallInfoFrame::c_default(),
4844        u2: CallInfoExtra::default(),
4845    };
4846
4847    if thread.call_info.is_empty() {
4848        thread.call_info.push(base_ci);
4849    } else {
4850        thread.call_info[0] = base_ci;
4851        thread.call_info.truncate(1);
4852    }
4853
4854    thread.stack[0] = StackValue { val: LuaValue::Nil, tbc_delta: 0 };
4855
4856    thread.top = StackIdx(1);
4857
4858    thread.ci = CallInfoIdx(0);
4859}
4860
4861fn free_stack(state: &mut LuaState) {
4862    if state.stack.is_empty() {
4863        return;
4864    }
4865    state.ci = CallInfoIdx(0);
4866    free_ci(state);
4867    debug_assert_eq!(state.nci, 0, "nci should be 0 after free_ci");
4868    // macros.tsv: luaM_freearray → (Rust's Drop handles deallocation; drop the call)
4869    state.stack.clear();
4870    state.stack.shrink_to_fit();
4871}
4872
4873fn init_registry(state: &mut LuaState) -> Result<(), LuaError> {
4874    // macros.tsv: luaH_new → state.new_table()
4875    let registry = state.new_table();
4876
4877    // macros.tsv: sethvalue → *o = LuaValue::Table(x.clone())
4878    state.global_mut().l_registry = LuaValue::Table(registry.clone());
4879
4880    // macros.tsv: luaH_resize → t.resize(state, na, nh)?
4881    // TODO(port): registry is a GcRef<LuaTable> (Rc); calling methods requires borrow_mut()
4882    // For Phase A, use RefCell interior mutability on LuaTable, or accept the limitation.
4883    // Using Rc::get_mut is not available because of possible aliasing.
4884    // TODO(port): LuaTable resize requires &mut access through Rc — needs RefCell<LuaTable>
4885    //   or a redesign in Phase B.
4886
4887    // macros.tsv: setthvalue → *o = LuaValue::Thread(x.clone())
4888    // TODO(port): cannot create GcRef<LuaState> to self (self-referential Rc).
4889    // In Phase E this would be resolved once coroutine threads are GcRef-tracked.
4890    // For Phase A: leave registry[LUA_RIDX_MAINTHREAD-1] as Nil and add a TODO.
4891    // TODO(port): set registry[LUA_RIDX_MAINTHREAD - 1] = LuaValue::Thread(main_thread_gcref)
4892
4893    // PORT NOTE (phase-b-reconcile): The lua-types LuaTable placeholder is
4894    // storage-less, so we can't actually persist the globals table inside
4895    // the registry via array_set. Store it in a direct GlobalState field
4896    // and patch get_global_table to read it from there. Symmetric for the
4897    // _LOADED module cache. Once the LuaTable placeholder reconciles, the
4898    // canonical registry storage takes over and these fields disappear.
4899    let globals = state.new_table();
4900    state.global_mut().globals = LuaValue::Table(globals);
4901    let loaded = state.new_table();
4902    state.global_mut().loaded = LuaValue::Table(loaded);
4903
4904    Ok(())
4905}
4906
4907fn lua_open(state: &mut LuaState) -> Result<(), LuaError> {
4908    stack_init(state);
4909    init_registry(state)?;
4910    crate::string::init(state)?;
4911    crate::tagmethods::init(state)?;
4912    // TODO(port): luaX_init lives in the lua-lex crate; cross-crate call needed in Phase B
4913    state.global_mut().gcstp = 0;
4914    state.global().heap.unpause();
4915    // macros.tsv: setnilvalue → *o = LuaValue::Nil
4916    // PORT NOTE: setting nilvalue = Nil signals completestate() → is_complete() = true
4917    state.global_mut().nilvalue = LuaValue::Nil;
4918    // macros.tsv: luai_userstateopen → (extension hook, no-op default; drop)
4919    Ok(())
4920}
4921
4922fn preinit_thread(thread: &mut LuaState, global: Rc<RefCell<GlobalState>>) {
4923    thread.global = global;
4924    thread.stack = Vec::new();
4925    thread.call_info = Vec::new();
4926    // PORT NOTE: We initialize ci to 0 but call_info is empty; stack_init() must be
4927    // called before any use of call_info.
4928    thread.ci = CallInfoIdx(0);
4929    thread.nci = 0;
4930    // PORT NOTE: In C, L->twups = L is a self-reference sentinel meaning "no open upvals".
4931    // In Rust, GlobalState.twups is a Vec<GcRef<LuaState>>; absence from that Vec is the
4932    // sentinel.  The per-thread `twups` field is removed (types.tsv: lua_State.twups → removed).
4933    thread.n_ccalls = 0;
4934    thread.hook = None;
4935    thread.hookmask = 0;
4936    thread.basehookcount = 0;
4937    thread.allowhook = true;
4938    // macros.tsv: resethookcount → state.reset_hook_count()
4939    thread.hookcount = thread.basehookcount;
4940
4941    // Sandbox inheritance: a coroutine joins the runtime-wide instruction/memory
4942    // budget so metering spans every thread, not just the main one. The budget
4943    // itself lives in `GlobalState` (shared); the new thread only needs the
4944    // count-hook mask armed so the dispatch loop traps and charges it.
4945    {
4946        let (active, interval) = {
4947            let g = thread.global.borrow();
4948            (g.sandbox_active(), g.sandbox.interval.get())
4949        };
4950        if active {
4951            thread.hookmask = SANDBOX_COUNT_MASK;
4952            thread.basehookcount = interval;
4953            thread.hookcount = interval;
4954        }
4955    }
4956    thread.openupval = Vec::new();
4957    thread.status = LuaStatus::Ok as u8;
4958    thread.errfunc = 0;
4959    thread.oldpc = 0;
4960    thread.gc_check_needed = true;
4961}
4962
4963fn close_state(state: &mut LuaState) {
4964    let is_complete = state.global().is_complete();
4965
4966    if !is_complete {
4967        // macros.tsv: luaC_freeallobjects via GcHandle
4968        state.gc().free_all_objects();
4969    } else {
4970        state.ci = CallInfoIdx(0);
4971        // TODO(port): crate::do_::close_protected(state, StackIdx(1), LuaStatus::Ok)
4972        // Ignoring result here because we are in teardown (same as C behavior).
4973        state.gc().free_all_objects();
4974        // macros.tsv: luai_userstateclose → (extension hook; drop)
4975    }
4976
4977    // macros.tsv: luaM_freearray → (Rust's Drop handles deallocation; drop the call)
4978    state.global_mut().strt = StringPool::default();
4979
4980    free_stack(state);
4981
4982    // PORT NOTE: C-specific memory accounting assertion; not applicable in Rust.
4983
4984    // PORT NOTE: Custom allocator freed LG here. Rust's allocator (via Drop) handles
4985    // deallocation of GlobalState and LuaState automatically.
4986}
4987
4988/// Create a new coroutine thread sharing the same GlobalState as the caller.
4989///
4990/// Pushes the new thread onto the caller's stack and returns `Ok(())`.
4991///
4992///
4993/// ```c
4994///
4995/// //   global_State *g = G(L);
4996/// //   GCObject *o;
4997/// //   lua_State *L1;
4998/// //   lua_lock(L); luaC_checkGC(L);
4999/// //   o = luaC_newobjdt(L, LUA_TTHREAD, sizeof(LX), offsetof(LX, l));
5000/// //   L1 = gco2th(o);
5001/// //   setthvalue2s(L, L->top.p, L1); api_incr_top(L);
5002/// //   preinit_thread(L1, g);
5003/// //   ... (copy hook settings, extra space, stack_init) ...
5004/// //   lua_unlock(L); return L1;
5005/// // }
5006/// ```
5007/// Allocate a fresh coroutine `LuaState`, register it under a new
5008/// `ThreadId`, and push the resulting `LuaValue::Thread(value)` onto
5009/// `state`'s stack.
5010///
5011/// If `initial_body` is `Some(f)`, `f` is also pushed onto the new
5012/// thread's stack so that `coroutine.status` reports `"suspended"`
5013/// rather than `"dead"`. The full cross-thread `xmove` from caller to
5014/// coroutine arrives in slice 02b; `co_create` uses `initial_body` to
5015/// stage the body without needing a real `xmove`.
5016pub fn new_thread(state: &mut LuaState, initial_body: Option<LuaValue>) -> Result<(), LuaError> {
5017    state.gc().check_step();
5018
5019    // PORT NOTE: In C, the new thread is GC-allocated as part of the allgc list.
5020    // In Rust (Phase A), we create a plain LuaState; Phase D will wire GC registration.
5021    // TODO(port): allocate via state.gc().new_obj(LuaType::Thread, ...) in Phase D
5022
5023    let global_rc = state.global_rc();
5024    let hookmask = state.hookmask;
5025    let basehookcount = state.basehookcount;
5026
5027    let reserved_id = {
5028        let mut g = state.global_mut();
5029        let id = g.next_thread_id;
5030        g.next_thread_id += 1;
5031        id
5032    };
5033
5034    let mut new_thread = LuaState {
5035        status: LuaStatus::Ok as u8,
5036        allowhook: true,
5037        nci: 0,
5038        top: StackIdx(0),
5039        stack_last: StackIdx(0),
5040        stack: Vec::new(),
5041        ci: CallInfoIdx(0),
5042        call_info: Vec::new(),
5043        openupval: Vec::new(),
5044        tbclist: Vec::new(),
5045        global: global_rc.clone(),
5046        hook: None,
5047        hookmask: 0,
5048        basehookcount: 0,
5049        hookcount: 0,
5050        errfunc: 0,
5051        n_ccalls: 0,
5052        oldpc: 0,
5053        marked: 0,
5054        cached_thread_id: reserved_id,
5055        gc_check_needed: false,
5056    };
5057
5058    preinit_thread(&mut new_thread, global_rc);
5059
5060    new_thread.hookmask = hookmask;
5061    new_thread.basehookcount = basehookcount;
5062    // TODO(port): lua_Hook is Box<dyn FnMut(...)>; not Clone.
5063    // Sharing a hook between threads would require Arc<Mutex<...>> (Phase E debug).
5064    new_thread.reset_hook_count();
5065
5066    // macros.tsv: lua_getextraspace → state.extra_space_mut() → &mut [u8]
5067    // TODO(port): LuaState.extra_space field not yet defined; Phase B
5068
5069    // macros.tsv: luai_userstatethread → (extension hook; drop)
5070
5071    stack_init(&mut new_thread);
5072
5073    if let Some(body) = initial_body {
5074        new_thread.push(body);
5075    }
5076
5077    let thread_ref: Rc<RefCell<LuaState>> = Rc::new(RefCell::new(new_thread));
5078
5079    let value = {
5080        let mut g = state.global_mut();
5081        let id = reserved_id;
5082        let value = GcRef::new(lua_types::value::LuaThread::new(id));
5083        g.threads.insert(
5084            id,
5085            ThreadRegistryEntry { state: thread_ref, value: value.clone() },
5086        );
5087        value
5088    };
5089
5090    state.push(LuaValue::Thread(value));
5091
5092    Ok(())
5093}
5094
5095/// Reset a thread to its base state, closing all to-be-closed variables.
5096///
5097/// Returns the final status code as an `i32` (mirrors the C API).
5098///
5099///
5100/// ```c
5101///
5102/// //   CallInfo *ci = L->ci = &L->base_ci;
5103/// //   setnilvalue(s2v(L->stack.p));
5104/// //   ci->func.p = L->stack.p;
5105/// //   ci->callstatus = CIST_C;
5106/// //   if (status == LUA_YIELD) status = LUA_OK;
5107/// //   L->status = LUA_OK;  /* so it can run __close metamethods */
5108/// //   status = luaD_closeprotected(L, 1, status);
5109/// //   if (status != LUA_OK) luaD_seterrorobj(L, status, L->stack.p + 1);
5110/// //   else L->top.p = L->stack.p + 1;
5111/// //   ci->top.p = L->top.p + LUA_MINSTACK;
5112/// //   luaD_reallocstack(L, cast_int(ci->top.p - L->stack.p), 0);
5113/// //   return status;
5114/// // }
5115/// ```
5116pub fn reset_thread(state: &mut LuaState, status: i32) -> i32 {
5117    state.ci = CallInfoIdx(0);
5118    let ci_idx = 0usize;
5119
5120    // macros.tsv: setnilvalue → *o = LuaValue::Nil; s2v → state.stack_at(idx)
5121    if !state.stack.is_empty() {
5122        state.stack[0].val = LuaValue::Nil;
5123    }
5124
5125    state.call_info[ci_idx].func = StackIdx(0);
5126    state.call_info[ci_idx].callstatus = CIST_C;
5127
5128    let mut status = if status == LuaStatus::Yield as i32 {
5129        LuaStatus::Ok as i32
5130    } else {
5131        status
5132    };
5133
5134    state.status = LuaStatus::Ok as u8;
5135
5136    let close_status = crate::do_::close_protected(
5137        state,
5138        StackIdx(1),
5139        LuaStatus::from_raw(status),
5140    );
5141    status = close_status as i32;
5142
5143    if status != LuaStatus::Ok as i32 {
5144        crate::do_::set_error_obj(state, LuaStatus::from_raw(status), StackIdx(1));
5145    } else {
5146        state.top = StackIdx(1);
5147    }
5148
5149    let new_ci_top = StackIdx(state.top.0 + LUA_MINSTACK as u32);
5150    state.call_info[ci_idx].top = new_ci_top;
5151
5152    // TODO(port): crate::do_::realloc_stack(state, new_ci_top.0 as i32, 0) — ldo.c → do_.rs
5153    // For Phase A, grow the stack if needed to at least new_ci_top slots.
5154    let needed = new_ci_top.0 as usize;
5155    if state.stack.len() < needed {
5156        state.stack.resize(needed, StackValue::default());
5157    }
5158
5159    status
5160}
5161
5162/// Close a coroutine thread from the perspective of another thread.
5163///
5164///
5165/// ```c
5166///
5167/// //   int status;
5168/// //   lua_lock(L);
5169/// //   L->n_ccalls = (from) ? getCcalls(from) : 0;
5170/// //   status = luaE_resetthread(L, L->status);
5171/// //   lua_unlock(L);
5172/// //   return status;
5173/// // }
5174/// ```
5175pub fn close_thread(state: &mut LuaState, from: Option<&LuaState>) -> i32 {
5176    // macros.tsv: getCcalls → state.c_calls()
5177    state.n_ccalls = match from {
5178        Some(f) => f.c_calls(),
5179        None => 0,
5180    };
5181    let current_status = state.status as i32;
5182    let result = reset_thread(state, current_status);
5183    result
5184}
5185
5186/// Deprecated wrapper for `close_thread(L, NULL)`.
5187///
5188///
5189/// ```c
5190///
5191/// //   return lua_closethread(L, NULL);
5192/// // }
5193/// ```
5194pub fn reset_thread_api(state: &mut LuaState) -> i32 {
5195    close_thread(state, None)
5196}
5197
5198/// Create a new independent Lua state.  Returns `None` only on OOM.
5199///
5200///
5201/// PORT NOTE: The C API takes a custom allocator `(f, ud)`.  The Rust-native API
5202/// uses the global Rust allocator; those parameters are dropped.  Equivalent to
5203/// `LuaState::new()` at the call site.
5204///
5205/// ```c
5206///
5207/// //   int i;
5208/// //   lua_State *L;
5209/// //   global_State *g;
5210/// //   LG *l = cast(LG *, (*f)(ud, NULL, LUA_TTHREAD, sizeof(LG)));
5211/// //   if (l == NULL) return NULL;
5212/// //   L = &l->l.l; g = &l->g;
5213/// //   L->tt = LUA_VTHREAD;
5214/// //   g->currentwhite = bitmask(WHITE0BIT);
5215/// //   L->marked = luaC_white(g);
5216/// //   preinit_thread(L, g);
5217/// //   g->allgc = obj2gco(L);
5218/// //   L->next = NULL;
5219/// //   incnny(L);
5220/// //   g->frealloc = f; g->ud = ud; g->warnf = NULL; g->ud_warn = NULL;
5221/// //   g->mainthread = L; g->seed = luai_makeseed(L);
5222/// //   g->gcstp = GCSTPGC;
5223/// //   ... (zero-init all GC list pointers and tunables) ...
5224/// //   setivalue(&g->nilvalue, 0);  /* signal: state not yet built */
5225/// //   ... (setgcparam tunables) ...
5226/// //   for (i=0; i < LUA_NUMTAGS; i++) g->mt[i] = NULL;
5227/// //   if (luaD_rawrunprotected(L, f_luaopen, NULL) != LUA_OK) {
5228/// //     close_state(L); L = NULL;
5229/// //   }
5230/// //   return L;
5231/// // }
5232/// ```
5233pub fn new_state() -> Option<LuaState> {
5234    // In Rust, allocation failure panics by default; we use Result internally.
5235
5236    // Build a dummy LuaString for memerrmsg and strcache initialization.
5237    // This is a chicken-and-egg problem: GlobalState.memerrmsg needs to be initialized
5238    // before luaS_init, but luaS_init creates the memerrmsg.
5239    // We use a placeholder Rc<LuaString> that will be replaced by luaS_init.
5240    // TODO(port): this is fragile; Phase B should ensure memerrmsg is properly set by luaS_init.
5241    // TODO(D-1c-bridge): allocation outside state context (new_state() free fn — no LuaState yet)
5242    let placeholder_str = GcRef::new(LuaString::placeholder());
5243
5244    // macros.tsv: bitmask → (1u32 << b); WHITE0BIT = 0 → 1u8
5245    let initial_white = 1u8 << WHITE0BIT;
5246
5247    // macros.tsv: setivalue → *o = LuaValue::Int(x)
5248    // PORT NOTE: non-nil nilvalue signals "state not yet complete"; see is_complete().
5249
5250    let global = GlobalState {
5251        parser_hook: None,
5252        cli_argv: None,
5253        cli_preload: None,
5254        lua_version: lua_types::LuaVersion::default(),
5255        file_loader_hook: None,
5256        file_open_hook: None,
5257        stdout_hook: None,
5258        stderr_hook: None,
5259        stdin_hook: None,
5260        env_hook: None,
5261        unix_time_hook: None,
5262        cpu_clock_hook: None,
5263        local_offset_hook: None,
5264        entropy_hook: None,
5265        temp_name_hook: None,
5266        popen_hook: None,
5267        file_remove_hook: None,
5268        file_rename_hook: None,
5269        os_execute_hook: None,
5270        dynlib_load_hook: None,
5271        dynlib_symbol_hook: None,
5272        dynlib_unload_hook: None,
5273        sandbox: SandboxLimits::default(),
5274        gc_debt: 0,
5275        gc_estimate: 0,
5276        lastatomic: 0,
5277        strt: StringPool::default(),
5278        l_registry: LuaValue::Nil,
5279        external_roots: ExternalRootSet::default(),
5280        globals: LuaValue::Nil,
5281        loaded: LuaValue::Nil,
5282        nilvalue: LuaValue::Int(0),
5283        seed: make_seed(),
5284        currentwhite: initial_white,
5285        gcstate: GCS_PAUSE,
5286        // macros.tsv: KGC_INC → GcKind::Incremental
5287        gckind: GcKind::Incremental as u8,
5288        gcstopem: false,
5289        genminormul: LUAI_GENMINORMUL,
5290        // macros.tsv: setgcparam → p = v / 4
5291        genmajormul: (LUAI_GENMAJORMUL / 4) as u8,
5292        gcstp: GCSTPGC,
5293        gcemergency: false,
5294        gcpause: (LUAI_GCPAUSE / 4) as u8,
5295        gcstepmul: (LUAI_GCMUL / 4) as u8,
5296        gcstepsize: LUAI_GCSTEPSIZE,
5297        // Lua 5.5 collectgarbage("param") defaults, observed on lua5.5.0:
5298        // [minormul, majorminor, minormajor, pause, stepmul, stepsize].
5299        gc55_params: [20, 50, 68, 250, 200, 9600],
5300        sweepgc_cursor: 0,
5301        weak_tables_registry: lua_gc::WeakRegistry::default(),
5302        finalizers: lua_gc::FinalizerRegistry::default(),
5303        gc_finalizer_error: None,
5304        twups: Vec::new(),
5305        panic: None,
5306        mainthread: None,
5307        threads: std::collections::HashMap::new(),
5308        main_thread_value: GcRef::new(lua_types::value::LuaThread::new(0)),
5309        current_thread_id: 0,
5310        main_thread_id: 0,
5311        next_thread_id: 1,
5312        memerrmsg: placeholder_str.clone(),
5313        tmname: Vec::new(),
5314        mt: std::array::from_fn(|_| None),
5315        strcache: std::array::from_fn(|_| {
5316            std::array::from_fn(|_| placeholder_str.clone())
5317        }),
5318        interned_lt: InternedStringMap::default(),
5319        warnf: None,
5320        warn_mode: WarnMode::Off,
5321        test_warn_enabled: false,
5322        test_warn_on: false,
5323        test_warn_mode: TestWarnMode::Normal,
5324        test_warn_last_to_cont: false,
5325        test_warn_buffer: Vec::new(),
5326        c_functions: Vec::new(),
5327        heap: lua_gc::Heap::new(),
5328        cross_thread_upvals: std::collections::HashMap::new(),
5329        suspended_parent_stacks: Vec::new(),
5330        suspended_parent_open_upvals: Vec::new(),
5331    };
5332
5333    let global_rc = Rc::new(RefCell::new(global));
5334
5335    // macros.tsv: luaC_white → g.current_white()
5336    let initial_marked = initial_white;
5337
5338    let mut main_thread = LuaState {
5339        status: LuaStatus::Ok as u8,
5340        allowhook: true,
5341        nci: 0,
5342        top: StackIdx(0),
5343        stack_last: StackIdx(0),
5344        stack: Vec::new(),
5345        ci: CallInfoIdx(0),
5346        call_info: Vec::new(),
5347        openupval: Vec::new(),
5348        tbclist: Vec::new(),
5349        global: global_rc.clone(),
5350        hook: None,
5351        hookmask: 0,
5352        basehookcount: 0,
5353        hookcount: 0,
5354        errfunc: 0,
5355        n_ccalls: 0,
5356        oldpc: 0,
5357        marked: initial_marked,
5358        cached_thread_id: 0,
5359        gc_check_needed: false,
5360    };
5361
5362    preinit_thread(&mut main_thread, global_rc.clone());
5363
5364    // macros.tsv: incnny → state.inc_nny() → L->n_ccalls += 0x10000
5365    main_thread.inc_nny();
5366
5367    // TODO(port): self-referential Rc cycle; Phase D GC handles cycles.
5368    // For Phase A: skip setting mainthread to avoid the cycle.
5369
5370    // TODO(port): Phase D — register main_thread in allgc as a GcRef
5371
5372    //      close_state(L); L = NULL; }
5373    // error_sites.tsv: luaD_rawrunprotected → state.run_protected(|s| f(s, ud))
5374    // PORT NOTE: We call lua_open directly since we're not using the protected-call
5375    // machinery yet (ldo.c is not ported). Errors from lua_open propagate as Err.
5376    match lua_open(&mut main_thread) {
5377        Ok(()) => {}
5378        Err(_) => {
5379            close_state(&mut main_thread);
5380            return None;
5381        }
5382    }
5383
5384    Some(main_thread)
5385}
5386
5387/// Close the Lua state and free all resources.
5388///
5389///
5390/// PORT NOTE: In C, `lua_close` gets the main thread via `G(L)->mainthread`
5391/// and closes that regardless of which thread is passed.  In Rust, the caller
5392/// should hold the main `LuaState` and drop it (which triggers `close_state`
5393/// via this function or `Drop`).
5394///
5395/// ```c
5396///
5397/// //   lua_lock(L);
5398/// //   L = G(L)->mainthread;  /* only the main thread can be closed */
5399/// //   close_state(L);
5400/// // }
5401/// ```
5402pub fn close(mut state: LuaState) {
5403    // PORT NOTE: In Rust, callers must pass the main LuaState directly (or obtain it
5404    // from GlobalState.mainthread).  We do not traverse to the main thread here;
5405    // the caller owns the root state.
5406    // TODO(port): assert that `state` is indeed the main thread before closing
5407    close_state(&mut state);
5408}
5409
5410/// Forward a warning message through the configured warning sink.
5411///
5412///
5413/// ```c
5414///
5415/// //   lua_WarnFunction wf = G(L)->warnf;
5416/// //   if (wf != NULL) wf(G(L)->ud_warn, msg, tocont);
5417/// // }
5418/// ```
5419pub(crate) fn warning(state: &mut LuaState, msg: &[u8], to_cont: bool) {
5420    let test_warn_enabled = state.global().test_warn_enabled;
5421    if test_warn_enabled {
5422        test_warn(state, msg, to_cont);
5423        return;
5424    }
5425
5426    // types.tsv: global_State.warnf → Option<Box<dyn FnMut(&[u8], bool)>>
5427    // types.tsv: global_State.ud_warn → (removed; folded into the closure)
5428    // PORT NOTE: We must drop the RefMut borrow before calling the closure to avoid
5429    // a potential re-entrant borrow_mut() if the closure calls back into Lua.
5430    // We check for the presence of warnf while holding a borrow, then call it.
5431    // TODO(port): if the warning function needs to call back into state (e.g. to push
5432    // a Lua error), this will panic at runtime due to RefCell re-entry. Phase B should
5433    // design a safe re-entrance pattern (e.g. take + restore the warnf closure).
5434    let has_warnf = state.global().warnf.is_some();
5435    if has_warnf {
5436        // Take the warnf closure out to avoid re-entrant borrow.
5437        let mut warnf = state.global_mut().warnf.take();
5438        if let Some(ref mut f) = warnf {
5439            f(msg, to_cont);
5440        }
5441        // Restore the closure.
5442        state.global_mut().warnf = warnf;
5443        return;
5444    }
5445    default_warn(state, msg, to_cont);
5446}
5447
5448fn test_warn(state: &mut LuaState, msg: &[u8], to_cont: bool) {
5449    let is_control = {
5450        let g = state.global();
5451        !g.test_warn_last_to_cont && !to_cont && msg.first() == Some(&b'@')
5452    };
5453    if is_control {
5454        let mut g = state.global_mut();
5455        match &msg[1..] {
5456            b"off" => g.test_warn_on = false,
5457            b"on" => g.test_warn_on = true,
5458            b"normal" => g.test_warn_mode = TestWarnMode::Normal,
5459            b"allow" => g.test_warn_mode = TestWarnMode::Allow,
5460            b"store" => g.test_warn_mode = TestWarnMode::Store,
5461            _ => {}
5462        }
5463        return;
5464    }
5465
5466    let finished = {
5467        let mut g = state.global_mut();
5468        g.test_warn_last_to_cont = to_cont;
5469        g.test_warn_buffer.extend_from_slice(msg);
5470        if to_cont {
5471            None
5472        } else {
5473            Some((
5474                std::mem::take(&mut g.test_warn_buffer),
5475                g.test_warn_mode,
5476                g.test_warn_on,
5477            ))
5478        }
5479    };
5480
5481    let Some((message, mode, warn_on)) = finished else {
5482        return;
5483    };
5484    match mode {
5485        TestWarnMode::Normal => {
5486            if warn_on && message.first() == Some(&b'#') {
5487                write_warning_message(&message);
5488            }
5489        }
5490        TestWarnMode::Allow => {
5491            if warn_on {
5492                write_warning_message(&message);
5493            }
5494        }
5495        TestWarnMode::Store => {
5496            if let Ok(s) = state.intern_str(&message) {
5497                state.push(LuaValue::Str(s));
5498                let _ = crate::api::set_global(state, b"_WARN");
5499            }
5500        }
5501    }
5502}
5503
5504fn write_warning_message(message: &[u8]) {
5505    use std::io::Write;
5506    let stderr = std::io::stderr();
5507    let mut h = stderr.lock();
5508    let _ = h.write_all(b"Lua warning: ");
5509    let _ = h.write_all(message);
5510    let _ = h.write_all(b"\n");
5511}
5512
5513/// The default warning handler: a faithful port of the `warnfoff` /
5514/// `warnfon` / `warnfcont` chain in upstream `lauxlib.c`. State is held in
5515/// `GlobalState::warn_mode` (C threads it via `lua_setwarnf`); output goes to
5516/// stderr (`lua_writestringerror`).
5517fn default_warn(state: &mut LuaState, msg: &[u8], to_cont: bool) {
5518    use std::io::Write;
5519    // checkcontrol: a leading-`@` non-continuation message is a control word.
5520    if !to_cont && msg.first() == Some(&b'@') {
5521        match &msg[1..] {
5522            b"off" => state.global_mut().warn_mode = WarnMode::Off,
5523            b"on" => state.global_mut().warn_mode = WarnMode::On,
5524            _ => {}
5525        }
5526        return;
5527    }
5528    let mode = state.global().warn_mode;
5529    match mode {
5530        WarnMode::Off => {}
5531        WarnMode::On | WarnMode::Cont => {
5532            let stderr = std::io::stderr();
5533            let mut h = stderr.lock();
5534            if mode == WarnMode::On {
5535                let _ = h.write_all(b"Lua warning: ");
5536            }
5537            let _ = h.write_all(msg);
5538            if to_cont {
5539                state.global_mut().warn_mode = WarnMode::Cont;
5540            } else {
5541                let _ = h.write_all(b"\n");
5542                state.global_mut().warn_mode = WarnMode::On;
5543            }
5544        }
5545    }
5546}
5547
5548#[cfg(test)]
5549mod tests {
5550    use super::*;
5551
5552    fn test_noop_cclosure(_: &mut LuaState) -> Result<usize, LuaError> {
5553        Ok(0)
5554    }
5555
5556    #[test]
5557    fn external_root_keys_reject_stale_slot_after_reuse() {
5558        let mut roots = ExternalRootSet::default();
5559
5560        let first = roots.insert(LuaValue::Int(1));
5561        assert_eq!(roots.len(), 1);
5562        assert_eq!(roots.get(first), Some(&LuaValue::Int(1)));
5563
5564        assert_eq!(roots.remove(first), Some(LuaValue::Int(1)));
5565        assert!(roots.get(first).is_none());
5566        assert!(roots.remove(first).is_none());
5567        assert_eq!(roots.len(), 0);
5568        assert_eq!(roots.vacant_len(), 1);
5569        assert!(roots.replace(first, LuaValue::Int(9)).is_none());
5570        assert!(roots.is_empty());
5571
5572        let second = roots.insert(LuaValue::Int(2));
5573        assert_eq!(first.index, second.index);
5574        assert_ne!(first, second);
5575        assert!(roots.get(first).is_none());
5576        assert_eq!(roots.get(second), Some(&LuaValue::Int(2)));
5577        assert!(roots.replace(first, LuaValue::Int(3)).is_none());
5578    }
5579
5580    #[test]
5581    fn external_roots_keep_heap_value_alive_until_unrooted() {
5582        let mut state = new_state().expect("state should initialize");
5583        let _heap_guard = {
5584            let g = state.global();
5585            lua_gc::HeapGuard::push(&g.heap)
5586        };
5587
5588        let table = state.new_table();
5589        assert_eq!(state.global().heap.allgc_count(), 1);
5590
5591        let key = state.external_root_value(LuaValue::Table(table));
5592        state.gc().full_collect();
5593        assert_eq!(state.global().heap.allgc_count(), 1);
5594        assert_eq!(state.global().external_roots.len(), 1);
5595
5596        assert!(state.external_unroot_value(key).is_some());
5597        state.gc().full_collect();
5598        assert_eq!(state.global().heap.allgc_count(), 0);
5599        assert!(state.global().external_roots.is_empty());
5600    }
5601
5602    #[test]
5603    fn table_buffer_accounting_refunds_on_sweep() {
5604        let mut state = new_state().expect("state should initialize");
5605        let _heap_guard = {
5606            let g = state.global();
5607            lua_gc::HeapGuard::push(&g.heap)
5608        };
5609
5610        let table = state.new_table();
5611        let key = state.external_root_value(LuaValue::Table(table));
5612        let header_bytes = state.global().heap.bytes_used();
5613        assert!(header_bytes > 0);
5614
5615        for i in 1..=128 {
5616            table
5617                .raw_set_int(&mut state, i, LuaValue::Int(i))
5618                .expect("integer table insert should succeed");
5619        }
5620        let grown_bytes = state.global().heap.bytes_used();
5621        assert!(
5622            grown_bytes > header_bytes,
5623            "table array/hash buffer growth must be charged to the GC heap"
5624        );
5625
5626        state.gc().full_collect();
5627        assert_eq!(
5628            state.global().heap.bytes_used(),
5629            grown_bytes,
5630            "rooted table buffer bytes should remain charged after collection"
5631        );
5632
5633        assert!(state.external_unroot_value(key).is_some());
5634        state.gc().full_collect();
5635        assert_eq!(state.global().heap.bytes_used(), 0);
5636        assert_eq!(state.global().heap.allgc_count(), 0);
5637    }
5638
5639    #[test]
5640    fn userdata_buffer_accounting_refunds_on_sweep() {
5641        let mut state = new_state().expect("state should initialize");
5642        let _heap_guard = {
5643            let g = state.global();
5644            lua_gc::HeapGuard::push(&g.heap)
5645        };
5646
5647        let payload_len = 4096;
5648        let userdata = state
5649            .new_userdata_typed(b"accounting", payload_len, 3)
5650            .expect("userdata allocation should succeed");
5651        state.pop_n(1);
5652        let key = state.external_root_value(LuaValue::UserData(userdata));
5653        let allocated_bytes = state.global().heap.bytes_used();
5654        assert!(
5655            allocated_bytes > payload_len,
5656            "userdata payload bytes must be charged to the GC heap"
5657        );
5658
5659        state.gc().full_collect();
5660        assert_eq!(
5661            state.global().heap.bytes_used(),
5662            allocated_bytes,
5663            "rooted userdata payload bytes should remain charged after collection"
5664        );
5665
5666        assert!(state.external_unroot_value(key).is_some());
5667        state.gc().full_collect();
5668        assert_eq!(state.global().heap.bytes_used(), 0);
5669        assert_eq!(state.global().heap.allgc_count(), 0);
5670    }
5671
5672    #[test]
5673    fn cclosure_upvalue_accounting_refunds_on_sweep() {
5674        let mut state = new_state().expect("state should initialize");
5675        let _heap_guard = {
5676            let g = state.global();
5677            lua_gc::HeapGuard::push(&g.heap)
5678        };
5679
5680        let nupvalues = 64;
5681        for i in 0..nupvalues {
5682            state.push(LuaValue::Int(i as i64));
5683        }
5684        crate::api::push_cclosure(&mut state, test_noop_cclosure, nupvalues as i32)
5685            .expect("C closure creation should succeed");
5686        let LuaValue::Function(LuaClosure::C(ccl)) = state.get_at(state.top_idx() - 1) else {
5687            panic!("expected heavy C closure");
5688        };
5689        let expected_payload = ccl.buffer_bytes();
5690        let key = state.external_root_value(LuaValue::Function(LuaClosure::C(ccl)));
5691        state.pop_n(1);
5692        let allocated_bytes = state.global().heap.bytes_used();
5693        assert!(
5694            allocated_bytes >= expected_payload,
5695            "C closure upvalue vector bytes must be charged to the GC heap"
5696        );
5697
5698        state.gc().full_collect();
5699        assert_eq!(
5700            state.global().heap.bytes_used(),
5701            allocated_bytes,
5702            "rooted C closure payload bytes should remain charged after collection"
5703        );
5704
5705        assert!(state.external_unroot_value(key).is_some());
5706        state.gc().full_collect();
5707        assert_eq!(state.global().heap.bytes_used(), 0);
5708        assert_eq!(state.global().heap.allgc_count(), 0);
5709    }
5710
5711    #[test]
5712    fn proto_and_lclosure_accounting_refunds_on_sweep() {
5713        let mut state = new_state().expect("state should initialize");
5714        let _heap_guard = {
5715            let g = state.global();
5716            lua_gc::HeapGuard::push(&g.heap)
5717        };
5718
5719        let mut proto = LuaProto::placeholder();
5720        proto.code = vec![lua_types::opcode::Instruction(0); 2048];
5721        proto.lineinfo = vec![0; 2048];
5722        proto.k = vec![LuaValue::Int(1); 512];
5723        let expected_proto_payload = proto.buffer_bytes();
5724        let proto = GcRef::new(proto);
5725        proto.account_buffer(expected_proto_payload as isize);
5726
5727        let closure = state.new_lclosure(proto, 16);
5728        let expected_closure_payload = closure.buffer_bytes();
5729        let key = state.external_root_value(LuaValue::Function(LuaClosure::Lua(closure)));
5730        let allocated_bytes = state.global().heap.bytes_used();
5731        assert!(
5732            allocated_bytes >= expected_proto_payload + expected_closure_payload,
5733            "proto and Lua closure vector bytes must be charged to the GC heap"
5734        );
5735
5736        state.gc().full_collect();
5737        assert_eq!(
5738            state.global().heap.bytes_used(),
5739            allocated_bytes,
5740            "rooted proto and Lua closure payload bytes should remain charged after collection"
5741        );
5742
5743        assert!(state.external_unroot_value(key).is_some());
5744        state.gc().full_collect();
5745        assert_eq!(state.global().heap.bytes_used(), 0);
5746        assert_eq!(state.global().heap.allgc_count(), 0);
5747    }
5748
5749    #[test]
5750    fn string_buffer_accounting_refunds_on_sweep() {
5751        let mut state = new_state().expect("state should initialize");
5752        let _heap_guard = {
5753            let g = state.global();
5754            lua_gc::HeapGuard::push(&g.heap)
5755        };
5756
5757        let payload = vec![b'x'; crate::string::MAX_SHORT_LEN + 4096];
5758        let string = state.intern_str(&payload).expect("long string should allocate");
5759        let key = state.external_root_value(LuaValue::Str(string));
5760        let allocated_bytes = state.global().heap.bytes_used();
5761        assert!(
5762            allocated_bytes > payload.len(),
5763            "long string backing bytes must be charged to the GC heap"
5764        );
5765
5766        state.gc().full_collect();
5767        assert_eq!(
5768            state.global().heap.bytes_used(),
5769            allocated_bytes,
5770            "rooted string buffer bytes should remain charged after collection"
5771        );
5772
5773        assert!(state.external_unroot_value(key).is_some());
5774        state.gc().full_collect();
5775        assert_eq!(state.global().heap.bytes_used(), 0);
5776        assert_eq!(state.global().heap.allgc_count(), 0);
5777    }
5778
5779    #[test]
5780    fn interned_short_string_cache_does_not_root_unreferenced_string() {
5781        let mut state = new_state().expect("state should initialize");
5782        let _heap_guard = {
5783            let g = state.global();
5784            lua_gc::HeapGuard::push(&g.heap)
5785        };
5786
5787        let payload = b"weak-cache-probe-a";
5788        let string = state
5789            .intern_str(payload)
5790            .expect("short string should intern");
5791        let id = string.identity();
5792        assert!(state.global().interned_lt.contains_key(&payload[..]));
5793        assert!(state.global().heap.allocation_token(id).is_some());
5794
5795        state.gc().full_collect();
5796        assert!(!state.global().interned_lt.contains_key(&payload[..]));
5797        assert_eq!(state.global().heap.allocation_token(id), None);
5798    }
5799
5800    #[test]
5801    fn interned_short_string_cache_keeps_reachable_string_until_unrooted() {
5802        let mut state = new_state().expect("state should initialize");
5803        let _heap_guard = {
5804            let g = state.global();
5805            lua_gc::HeapGuard::push(&g.heap)
5806        };
5807
5808        let payload = b"weak-cache-probe-b";
5809        let string = state
5810            .intern_str(payload)
5811            .expect("short string should intern");
5812        let id = string.identity();
5813        let key = state.external_root_value(LuaValue::Str(string));
5814
5815        state.gc().full_collect();
5816        assert!(state.global().interned_lt.contains_key(&payload[..]));
5817        assert!(state.global().heap.allocation_token(id).is_some());
5818
5819        assert!(state.external_unroot_value(key).is_some());
5820        state.gc().full_collect();
5821        assert!(!state.global().interned_lt.contains_key(&payload[..]));
5822        assert_eq!(state.global().heap.allocation_token(id), None);
5823    }
5824
5825    #[test]
5826    fn gc_phase_predicates_follow_heap_state() {
5827        let mut state = new_state().expect("state should initialize");
5828        let _heap_guard = {
5829            let g = state.global();
5830            lua_gc::HeapGuard::push(&g.heap)
5831        };
5832
5833        {
5834            let mut g = state.global_mut();
5835            g.gckind = GcKind::Incremental as u8;
5836            g.lastatomic = 0;
5837            assert!(!g.is_gen_mode());
5838            g.lastatomic = 1;
5839            assert!(g.is_gen_mode());
5840            g.lastatomic = 0;
5841        }
5842
5843        let mut roots = Vec::new();
5844        for _ in 0..16 {
5845            let table = state.new_table();
5846            roots.push(state.external_root_value(LuaValue::Table(table)));
5847        }
5848
5849        let mut saw_keep = false;
5850        let mut saw_sweep = false;
5851        for _ in 0..128 {
5852            state.gc().incremental_step(1);
5853            let g = state.global();
5854            let heap_state = g.heap.gc_state();
5855            assert_eq!(
5856                g.keep_invariant(),
5857                heap_state.is_invariant()
5858            );
5859            assert_eq!(g.is_sweep_phase(), heap_state.is_sweep());
5860            saw_keep |= g.keep_invariant();
5861            saw_sweep |= g.is_sweep_phase();
5862            if heap_state.is_pause() && saw_keep && saw_sweep {
5863                break;
5864            }
5865        }
5866
5867        assert!(saw_keep, "incremental cycle should expose an invariant phase");
5868        assert!(saw_sweep, "incremental cycle should expose a sweep phase");
5869
5870        for key in roots {
5871            assert!(state.external_unroot_value(key).is_some());
5872        }
5873        state.gc().full_collect();
5874    }
5875
5876    #[test]
5877    fn gc_barrier_keeps_new_child_stored_in_black_parent() {
5878        let mut state = new_state().expect("state should initialize");
5879        let _heap_guard = {
5880            let g = state.global();
5881            lua_gc::HeapGuard::push(&g.heap)
5882        };
5883
5884        let parent = state.new_table();
5885        let parent_key = state.external_root_value(LuaValue::Table(parent));
5886        state.gc().incremental_step(1);
5887        assert!(
5888            state.global().keep_invariant(),
5889            "test setup should leave the parent marked during an active cycle"
5890        );
5891
5892        let child = state.new_table();
5893        let parent_value = LuaValue::Table(parent);
5894        let child_value = LuaValue::Table(child);
5895        parent
5896            .raw_set_int(&mut state, 1, child_value)
5897            .expect("table store should succeed");
5898        state.gc_barrier_back(&parent_value, &child_value);
5899
5900        for _ in 0..128 {
5901            if state.gc().incremental_step(1) {
5902                break;
5903            }
5904        }
5905
5906        assert_eq!(state.global().heap.allgc_count(), 2);
5907        assert_eq!(
5908            parent.get_int(1).as_table().map(|t| t.identity()),
5909            Some(child.identity())
5910        );
5911
5912        assert!(state.external_unroot_value(parent_key).is_some());
5913        state.gc().full_collect();
5914        assert_eq!(state.global().heap.allgc_count(), 0);
5915    }
5916
5917    #[test]
5918    fn generational_mode_promotes_and_barriers_age_objects() {
5919        let mut state = new_state().expect("state should initialize");
5920        let _heap_guard = {
5921            let g = state.global();
5922            lua_gc::HeapGuard::push(&g.heap)
5923        };
5924
5925        let parent = state.new_table();
5926        let parent_key = state.external_root_value(LuaValue::Table(parent));
5927
5928        state.gc().change_mode(GcKind::Generational);
5929        assert_eq!(parent.0.age(), lua_gc::GcAge::Old);
5930        assert_eq!(parent.0.color(), lua_gc::Color::Black);
5931        let majorbase = state.global().gc_estimate;
5932        assert!(majorbase > 0);
5933        assert!(state.global().gc_debt() <= 0);
5934
5935        let child = state.new_table();
5936        let parent_value = LuaValue::Table(parent);
5937        let child_value = LuaValue::Table(child);
5938        parent
5939            .raw_set_int(&mut state, 1, child_value.clone())
5940            .expect("table store should succeed");
5941        state.gc_barrier_back(&parent_value, &child_value);
5942        assert_eq!(parent.0.age(), lua_gc::GcAge::Touched1);
5943        assert_eq!(parent.0.color(), lua_gc::Color::Gray);
5944        assert_eq!(child.0.age(), lua_gc::GcAge::New);
5945
5946        let metatable = state.new_table();
5947        parent.set_metatable(Some(metatable));
5948        state.gc().obj_barrier(&parent, &metatable);
5949        assert_eq!(metatable.0.age(), lua_gc::GcAge::Old0);
5950
5951        assert!(state.gc().generational_step_minor_only());
5952        assert_eq!(parent.0.age(), lua_gc::GcAge::Touched2);
5953        assert_eq!(child.0.age(), lua_gc::GcAge::Survival);
5954        assert_eq!(metatable.0.age(), lua_gc::GcAge::Old1);
5955        assert_eq!(state.global().gc_estimate, majorbase);
5956        assert!(state.global().gc_debt() <= 0);
5957
5958        state.gc().change_mode(GcKind::Incremental);
5959        assert_eq!(parent.0.age(), lua_gc::GcAge::New);
5960        assert_eq!(child.0.age(), lua_gc::GcAge::New);
5961        assert_eq!(metatable.0.age(), lua_gc::GcAge::New);
5962
5963        assert!(state.external_unroot_value(parent_key).is_some());
5964        state.gc().full_collect();
5965    }
5966
5967    #[test]
5968    fn generational_upvalue_write_barrier_marks_young_child_old0() {
5969        let mut state = new_state().expect("state should initialize");
5970        let _heap_guard = {
5971            let g = state.global();
5972            lua_gc::HeapGuard::push(&g.heap)
5973        };
5974
5975        let proto = state.new_proto();
5976        let closure = state.new_lclosure(proto, 1);
5977        let closure_key = state.external_root_value(LuaValue::Function(LuaClosure::Lua(closure)));
5978        state.gc().change_mode(GcKind::Generational);
5979        let uv = closure.upval(0);
5980        assert_eq!(uv.0.age(), lua_gc::GcAge::Old);
5981
5982        let child = state.new_table();
5983        state
5984            .upvalue_set(&closure, 0, LuaValue::Table(child))
5985            .expect("closed upvalue write should succeed");
5986        assert_eq!(child.0.age(), lua_gc::GcAge::Old0);
5987
5988        assert!(state.external_unroot_value(closure_key).is_some());
5989        state.gc().full_collect();
5990    }
5991
5992    #[test]
5993    fn cclosure_setupvalue_replaces_upvalue() {
5994        let mut state = new_state().expect("state should initialize");
5995        let _heap_guard = {
5996            let g = state.global();
5997            lua_gc::HeapGuard::push(&g.heap)
5998        };
5999
6000        let first = state.new_table();
6001        state.push(LuaValue::Table(first));
6002        crate::api::push_cclosure(&mut state, test_noop_cclosure, 1)
6003            .expect("C closure creation should succeed");
6004        let LuaValue::Function(LuaClosure::C(ccl)) = state.get_at(state.top_idx() - 1) else {
6005            panic!("expected heavy C closure");
6006        };
6007
6008        let second = state.new_table();
6009        state.push(LuaValue::Table(second));
6010        let name = crate::api::setup_value(&mut state, -2, 1)
6011            .expect("C closure upvalue should exist");
6012
6013        assert!(name.is_empty());
6014        let upvalues = ccl.upvalues.borrow();
6015        let LuaValue::Table(actual) = upvalues[0].clone() else {
6016            panic!("expected table upvalue");
6017        };
6018        assert_eq!(actual.identity(), second.identity());
6019    }
6020
6021    #[test]
6022    fn generational_cclosure_setupvalue_barrier_marks_young_child_old0() {
6023        let mut state = new_state().expect("state should initialize");
6024        let _heap_guard = {
6025            let g = state.global();
6026            lua_gc::HeapGuard::push(&g.heap)
6027        };
6028
6029        state.push(LuaValue::Nil);
6030        crate::api::push_cclosure(&mut state, test_noop_cclosure, 1)
6031            .expect("C closure creation should succeed");
6032        let LuaValue::Function(LuaClosure::C(ccl)) = state.get_at(state.top_idx() - 1) else {
6033            panic!("expected heavy C closure");
6034        };
6035        let closure_key = state.external_root_value(LuaValue::Function(LuaClosure::C(ccl)));
6036
6037        state.gc().change_mode(GcKind::Generational);
6038        assert_eq!(ccl.0.age(), lua_gc::GcAge::Old);
6039
6040        let child = state.new_table();
6041        state.push(LuaValue::Table(child));
6042        crate::api::setup_value(&mut state, -2, 1)
6043            .expect("C closure upvalue should exist");
6044
6045        assert_eq!(child.0.age(), lua_gc::GcAge::Old0);
6046
6047        assert!(state.external_unroot_value(closure_key).is_some());
6048        state.gc().full_collect();
6049    }
6050
6051    #[test]
6052    fn generational_closure_upvalue_slot_barrier_marks_new_upval_old0() {
6053        let mut state = new_state().expect("state should initialize");
6054        let _heap_guard = {
6055            let g = state.global();
6056            lua_gc::HeapGuard::push(&g.heap)
6057        };
6058
6059        let proto = state.new_proto();
6060        let closure = state.new_lclosure(proto, 1);
6061        let closure_key = state.external_root_value(LuaValue::Function(LuaClosure::Lua(closure)));
6062        state.gc().change_mode(GcKind::Generational);
6063        assert_eq!(closure.0.age(), lua_gc::GcAge::Old);
6064
6065        let replacement = state.new_upval_closed(LuaValue::Nil);
6066        closure.set_upval(0, replacement);
6067        state.gc().obj_barrier(&closure, &replacement);
6068        assert_eq!(replacement.0.age(), lua_gc::GcAge::Old0);
6069
6070        assert!(state.external_unroot_value(closure_key).is_some());
6071        state.gc().full_collect();
6072    }
6073
6074    #[test]
6075    fn cross_thread_upvalue_mirror_traces_values_as_roots() {
6076        let mut state = new_state().expect("state should initialize");
6077        let _heap_guard = {
6078            let g = state.global();
6079            lua_gc::HeapGuard::push(&g.heap)
6080        };
6081
6082        let mirrored = state.new_table();
6083        state
6084            .global_mut()
6085            .cross_thread_upvals
6086            .insert((999, StackIdx(0)), LuaValue::Table(mirrored));
6087
6088        state.gc().full_collect();
6089        assert_eq!(state.global().heap.allgc_count(), 1);
6090
6091        state.global_mut().cross_thread_upvals.clear();
6092        state.gc().full_collect();
6093        assert_eq!(state.global().heap.allgc_count(), 0);
6094    }
6095
6096    #[test]
6097    fn generational_full_collect_promotes_new_survivors_to_old() {
6098        let mut state = new_state().expect("state should initialize");
6099        let _heap_guard = {
6100            let g = state.global();
6101            lua_gc::HeapGuard::push(&g.heap)
6102        };
6103
6104        state.gc().change_mode(GcKind::Generational);
6105        let table = state.new_table();
6106        let table_key = state.external_root_value(LuaValue::Table(table));
6107        assert_eq!(table.0.age(), lua_gc::GcAge::New);
6108
6109        state.gc().full_collect();
6110        assert_eq!(table.0.age(), lua_gc::GcAge::Old);
6111        assert_eq!(table.0.color(), lua_gc::Color::Black);
6112
6113        assert!(state.external_unroot_value(table_key).is_some());
6114        state.gc().full_collect();
6115    }
6116
6117    #[test]
6118    fn gc_packed_params_return_user_visible_values() {
6119        let mut state = new_state().expect("state should initialize");
6120        assert_eq!(
6121            crate::api::gc(&mut state, crate::api::GcArgs::SetPause { value: 200 }),
6122            200
6123        );
6124        assert_eq!(state.global().gc_pause_param(), 200);
6125        assert_eq!(
6126            crate::api::gc(&mut state, crate::api::GcArgs::SetStepMul { value: 200 }),
6127            100
6128        );
6129        assert_eq!(state.global().gc_stepmul_param(), 200);
6130
6131        crate::api::gc(
6132            &mut state,
6133            crate::api::GcArgs::Gen {
6134                minormul: 0,
6135                majormul: 200,
6136            },
6137        );
6138        assert_eq!(state.global().gc_genmajormul_param(), 200);
6139    }
6140
6141    #[test]
6142    fn generational_step_runs_bad_major_when_growth_exceeds_genmajormul() {
6143        let mut state = new_state().expect("state should initialize");
6144        let _heap_guard = {
6145            let g = state.global();
6146            lua_gc::HeapGuard::push(&g.heap)
6147        };
6148
6149        let root = state.new_table();
6150        let root_key = state.external_root_value(LuaValue::Table(root));
6151        state.gc().change_mode(GcKind::Generational);
6152
6153        let root_value = LuaValue::Table(root);
6154        for i in 1..=64 {
6155            let child = state.new_table();
6156            let child_value = LuaValue::Table(child);
6157            root
6158                .raw_set_int(&mut state, i, child_value.clone())
6159                .expect("table store should succeed");
6160            state.gc_barrier_back(&root_value, &child_value);
6161        }
6162
6163        {
6164            let mut g = state.global_mut();
6165            g.gc_estimate = 1;
6166            set_debt(&mut *g, 1);
6167        }
6168
6169        assert!(state.gc().generational_step());
6170        let g = state.global();
6171        assert!(g.is_gen_mode());
6172        assert!(g.lastatomic > 0, "bad major collection should arm stepgenfull");
6173        assert!(g.gc_estimate > 1);
6174        assert!(g.gc_debt() <= 0);
6175        assert_eq!(root.0.age(), lua_gc::GcAge::Old);
6176        drop(g);
6177
6178        assert!(state.external_unroot_value(root_key).is_some());
6179        state.gc().full_collect();
6180    }
6181
6182    #[test]
6183    fn generational_implicit_step_runs_major_when_heap_threshold_exceeded() {
6184        let mut state = new_state().expect("state should initialize");
6185        let _heap_guard = {
6186            let g = state.global();
6187            lua_gc::HeapGuard::push(&g.heap)
6188        };
6189
6190        let root = state.new_table();
6191        let root_key = state.external_root_value(LuaValue::Table(root));
6192        state.gc().change_mode(GcKind::Generational);
6193
6194        let root_value = LuaValue::Table(root);
6195        for i in 1..=64 {
6196            let child = state.new_table();
6197            let child_value = LuaValue::Table(child);
6198            root
6199                .raw_set_int(&mut state, i, child_value.clone())
6200                .expect("table store should succeed");
6201            state.gc_barrier_back(&root_value, &child_value);
6202        }
6203
6204        {
6205            let mut g = state.global_mut();
6206            g.gc_estimate = 1;
6207            set_debt(&mut *g, -1);
6208            g.heap.set_threshold_bytes(1);
6209        }
6210
6211        assert!(state.gc().generational_step());
6212        let g = state.global();
6213        assert!(g.is_gen_mode());
6214        assert!(
6215            g.lastatomic > 0,
6216            "implicit threshold-triggered growth should arm a bad major"
6217        );
6218        assert!(g.gc_debt() <= 0);
6219        drop(g);
6220
6221        assert!(state.external_unroot_value(root_key).is_some());
6222        state.gc().full_collect();
6223    }
6224
6225    #[test]
6226    fn generational_stepgenfull_returns_to_gen_after_good_collection() {
6227        let mut state = new_state().expect("state should initialize");
6228        let _heap_guard = {
6229            let g = state.global();
6230            lua_gc::HeapGuard::push(&g.heap)
6231        };
6232
6233        let root = state.new_table();
6234        let root_key = state.external_root_value(LuaValue::Table(root));
6235        state.gc().change_mode(GcKind::Generational);
6236        {
6237            let mut g = state.global_mut();
6238            g.lastatomic = 1024;
6239        }
6240
6241        assert!(state.gc().generational_step());
6242        let g = state.global();
6243        assert_eq!(g.gckind, GcKind::Generational as u8);
6244        assert_eq!(g.lastatomic, 0);
6245        assert!(g.gc_debt() <= 0);
6246        assert_eq!(root.0.age(), lua_gc::GcAge::Old);
6247        assert_eq!(root.0.color(), lua_gc::Color::Black);
6248        drop(g);
6249
6250        assert!(state.external_unroot_value(root_key).is_some());
6251        state.gc().full_collect();
6252    }
6253
6254    #[test]
6255    fn generational_step_zero_reports_false_without_positive_debt() {
6256        let mut state = new_state().expect("state should initialize");
6257        let _heap_guard = {
6258            let g = state.global();
6259            lua_gc::HeapGuard::push(&g.heap)
6260        };
6261
6262        state.gc().change_mode(GcKind::Generational);
6263        assert_eq!(
6264            crate::api::gc(&mut state, crate::api::GcArgs::Step { data: 0 }),
6265            0
6266        );
6267        assert_eq!(
6268            crate::api::gc(&mut state, crate::api::GcArgs::Step { data: 1 }),
6269            1
6270        );
6271    }
6272}
6273
6274// ──────────────────────────────────────────────────────────────────────────────
6275// PORT STATUS
6276//   source:        src/lstate.c  (445 lines, 25 functions)
6277//                  src/lstate.h  (408 lines; struct definitions merged)
6278//   target_crate:  lua-vm
6279//   confidence:    medium
6280//   todos:         44
6281//   port_notes:    34
6282//   unsafe_blocks: 0   (must be 0 outside explicit unsafe-budget crates)
6283//   notes:         Logic faithfully follows lstate.c. Key structural changes:
6284//                  (1) LX/LG C layout wrappers dropped; GlobalState is Rc<RefCell<>>.
6285//                  (2) CallInfo linked list → Vec<CallInfo> with CallInfoIdx indices;
6286//                      shrink_ci uses truncation rather than node-by-node removal.
6287//                  (3) lua_State.twups self-reference → membership in GlobalState.twups Vec.
6288//                  (4) errorJmp/setjmp → removed; errors use Result<T, LuaError>.
6289//                  (5) Custom allocator (lua_Alloc) → dropped; Rust's allocator handles it.
6290//                  (6) make_seed: ASLR pointer entropy requires unsafe; time-only for Phase A.
6291//                  (7) Perf: LuaState.cached_thread_id stores the thread's own id once at
6292//                      construction; upvalue_get/_set compare against this u64 field
6293//                      instead of borrowing global.current_thread_id on every read.
6294//                      Invariant survives coroutine resume because each thread caches its
6295//                      OWN id, not the global's id (see field doc on cached_thread_id).
6296//                  (8) Perf: LuaTableRefExt::{raw_set, raw_set_int, get, get_int,
6297//                      get_short_str, metatable, as_ptr} and table_{raw,set_with_tm,
6298//                      array_set} carry #[inline] so the per-set dispatch chain
6299//                      collapses into set_i_value / vm.rs OP_SETI callers. The
6300//                      historical reject_invalid_table_key precheck moved into
6301//                      LuaTable::try_raw_set (lua-types) and was dropped at this
6302//                      layer; raw_set now takes the key by value, eliminating a
6303//                      LuaValue clone per set. gc_barrier_back is invoked
6304//                      before the store in table_set_with_tm (semantically
6305//                      equivalent: the barrier only inspects the value's color,
6306//                      not its location), letting v be moved directly into
6307//                      table_raw_set without an intermediate clone.
6308//                  Key TODOs: luaT_init and luaX_init cross-crate calls (Phase B);
6309//                  init_registry table mutations through Rc (needs RefCell<LuaTable>);
6310//                  luaD_closeprotected/seterrorobj/reallocstack in reset_thread (ldo.c);
6311//                  GcRef<LuaState> self-reference for mainthread (Phase D);
6312//                  LuaString::placeholder() helper needed for GlobalState init;
6313//                  LuaValue and LuaTable should move to object.rs once that lands.
6314// ──────────────────────────────────────────────────────────────────────────────