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