Skip to main content

lua_vm/
state.rs

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