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// C: #define lstate_c
13// C: #define LUA_CORE
14
15// PORT NOTE: The C `LX` (thread + extra space) and `LG` (LX + global state) layout
16// wrappers are C-only pointer-arithmetic helpers for allocating the main thread and
17// GlobalState as one contiguous block. In Rust, `GlobalState` and `LuaState` are
18// separate heap-allocated values linked via `Rc<RefCell<GlobalState>>`. No LX/LG
19// equivalents are needed.
20
21// PORT NOTE: C macro `fromstate(L)` (cast LX* from lua_State*) is C-only pointer
22// arithmetic and is not translated. Rust owns the allocations via Rc/Box.
23
24use std::cell::RefCell;
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 { i.0 as i32 }
41
42impl From<u32> for StackIdxConv {
43    #[inline(always)]
44    fn from(v: u32) -> Self { StackIdxConv(StackIdx(v)) }
45}
46impl From<i32> for StackIdxConv {
47    #[inline(always)]
48    fn from(v: i32) -> Self { StackIdxConv(StackIdx(v.max(0) as u32)) }
49}
50impl From<usize> for StackIdxConv {
51    #[inline(always)]
52    fn from(v: usize) -> Self { StackIdxConv(StackIdx(v as u32)) }
53}
54impl From<StackIdx> for StackIdxConv {
55    #[inline(always)]
56    fn from(v: StackIdx) -> Self { StackIdxConv(v) }
57}
58pub use lua_types::value::{LuaTable, LuaValue, F2Imod};
59pub use lua_types::string::LuaString;
60pub use lua_types::userdata::LuaUserData;
61pub use lua_types::closure::{LuaCFnPtr, LuaClosure, LuaLClosure as LuaClosureLua, LuaCClosure as LuaClosureC};
62pub use lua_types::proto::LuaProto;
63pub use lua_types::upval::{UpVal, UpValState};
64pub use lua_types::gc::GcRef;
65
66/// A Lua-callable function pointer. C: `lua_CFunction`.
67///
68/// TODO(phase-b): the lua-types crate uses a placeholder
69/// `LuaCFnPtr = fn() -> i32` since it can't reference `LuaState` without a
70/// circular dep. The real signature is `fn(&mut LuaState) -> Result<usize, LuaError>`,
71/// kept here as the lua-vm-facing type alias.
72pub type LuaCFunction = fn(&mut LuaState) -> Result<usize, LuaError>;
73
74// ─── Constants (from macros.tsv) ──────────────────────────────────────────────
75
76// C: #define EXTRA_STACK 5  (lstate.h)
77// macros.tsv: EXTRA_STACK → const EXTRA_STACK: u32 = 5
78pub(crate) const EXTRA_STACK: usize = 5;
79
80// C: LUA_MINSTACK = 20  (lua.h)
81// macros.tsv: LUA_MINSTACK → const LUA_MINSTACK: u32 = 20
82pub(crate) const LUA_MINSTACK: usize = 20;
83
84// C: #define BASIC_STACK_SIZE (2 * LUA_MINSTACK)  (lstate.h)
85// macros.tsv: BASIC_STACK_SIZE → const BASIC_STACK_SIZE: u32 = 2 * LUA_MINSTACK
86pub(crate) const BASIC_STACK_SIZE: usize = 2 * LUA_MINSTACK;
87
88// C: LUAI_MAXCCALLS = 200  (luaconf.h)
89// PORT NOTE: lowered from 200 to 80 because our debug-build Rust frames
90// are ~5–10× larger than C frames (debuginfo, stack-allocated CallInfo
91// arrays, marker state). At 200 we SIGSEGV on cstack's 1000-coroutine
92// close cascade before nCcalls trips. 80 is safe for an 8 MB Rust thread
93// stack with a comfortable margin.
94pub(crate) const LUAI_MAXCCALLS: u32 = 200;
95
96// C: #define CIST_C (1 << 1)  (lstate.h)
97// macros.tsv: CIST_C → const CIST_C: u16 = 1 << 1
98pub(crate) const CIST_C: u16 = 1 << 1;
99
100// Remaining CIST_* bits from macros.tsv
101pub(crate) const CIST_OAH: u16 = 1 << 0;
102pub(crate) const CIST_FRESH: u16 = 1 << 2;
103pub(crate) const CIST_HOOKED: u16 = 1 << 3;
104pub(crate) const CIST_YPCALL: u16 = 1 << 4;
105pub(crate) const CIST_TAIL: u16 = 1 << 5;
106pub(crate) const CIST_HOOKYIELD: u16 = 1 << 6;
107pub(crate) const CIST_FIN: u16 = 1 << 7;
108pub(crate) const CIST_TRAN: u16 = 1 << 8;
109pub(crate) const CIST_CLSRET: u16 = 1 << 9;
110pub(crate) const CIST_RECST: u32 = 10;
111
112// C: LUA_RIDX_MAINTHREAD = 1, LUA_RIDX_GLOBALS = 2  (lua.h)
113// macros.tsv: LUA_RIDX_MAINTHREAD → const LUA_RIDX_MAINTHREAD: i64 = 1
114pub(crate) const LUA_RIDX_MAINTHREAD: i64 = 1;
115pub(crate) const LUA_RIDX_GLOBALS: i64 = 2;
116// C: LUA_RIDX_LAST = LUA_RIDX_GLOBALS = 2
117pub(crate) const LUA_RIDX_LAST: usize = 2;
118
119// C: LUA_NUMTAGS = 9  (lua.h)
120// macros.tsv: LUA_NUMTYPES → const LUA_NUMTYPES: usize = 9
121const LUA_NUMTYPES: usize = 9;
122
123// C: LUA_EXTRASPACE  (lua.h) — sizeof(void *) on most platforms
124const LUA_EXTRASPACE: usize = std::mem::size_of::<*mut ()>();
125
126// C: GCSTPGC — GC stopped for state building (lgc.h constant)
127// TODO(port): import from crate::gc (lgc.c → gc.rs) once it exists in Phase D
128const GCSTPUSR: u8 = 1;
129const GCSTPGC: u8 = 2;
130
131// C: GCSpause (lgc.h) — initial GC state
132// TODO(port): import from crate::gc in Phase D
133const GCS_PAUSE: u8 = 0;
134
135// C: LUAI_GCPAUSE, LUAI_GCMUL, LUAI_GCSTEPSIZE, LUAI_GENMAJORMUL, LUAI_GENMINORMUL (luaconf.h)
136const LUAI_GCPAUSE: u32 = 200;
137const LUAI_GCMUL: u32 = 100;
138const LUAI_GCSTEPSIZE: u8 = 13;
139const LUAI_GENMAJORMUL: u32 = 100;
140const LUAI_GENMINORMUL: u8 = 20;
141
142// C: WHITE0BIT = 0  (lgc.h)
143const WHITE0BIT: u8 = 0;
144
145// C: STRCACHE_N, STRCACHE_M  (llimits.h)
146const STRCACHE_N: usize = 53;
147const STRCACHE_M: usize = 2;
148
149// ─── GcKind enum ─────────────────────────────────────────────────────────────
150
151/// Garbage collector operating mode.
152///
153/// C: `KGC_INC` / `KGC_GEN` constants in `lstate.h`.
154/// macros.tsv: `KGC_INC → GcKind::Incremental`, `KGC_GEN → GcKind::Generational`
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum GcKind {
157    // C: KGC_INC = 0
158    Incremental = 0,
159    // C: KGC_GEN = 1
160    Generational = 1,
161}
162
163// ─── LuaStatus enum ──────────────────────────────────────────────────────────
164
165/// Thread / call status codes.
166///
167/// C: `LUA_OK`, `LUA_YIELD`, `LUA_ERRRUN`, … constants in `lua.h`.
168pub use lua_types::status::LuaStatus;
169
170// ─── StackValue ───────────────────────────────────────────────────────────────
171
172/// One slot on the Lua value stack.  Wraps a `LuaValue` and an optional
173/// to-be-closed delta (for the `tbclist` mechanism).
174///
175/// C: `StackValue` in `lobject.h`.
176/// types.tsv: `StackValue → StackValue { val: LuaValue, tbclist.delta: u16 }`
177#[derive(Clone)]
178pub struct StackValue {
179    // C: StackValue.val — the payload TValue
180    pub val: LuaValue,
181    // C: StackValue.tbclist.delta — to-be-closed linked-list delta
182    pub tbc_delta: u16,
183}
184
185impl Default for StackValue {
186    fn default() -> Self {
187        StackValue {
188            val: LuaValue::Nil,
189            tbc_delta: 0,
190        }
191    }
192}
193
194// ─── CallInfo ────────────────────────────────────────────────────────────────
195
196/// Saved state for a Lua or C call frame.
197///
198/// C: `struct CallInfo` in `lstate.h`.
199/// types.tsv: CallInfo → CallInfo (several fields renamed / adapted).
200///
201/// The C intrusive doubly-linked list (`previous`, `next` as raw pointers) is
202/// replaced by `Option<CallInfoIdx>` indices into `LuaState::call_info`.
203#[derive(Clone)]
204pub struct CallInfo {
205    // C: StkIdRel func — stack index of the called function value
206    // types.tsv: CallInfo.func → StackIdx
207    pub func: StackIdx,
208
209    // C: StkIdRel top — stack-top reservation for this call
210    // types.tsv: CallInfo.top → StackIdx
211    pub top: StackIdx,
212
213    // C: struct CallInfo *previous
214    // types.tsv: CallInfo.previous → CallInfoIdx (Option at boundary)
215    pub previous: Option<CallInfoIdx>,
216
217    // C: struct CallInfo *next
218    // types.tsv: CallInfo.next → CallInfoIdx (Option at tail)
219    pub next: Option<CallInfoIdx>,
220
221    // C: union { struct { savedpc, trap, nextraargs } l; struct { k, old_errfunc, ctx } c; } u
222    pub u: CallInfoFrame,
223
224    // C: union { funcidx, nyield, nres, transferinfo } u2
225    pub u2: CallInfoExtra,
226
227    // C: short nresults
228    // types.tsv: CallInfo.nresults → i16
229    pub nresults: i16,
230
231    // C: unsigned short callstatus
232    // types.tsv: CallInfo.callstatus → u16 (bit-packed CIST_* flags)
233    pub callstatus: u16,
234}
235
236/// Payload of `CallInfo.u`.
237///
238/// C: `union { struct l { savedpc, trap, nextraargs }; struct c { k, old_errfunc, ctx } } u`
239#[derive(Clone, Copy)]
240pub enum CallInfoFrame {
241    // C: ci->u.l — Lua function call
242    Lua {
243        // C: const Instruction *savedpc → u32 offset into Proto.code
244        // types.tsv: CallInfo.u.l.savedpc → u32
245        savedpc: u32,
246        // C: volatile l_signalT trap
247        // types.tsv: CallInfo.u.l.trap → bool
248        trap: bool,
249        // C: int nextraargs
250        // types.tsv: CallInfo.u.l.nextraargs → i32
251        nextraargs: i32,
252    },
253    // C: ci->u.c — C function call
254    C {
255        // C: lua_KFunction k — continuation for yields
256        // types.tsv: CallInfo.u.c.k → Option<lua_KFunction>
257        k: Option<LuaKFunction>,
258        // C: ptrdiff_t old_errfunc
259        // types.tsv: CallInfo.u.c.old_errfunc → isize
260        old_errfunc: isize,
261        // C: lua_KContext ctx
262        // types.tsv: CallInfo.u.c.ctx → isize
263        ctx: isize,
264    },
265}
266
267/// Continuation function for yieldable C calls.  C: `lua_KFunction`.
268pub type LuaKFunction = fn(&mut LuaState, status: i32, ctx: isize) -> Result<usize, LuaError>;
269
270/// Payload of `CallInfo.u2`.
271///
272/// C: `union { funcidx, nyield, nres, transferinfo } u2`
273/// types.tsv: CallInfo.u2 → CallInfoExtra (Rust: struct with all fields, interpretation by context)
274#[derive(Default, Clone, Copy)]
275pub struct CallInfoExtra {
276    // C: int funcidx / nyield / nres — overloaded single int field
277    pub value: i32,
278    // C: struct transferinfo { unsigned short ftransfer, ntransfer }
279    pub ftransfer: u16,
280    pub ntransfer: u16,
281}
282
283impl CallInfoFrame {
284    /// Default C-call frame (no continuation, zero context).
285    pub fn c_default() -> Self {
286        CallInfoFrame::C {
287            k: None,
288            old_errfunc: 0,
289            ctx: 0,
290        }
291    }
292
293    /// Default Lua-call frame (pc=0, no trap, no extra args).
294    pub fn lua_default() -> Self {
295        CallInfoFrame::Lua {
296            savedpc: 0,
297            trap: false,
298            nextraargs: 0,
299        }
300    }
301}
302
303impl Default for CallInfo {
304    fn default() -> Self {
305        CallInfo {
306            func: StackIdx(0),
307            top: StackIdx(0),
308            previous: None,
309            next: None,
310            u: CallInfoFrame::c_default(),
311            u2: CallInfoExtra::default(),
312            nresults: 0,
313            callstatus: 0,
314        }
315    }
316}
317
318impl CallInfo {
319    pub fn is_lua(&self) -> bool { (self.callstatus & CIST_C) == 0 }
320    pub fn is_lua_code(&self) -> bool { self.is_lua() }
321    /// Whether the active function is a vararg function.
322    ///
323    /// Currently returns `false` unconditionally — vararg introspection via
324    /// `debug.getinfo` reports no vararg info instead of panicking.
325    ///
326    /// TODO(port): wire when CallInfo carries proto access for vararg detection.
327    pub fn is_vararg_func(&self) -> bool { false }
328    pub fn saved_pc(&self) -> u32 {
329        if let CallInfoFrame::Lua { savedpc, .. } = self.u { savedpc } else { 0 }
330    }
331    pub fn set_saved_pc(&mut self, pc: u32) {
332        if let CallInfoFrame::Lua { ref mut savedpc, .. } = self.u { *savedpc = pc; }
333    }
334    pub fn nextra_args(&self) -> i32 {
335        if let CallInfoFrame::Lua { nextraargs, .. } = self.u { nextraargs } else { 0 }
336    }
337    pub fn transfer_ftransfer(&self) -> u16 { self.u2.ftransfer }
338    pub fn transfer_ntransfer(&self) -> u16 { self.u2.ntransfer }
339    pub fn set_trap(&mut self, t: bool) {
340        if let CallInfoFrame::Lua { ref mut trap, .. } = self.u { *trap = t; }
341    }
342    /// Read the 3-bit recover-status field packed into bits 10-12 of callstatus.
343    ///
344    /// C: `#define getcistrecst(ci) (((ci)->callstatus >> CIST_RECST) & 7)`
345    pub fn recover_status(&self) -> i32 {
346        ((self.callstatus >> CIST_RECST) & 7) as i32
347    }
348    /// Write the 3-bit recover-status field. `status` must fit in three bits.
349    ///
350    /// C: `#define setcistrecst(ci,st)` (lstate.h)
351    pub fn set_recover_status<T: Into<i32>>(&mut self, status: T) {
352        let st = (status.into() & 7) as u16;
353        self.callstatus = (self.callstatus & !(7u16 << CIST_RECST)) | (st << CIST_RECST);
354    }
355    pub fn get_oah(&self) -> bool { (self.callstatus & CIST_OAH) != 0 }
356    /// Store the current `allowhook` value into callstatus bit 0 (CIST_OAH).
357    ///
358    /// C: `#define setoah(st,v) ((st) = ((st) & ~CIST_OAH) | (v))`
359    pub fn set_oah(&mut self, allow: bool) {
360        self.callstatus = (self.callstatus & !CIST_OAH) | (if allow { CIST_OAH } else { 0 });
361    }
362    pub fn u_c_old_errfunc(&self) -> isize {
363        if let CallInfoFrame::C { old_errfunc, .. } = self.u { old_errfunc } else { 0 }
364    }
365    pub fn u_c_ctx(&self) -> isize {
366        if let CallInfoFrame::C { ctx, .. } = self.u { ctx } else { 0 }
367    }
368    pub fn u_c_k(&self) -> Option<LuaKFunction> {
369        if let CallInfoFrame::C { k, .. } = self.u { k } else { None }
370    }
371    /// Set continuation function on a C-call frame.
372    ///
373    /// Panics if invoked on a Lua frame (callers must check `is_lua()` first).
374    pub fn set_u_c_k(&mut self, k: Option<LuaKFunction>) {
375        if let CallInfoFrame::C { k: ref mut slot, .. } = self.u {
376            *slot = k;
377        }
378    }
379    /// Set continuation context on a C-call frame.
380    pub fn set_u_c_ctx(&mut self, ctx: isize) {
381        if let CallInfoFrame::C { ctx: ref mut slot, .. } = self.u {
382            *slot = ctx;
383        }
384    }
385    /// Set saved old_errfunc on a C-call frame.
386    pub fn set_u_c_old_errfunc(&mut self, old_errfunc: isize) {
387        if let CallInfoFrame::C { old_errfunc: ref mut slot, .. } = self.u {
388            *slot = old_errfunc;
389        }
390    }
391    /// Set the `u2.funcidx` field, used by yieldable pcall for error recovery.
392    ///
393    /// C: `ci->u2.funcidx = cast_int(savestack(L, c.func))`
394    pub fn set_u2_funcidx(&mut self, idx: i32) {
395        self.u2.value = idx;
396    }
397}
398
399// ─── Phase-B value/proto/instruction helpers ──────────────────────────────────
400
401/// Extension methods on `LuaValue`. TODO(phase-b): move these to
402/// `lua_types::value` (or wherever the canonical impl lives) once the type
403/// helpers stabilise.
404pub trait LuaValueExt {
405    fn base_type(&self) -> lua_types::LuaType;
406    fn to_number_no_strconv(&self) -> Option<f64>;
407    fn to_number_with_strconv(&self) -> Option<f64>;
408    fn to_integer_no_strconv(&self) -> Option<i64>;
409    fn to_integer_with_strconv(&self) -> Option<i64>;
410    fn full_type_tag(&self) -> u8;
411}
412
413impl LuaValueExt for LuaValue {
414    fn base_type(&self) -> lua_types::LuaType { self.type_tag() }
415    fn to_number_no_strconv(&self) -> Option<f64> {
416        match self {
417            LuaValue::Float(f) => Some(*f),
418            LuaValue::Int(i) => Some(*i as f64),
419            _ => None,
420        }
421    }
422    fn to_number_with_strconv(&self) -> Option<f64> {
423        if let Some(n) = self.to_number_no_strconv() { return Some(n); }
424        if let LuaValue::Str(s) = self {
425            let mut tmp = LuaValue::Nil;
426            let sz = crate::object::str2num(s.as_bytes(), &mut tmp);
427            if sz == 0 { return None; }
428            return match tmp {
429                LuaValue::Int(i) => Some(i as f64),
430                LuaValue::Float(f) => Some(f),
431                _ => None,
432            };
433        }
434        None
435    }
436    fn to_integer_no_strconv(&self) -> Option<i64> {
437        match self {
438            LuaValue::Int(i) => Some(*i),
439            LuaValue::Float(f) if f.fract() == 0.0 && f.is_finite() => {
440                // C: lua_numbertointeger range check —
441                //   d >= LUA_MININTEGER && d < -(lua_Number)LUA_MININTEGER.
442                // Without this, Rust's `as i64` saturates and silently
443                // produces i64::MAX / i64::MIN for out-of-range floats.
444                let min_f = i64::MIN as f64;
445                let max_plus1_f = -(i64::MIN as f64);
446                if *f >= min_f && *f < max_plus1_f {
447                    Some(*f as i64)
448                } else {
449                    None
450                }
451            }
452            _ => None,
453        }
454    }
455    fn to_integer_with_strconv(&self) -> Option<i64> {
456        if let Some(i) = self.to_integer_no_strconv() { return Some(i); }
457        if let LuaValue::Str(s) = self {
458            let mut tmp = LuaValue::Nil;
459            let sz = crate::object::str2num(s.as_bytes(), &mut tmp);
460            if sz == 0 { return None; }
461            return tmp.to_integer_no_strconv();
462        }
463        None
464    }
465    fn full_type_tag(&self) -> u8 {
466        match self {
467            LuaValue::Nil => 0x00,
468            LuaValue::Bool(false) => 0x01,
469            LuaValue::Bool(true) => 0x11,
470            LuaValue::Int(_) => 0x03,
471            LuaValue::Float(_) => 0x13,
472            LuaValue::Str(s) if s.is_short() => 0x04,
473            LuaValue::Str(_) => 0x14,
474            LuaValue::LightUserData(_) => 0x02,
475            LuaValue::Table(_) => 0x05,
476            LuaValue::Function(LuaClosure::Lua(_)) => 0x06,
477            LuaValue::Function(LuaClosure::LightC(_)) => 0x16,
478            LuaValue::Function(LuaClosure::C(_)) => 0x26,
479            LuaValue::UserData(_) => 0x07,
480            LuaValue::Thread(_) => 0x08,
481        }
482    }
483}
484
485/// Extension methods on `lua_types::LuaType`.
486pub trait LuaTypeExt {
487    fn type_name(&self) -> &'static [u8];
488}
489
490impl LuaTypeExt for lua_types::LuaType {
491    fn type_name(&self) -> &'static [u8] {
492        use lua_types::LuaType::*;
493        match self {
494            None => b"no value",
495            Nil => b"nil",
496            Boolean => b"boolean",
497            LightUserData => b"userdata",
498            Number => b"number",
499            String => b"string",
500            Table => b"table",
501            Function => b"function",
502            UserData => b"userdata",
503            Thread => b"thread",
504        }
505    }
506}
507
508/// StackIdx checked-arithmetic helpers. Returns the raw `u32` because Phase A
509/// callers use the result in arithmetic comparisons against other `u32`
510/// quantities (stack-distance offsets).
511pub trait StackIdxExt {
512    fn saturating_sub(self, n: impl Into<StackIdxConv>) -> u32;
513    fn wrapping_sub(self, n: impl Into<StackIdxConv>) -> u32;
514    fn raw(self) -> u32;
515}
516impl StackIdxExt for StackIdx {
517    #[inline(always)]
518    fn saturating_sub(self, n: impl Into<StackIdxConv>) -> u32 { self.0.saturating_sub(n.into().0.0) }
519    #[inline(always)]
520    fn wrapping_sub(self, n: impl Into<StackIdxConv>) -> u32 { self.0.wrapping_sub(n.into().0.0) }
521    #[inline(always)]
522    fn raw(self) -> u32 { self.0 }
523}
524
525/// `GcRef<LuaTable>` / `GcRef<LuaUserData>` field-access helpers. These
526/// methods are needed by api.rs and tagmethods.rs but the lua-types
527/// placeholders don't yet expose them. TODO(phase-b): replace with real
528/// accessor methods on the canonical types in lua-types.
529///
530/// PORT NOTE: the historical `reject_invalid_table_key` precheck used to
531/// guard nil/NaN keys at this layer; it has moved inside
532/// [`LuaTable::try_raw_set`] (alongside the integer-fast-path match) so
533/// the lua-vm wrapper does not double-check.
534pub trait LuaTableRefExt {
535    fn metatable(&self) -> Option<GcRef<LuaTable>>;
536    fn as_ptr(&self) -> *const ();
537    fn get(&self, _k: &LuaValue) -> LuaValue;
538    fn get_int(&self, _k: i64) -> LuaValue;
539    fn get_short_str(&self, _k: &GcRef<LuaString>) -> LuaValue;
540    fn raw_set(&self, _state: &mut LuaState, _k: LuaValue, _v: LuaValue) -> Result<(), LuaError>;
541    fn raw_set_int(&self, _state: &mut LuaState, _k: i64, _v: LuaValue) -> Result<(), LuaError>;
542    fn invalidate_tm_cache(&self);
543    fn resize(&self, _state: &mut LuaState, _na: usize, _nh: usize) -> Result<(), LuaError>;
544    fn next(&self, _k: LuaValue) -> Result<Option<(LuaValue, LuaValue)>, LuaError>;
545}
546impl LuaTableRefExt for GcRef<LuaTable> {
547    #[inline]
548    fn metatable(&self) -> Option<GcRef<LuaTable>> { (**self).metatable() }
549    #[inline]
550    fn as_ptr(&self) -> *const () { GcRef::identity(self) as *const () }
551    #[inline]
552    fn get(&self, k: &LuaValue) -> LuaValue { (**self).get(k) }
553    #[inline]
554    fn get_int(&self, k: i64) -> LuaValue { (**self).get_int(k) }
555    #[inline]
556    fn get_short_str(&self, k: &GcRef<LuaString>) -> LuaValue { (**self).get_short_str(k) }
557    /// Forwards to [`LuaTable::try_raw_set`], which performs the nil/NaN
558    /// key validation internally as part of its integer-fast-path match.
559    #[inline]
560    fn raw_set(&self, _state: &mut LuaState, k: LuaValue, v: LuaValue) -> Result<(), LuaError> {
561        (**self).try_raw_set(k, v)
562    }
563    #[inline]
564    fn raw_set_int(&self, _state: &mut LuaState, k: i64, v: LuaValue) -> Result<(), LuaError> {
565        (**self).try_raw_set_int(k, v)
566    }
567    fn invalidate_tm_cache(&self) {}
568    fn resize(&self, _state: &mut LuaState, na: usize, nh: usize) -> Result<(), LuaError> {
569        let na32 = na.min(u32::MAX as usize) as u32;
570        let nh32 = nh.min(u32::MAX as usize) as u32;
571        (**self).resize(na32, nh32)
572    }
573    fn next(&self, k: LuaValue) -> Result<Option<(LuaValue, LuaValue)>, LuaError> {
574        (**self).try_next_pair(&k)
575    }
576}
577
578pub trait LuaUserDataRefExt {
579    fn metatable(&self) -> Option<GcRef<LuaTable>>;
580    fn set_metatable(&self, mt: Option<GcRef<LuaTable>>);
581    fn as_ptr(&self) -> *const ();
582    fn len(&self) -> usize;
583}
584impl LuaUserDataRefExt for GcRef<LuaUserData> {
585    fn metatable(&self) -> Option<GcRef<LuaTable>> { (**self).metatable() }
586    fn set_metatable(&self, mt: Option<GcRef<LuaTable>>) { (**self).set_metatable(mt); }
587    fn as_ptr(&self) -> *const () { GcRef::identity(self) as *const () }
588    fn len(&self) -> usize { self.0.data.len() }
589}
590
591pub trait LuaStringRefExt {
592    fn is_white(&self) -> bool;
593    fn hash(&self) -> u32;
594    fn as_gc_ref(&self) -> GcRef<LuaString>;
595}
596impl LuaStringRefExt for GcRef<LuaString> {
597    fn is_white(&self) -> bool { false }
598    fn hash(&self) -> u32 { self.0.hash() }
599    fn as_gc_ref(&self) -> GcRef<LuaString> { self.clone() }
600}
601
602pub trait LuaLClosureRefExt {
603    fn proto(&self) -> &GcRef<LuaProto>;
604    fn nupvalues(&self) -> usize;
605}
606impl LuaLClosureRefExt for GcRef<lua_types::closure::LuaLClosure> {
607    fn proto(&self) -> &GcRef<LuaProto> { &self.0.proto }
608    fn nupvalues(&self) -> usize { self.0.upvals.len() }
609}
610
611/// `LuaClosure` accessor — `nupvalues()` reports the upvalue count uniformly.
612pub trait LuaClosureExt {
613    fn nupvalues(&self) -> usize;
614}
615impl LuaClosureExt for LuaClosure {
616    fn nupvalues(&self) -> usize {
617        match self {
618            LuaClosure::Lua(l) => l.0.upvals.len(),
619            LuaClosure::C(c) => c.0.upvalues.len(),
620            LuaClosure::LightC(_) => 0,
621        }
622    }
623}
624
625/// `LuaProto` source bytes accessor.
626pub trait LuaProtoExt {
627    fn source_bytes(&self) -> &[u8];
628    fn source_string(&self) -> Option<&GcRef<LuaString>>;
629}
630impl LuaProtoExt for LuaProto {
631    fn source_bytes(&self) -> &[u8] {
632        match &self.source { Some(s) => s.0.as_bytes(), None => &[] }
633    }
634    fn source_string(&self) -> Option<&GcRef<LuaString>> { self.source.as_ref() }
635}
636
637// ─── Collectable trait (GC interface) ────────────────────────────────────────
638
639/// Marker trait for GC-managed objects.
640///
641/// C: `GCObject` in `lobject.h` / `lstate.h`. Phase A–C: objects are Rc-tracked;
642/// Phase D: real tracing GC.
643/// types.tsv: `GCObject → (trait Collectable; concrete = GcRef<T>)`
644pub trait Collectable: std::fmt::Debug {}
645
646impl std::fmt::Debug for LuaState {
647    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
648        write!(f, "LuaState")
649    }
650}
651impl Collectable for LuaState {}
652
653// ─── GlobalState ─────────────────────────────────────────────────────────────
654
655/// Function-pointer signature for the text-source parser, installed on
656/// [`GlobalState::parser_hook`] by the embedder.
657///
658/// The implementation lives in `lua-parse`; `lua-vm` cannot depend on it
659/// directly (that would form a cycle), so the parser is reached via this
660/// function pointer registered at startup.
661pub type ParserHook = fn(
662    state: &mut LuaState,
663    source: &[u8],
664    name: &[u8],
665    firstchar: i32,
666) -> Result<GcRef<lua_types::closure::LuaLClosure>, LuaError>;
667
668/// Function-pointer signature for reading a file's full contents into memory,
669/// installed on [`GlobalState::file_loader_hook`] by the embedder.
670///
671/// `std::fs` is banned outside `lua-cli`, so `lua-stdlib`'s `loadfile` and
672/// `searcher_lua` reach the filesystem via this hook. `None` keeps the file
673/// system unreachable, which is appropriate for embeddings where modules are
674/// served exclusively from `package.preload`.
675pub type FileLoaderHook = fn(filename: &[u8]) -> Result<Vec<u8>, LuaError>;
676
677/// Function-pointer signature for opening a file handle, installed on
678/// [`GlobalState::file_open_hook`] by the embedder.
679///
680/// `std::fs` is banned outside `lua-cli`, so `lua-stdlib`'s io library reaches
681/// the filesystem via this hook. `None` causes `io.open` and `io.output(name)`
682/// to return a "file system not available" error, which is appropriate for
683/// sandboxed embeddings.
684///
685/// `mode` is a Lua fopen-style mode string (e.g. `b"r"`, `b"w"`, `b"a"`,
686/// `b"r+"`, etc.). The hook must honour at least `r`, `w`, and `a`.
687pub type FileOpenHook =
688    fn(filename: &[u8], mode: &[u8]) -> Result<Box<dyn lua_types::LuaFileHandle>, LuaError>;
689
690/// Function-pointer signature for spawning a child process with a connected
691/// pipe, installed on [`GlobalState::popen_hook`] by the embedder.
692///
693/// `std::process::Command` is banned outside `lua-cli`, so `lua-stdlib`'s
694/// `io.popen` reaches the OS through this hook. `None` causes `io.popen` to
695/// raise a clean Lua error ("popen not enabled in this build"), which is
696/// appropriate for sandboxed embeddings.
697///
698/// `mode` is the Lua popen mode string — `b"r"` for reading the child's
699/// stdout, `b"w"` for writing to the child's stdin.
700pub type PopenHook =
701    fn(cmd: &[u8], mode: &[u8]) -> Result<Box<dyn lua_types::LuaFileHandle>, LuaError>;
702
703/// Function-pointer signature for removing a file, installed on
704/// [`GlobalState::file_remove_hook`] by the embedder.
705///
706/// `std::fs` is banned outside `lua-cli`, so `lua-stdlib`'s `os.remove`
707/// reaches the filesystem via this hook. Returns `Ok(())` on success.
708pub type FileRemoveHook = fn(filename: &[u8]) -> Result<(), LuaError>;
709
710/// Function-pointer signature for renaming a file, installed on
711/// [`GlobalState::file_rename_hook`] by the embedder.
712///
713/// `std::fs` is banned outside `lua-cli`, so `lua-stdlib`'s `os.rename`
714/// reaches the filesystem via this hook. Returns `Ok(())` on success.
715pub type FileRenameHook = fn(from: &[u8], to: &[u8]) -> Result<(), LuaError>;
716
717/// Reason a shell command terminated, returned by [`OsExecuteHook`].
718///
719/// Mirrors the two string literals that C-Lua's `l_inspectstat` / `luaL_execresult`
720/// can produce: `"exit"` for normal process exit, `"signal"` for signal termination
721/// (POSIX only).
722#[derive(Clone, Copy, Debug)]
723pub enum OsExecuteReason {
724    /// Process exited with an exit code (`WIFEXITED` / `ExitStatus::code()` is `Some`).
725    Exit,
726    /// Process was terminated by a signal (`WIFSIGNALED` / `ExitStatus::signal()` is `Some`).
727    Signal,
728}
729
730/// Result returned by [`OsExecuteHook`], carrying the three values that
731/// C-Lua's `luaL_execresult` pushes: `(boolean|nil, "exit"|"signal", int)`.
732#[derive(Debug)]
733pub struct OsExecuteResult {
734    /// `true` when the command exited successfully (exit code 0).
735    pub success: bool,
736    /// How the process terminated.
737    pub reason: OsExecuteReason,
738    /// Exit code (for `Exit`) or signal number (for `Signal`).
739    pub code: i32,
740}
741
742/// Function-pointer signature for executing a shell command, installed on
743/// [`GlobalState::os_execute_hook`] by the embedder.
744///
745/// `std::process` is banned outside `lua-cli`, so `lua-stdlib`'s `os.execute`
746/// reaches the shell via this hook. Returns an [`OsExecuteResult`] on success,
747/// or a [`LuaError`] when the spawn itself fails.
748pub type OsExecuteHook = fn(cmd: &[u8]) -> Result<OsExecuteResult, LuaError>;
749
750/// Opaque handle to a dynamically loaded library, allocated by a
751/// [`DynLibLoadHook`] backend and stored in `package._CLIBS`.
752///
753/// The handle is a backend-owned `u64`; the embedder is free to use it as an
754/// index into a `Vec<libloading::Library>` or a `HashMap` key. `lua-stdlib`
755/// stores the value verbatim and never inspects it.
756#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
757pub struct DynLibId(pub u64);
758
759/// Resolved dynamic-library symbol.
760///
761/// Only `RustNative` is callable by this build of the VM. `LuaCAbi` resolves
762/// to a real C function pointer compiled against stock Lua 5.4's `lua_State *`
763/// ABI but cannot be safely invoked here — it is reported as an `"init"`
764/// failure with a clear message. `Unsupported` carries an embedder-provided
765/// reason byte-string.
766pub enum DynamicSymbol {
767    /// Function pointer that follows this build's Rust-native module ABI:
768    /// `fn(&mut LuaState) -> Result<usize, LuaError>`.
769    RustNative(LuaCFunction),
770    /// Symbol exported against stock Lua 5.4's C ABI. The function pointer is
771    /// resolved but never called from this build, since `lua_State *` is not
772    /// our `LuaState`. Kept as a payload so a future C-ABI facade can pick it
773    /// up; the embedder is responsible for ensuring the underlying library
774    /// outlives this value.
775    LuaCAbi(*const ()),
776    /// Embedder-provided refusal reason, e.g. "symbol resolved but ABI version
777    /// mismatch". Reported verbatim as an `"init"` failure.
778    Unsupported { reason: Vec<u8> },
779}
780
781/// Function-pointer signature for loading a dynamic library, installed on
782/// [`GlobalState::dynlib_load_hook`] by the embedder.
783///
784/// `libloading`/`dlopen`/`LoadLibraryEx` are FFI calls and require `unsafe`,
785/// which is banned in `lua-stdlib`. `lua-cli` installs a `libloading`-backed
786/// implementation. `None` causes `package.loadlib` to return the C-Lua
787/// `"absent"` failure shape, matching the fallback platform stub.
788///
789/// `see_global` mirrors C-Lua's `seeglb` (POSIX `RTLD_GLOBAL`): set when the
790/// caller invokes `package.loadlib(path, "*")`.
791pub type DynLibLoadHook =
792    fn(state: &mut LuaState, path: &[u8], see_global: bool) -> Result<DynLibId, LuaError>;
793
794/// Function-pointer signature for resolving a symbol in a previously loaded
795/// dynamic library, installed on [`GlobalState::dynlib_symbol_hook`].
796///
797/// The hook receives the [`DynLibId`] returned by [`DynLibLoadHook`] and the
798/// requested symbol name. Returning `DynamicSymbol::RustNative` makes the
799/// symbol callable; `LuaCAbi`/`Unsupported` propagate to `package.loadlib`
800/// as an `"init"` failure with a clear message.
801pub type DynLibSymbolHook =
802    fn(state: &mut LuaState, handle: DynLibId, symbol: &[u8]) -> Result<DynamicSymbol, LuaError>;
803
804/// Function-pointer signature for unloading a dynamic library, installed on
805/// [`GlobalState::dynlib_unload_hook`].
806///
807/// Called from the `_CLIBS` `__gc` metamethod when the Lua state closes.
808/// `libloading`'s safety model requires every loaded library to outlive the
809/// last symbol it exports; the CLI backend is therefore free to ignore this
810/// hook and keep libraries alive until process exit.
811pub type DynLibUnloadHook = fn(handle: DynLibId);
812
813/// One row of [`GlobalState::threads`]. Pairs the per-thread `LuaState`
814/// with the canonical `GcRef<LuaThread>` so every `push_thread` for the
815/// same id shares pointer-identity. Phase E-1 adds this; Phase E-2
816/// extends it with interior-mutability bookkeeping when `resume`/`yield`
817/// need to mutate the child thread while the parent holds a borrow.
818pub struct ThreadRegistryEntry {
819    /// The owned coroutine `LuaState`. Wrapped in `Rc<RefCell<...>>` so
820    /// that `coroutine.resume` can borrow the child mutably while the
821    /// parent is still in scope. Single-threaded — borrows never overlap
822    /// in practice because only one resume path is live at a time.
823    pub state: Rc<RefCell<LuaState>>,
824    /// Canonical thread-value handle. Reused on every push so
825    /// `GcRef::ptr_eq` is true across pushes.
826    pub value: GcRef<lua_types::value::LuaThread>,
827}
828
829/// Process-wide state shared by all Lua threads.
830///
831/// C: `global_State` in `lstate.h`.
832/// types.tsv: `global_State → GlobalState`
833///
834/// Not exposed directly at the API; accessed via `state.global()` / `state.global_mut()`.
835pub struct GlobalState {
836    /// Phase-B hook for the Lua text parser. Set by the embedder (`lua-cli`
837    /// or stdlib host) to bridge the cyclic crate split between `lua-vm` and
838    /// `lua-parse`: when `f_parser` decides the chunk is text, it invokes
839    /// this hook instead of the parser stub. `None` leaves the stub in place
840    /// so unit tests that never load text still work.
841    pub parser_hook: Option<ParserHook>,
842
843    /// Phase-B hook for reading a Lua source file from disk. Set by `lua-cli`
844    /// (or any embedder that wants `require`/`loadfile` to reach the file
845    /// system) since `std::fs` is banned in `lua-stdlib`. `None` makes
846    /// `loadfile` and the Lua-file searcher report a file-not-found error.
847    pub file_loader_hook: Option<FileLoaderHook>,
848
849    /// Phase-B hook for opening a file handle for read/write/append. Set by
850    /// `lua-cli` since `std::fs` is banned in `lua-stdlib`. `None` causes
851    /// `io.open` and `io.output(name)` to return an error; the standard streams
852    /// (`io.stdin`, `io.stdout`, `io.stderr`) remain functional.
853    pub file_open_hook: Option<FileOpenHook>,
854
855    /// Phase-G hook for spawning a child process and connecting one stream
856    /// (stdin or stdout) to a Lua file handle. Set by `lua-cli` since
857    /// `std::process::Command` is banned in `lua-stdlib`. `None` causes
858    /// `io.popen` to raise a Lua error rather than panic.
859    pub popen_hook: Option<PopenHook>,
860
861    /// Phase-B hook for removing a file. Set by `lua-cli` since `std::fs` is
862    /// banned in `lua-stdlib`. `None` causes `os.remove` to return an error.
863    pub file_remove_hook: Option<FileRemoveHook>,
864
865    /// Phase-B hook for renaming a file. Set by `lua-cli` since `std::fs` is
866    /// banned in `lua-stdlib`. `None` causes `os.rename` to return an error.
867    pub file_rename_hook: Option<FileRenameHook>,
868
869    /// Phase-G hook for executing a shell command. Set by `lua-cli` since
870    /// `std::process` is banned in `lua-stdlib`. `None` causes `os.execute`
871    /// to report no shell available (matching C-Lua's `system(NULL) == 0`).
872    pub os_execute_hook: Option<OsExecuteHook>,
873
874    /// Phase-D-3.5 hook for loading a dynamic library (`dlopen` /
875    /// `LoadLibraryEx`). Set by `lua-cli` since `libloading` is FFI and
876    /// requires `unsafe`, which is banned in `lua-stdlib`. `None` causes
877    /// `package.loadlib` to return the `"absent"` fallback shape.
878    pub dynlib_load_hook: Option<DynLibLoadHook>,
879
880    /// Phase-D-3.5 hook for resolving a symbol in a previously loaded
881    /// dynamic library (`dlsym` / `GetProcAddress`). Set by `lua-cli`.
882    /// `None` is treated as "absent" by `package.loadlib`.
883    pub dynlib_symbol_hook: Option<DynLibSymbolHook>,
884
885    /// Phase-D-3.5 hook for unloading a dynamic library (`dlclose` /
886    /// `FreeLibrary`). Set by `lua-cli`. `None` keeps libraries loaded
887    /// until process exit, which matches `libloading`'s safety model.
888    pub dynlib_unload_hook: Option<DynLibUnloadHook>,
889
890    // C: l_mem totalbytes — Phase D memory accounting
891    // types.tsv: global_State.totalbytes → isize
892    pub totalbytes: isize,
893
894    // C: l_mem GCdebt — Phase D GC pacing
895    // types.tsv: global_State.GCdebt → isize
896    pub gc_debt: isize,
897
898    // C: lu_mem GCestimate — Phase D
899    pub gc_estimate: usize,
900
901    // C: lu_mem lastatomic — Phase D
902    // types.tsv: global_State.lastatomic → usize
903    pub lastatomic: usize,
904
905    // C: stringtable strt — intern table for short strings
906    // types.tsv: global_State.strt → StringPool
907    pub strt: StringPool,
908
909    // C: TValue l_registry — the Lua registry (always a table once state is complete)
910    // types.tsv: global_State.l_registry → LuaValue
911    pub l_registry: LuaValue,
912
913    // PORT NOTE (phase-b-reconcile): The lua-types LuaTable placeholder has
914    // no storage, so we cannot persist `registry[LUA_RIDX_GLOBALS] = globals`
915    // via the canonical registry path. Until the placeholder reconciles with
916    // lua-vm::table::LuaTable, the globals table lives in a direct field
917    // and `get_global_table` reads it from here. Same for `loaded` (the
918    // module cache normally at `registry[_LOADED]`).
919    pub globals: LuaValue,
920    pub loaded: LuaValue,
921
922    // C: TValue nilvalue — nil sentinel; non-nil signals state not yet fully built
923    // types.tsv: global_State.nilvalue → LuaValue
924    // PORT NOTE: In Rust we use a dedicated `is_complete: bool` flag rather than
925    // the C trick of checking `ttisnil(&g->nilvalue)`. See `is_complete()`.
926    pub nilvalue: LuaValue,
927
928    // C: unsigned int seed — randomized seed for hashes
929    // types.tsv: global_State.seed → u32
930    pub seed: u32,
931
932    // C: lu_byte currentwhite — Phase D GC color
933    // types.tsv: global_State.currentwhite → u8
934    pub currentwhite: u8,
935
936    // C: lu_byte gcstate — Phase D GC FSM state
937    pub gcstate: u8,
938
939    // C: lu_byte gckind — Phase D KGC_INC vs KGC_GEN
940    pub gckind: u8,
941
942    // C: lu_byte gcstopem — Phase D
943    pub gcstopem: bool,
944
945    // C: lu_byte genminormul — Phase D generational tuning
946    // types.tsv: global_State.genminormul → u8
947    pub genminormul: u8,
948
949    // C: lu_byte genmajormul — Phase D
950    pub genmajormul: u8,
951
952    // C: lu_byte gcstp — controls GC running (GCSTPGC etc.)
953    pub gcstp: u8,
954
955    // C: lu_byte gcemergency — Phase D emergency collection flag
956    pub gcemergency: bool,
957
958    // C: lu_byte gcpause — pause size between GCs (/4 stored)
959    // types.tsv: global_State.gcpause → u8
960    pub gcpause: u8,
961
962    // C: lu_byte gcstepmul — GC speed (/4 stored)
963    // types.tsv: global_State.gcstepmul → u8
964    pub gcstepmul: u8,
965
966    // C: lu_byte gcstepsize — log2 of GC granularity
967    pub gcstepsize: u8,
968
969    // Phase-D NOTE: the C-Lua intrusive GC lists (allgc, sweepgc, finobj,
970    // gray, grayagain, weak, ephemeron, allweak) were declared here as
971    // `Vec<GcRef<dyn Collectable>>` during Phase A but never populated or
972    // read. The real GC owns its own allgc chain inside `self.heap`
973    // (lua_gc::Heap). Removed during D-1e-prep to clear the `?Sized` blocker
974    // for swapping `GcRef<T> = Gc<T>` (Gc requires T: Sized for unsizing).
975    // sweepgc_cursor stayed because non-list bookkeeping kept it.
976    pub sweepgc_cursor: usize,
977
978    /// Phase-B cross-table weak-sweep registry.
979    ///
980    /// `lua_types::value::sweep_weak_tables` iterates this list at
981    /// `collectgarbage("collect")` time to clear entries whose weak target
982    /// is held only by other weak slots. Holds `Weak<LuaTable>` so the
983    /// registry itself does not pin tables that the user has dropped.
984    /// Replaced by the proper `weak` / `ephemeron` / `allweak` lists when
985    /// Phase D's incremental sweep lands.
986    pub weak_tables_registry: Vec<lua_types::gc::GcWeak<lua_types::value::LuaTable>>,
987
988    /// Phase-B long-string allocation tracker.
989    ///
990    /// Each entry pairs a `Weak<LuaString>` with the byte count that was
991    /// added to `gc_debt` at allocation time. `collectgarbage("count")` walks
992    /// the list and reclaims `gc_debt` for entries whose weak target has been
993    /// dropped, so the Lua-visible memory total tracks live long-string bytes.
994    /// Short strings are interned and bounded in size, so they are not tracked
995    /// individually. Replaced by Phase D's real allocator accounting.
996    pub gc_tracked_long_strings: Vec<(lua_types::gc::GcWeak<lua_types::string::LuaString>, usize)>,
997
998    /// Phase-B pending-finalizer registry.
999    ///
1000    /// Each entry is a strong `GcRef<LuaTable>` to a table whose metatable
1001    /// carried `__gc` at the time `setmetatable` was called. The strong ref
1002    /// pins the table so a normal `Rc::drop` does not destroy it before its
1003    /// `__gc` metamethod runs. The Phase-B finalizer sweep
1004    /// (`crate::api::run_pending_finalizers`) scans this list, takes any
1005    /// entry whose strong count is 1 (only this list holds it — i.e. the
1006    /// user has dropped every reference), and invokes its `__gc` before
1007    /// releasing the ref. Replaced by `finobj` / `tobefnz` when the real
1008    /// incremental GC lands in Phase D.
1009    pub pending_finalizers: Vec<GcRef<lua_types::value::LuaTable>>,
1010
1011    /// Tables identified by the most recent `collect_via_heap` mark phase as
1012    /// reachable only through `pending_finalizers` (i.e. the user has dropped
1013    /// every reference). Their `__gc` runs the next time
1014    /// `run_pending_finalizers` executes; entries are then cleared. Traced as
1015    /// strong roots so they survive the sweep that scheduled them.
1016    pub to_be_finalized: Vec<GcRef<lua_types::value::LuaTable>>,
1017
1018    // Phase-D NOTE: tobefnz + fixedgc removed (dead since Phase A — see
1019    // sibling note above re allgc et al). Pending finalizers live in
1020    // `pending_finalizers` above; fixed objects live in heap.allgc with the
1021    // GC's own `fixed` bit.
1022
1023    // Generational cohort markers — Phase D only
1024    // types.tsv: global_State.survival/old1/reallyold/firstold1/finobjsur/finobjold1/finobjrold
1025    //   → (removed; replaced by index cursors in Phase D)
1026
1027    // C: struct lua_State *twups — threads with open upvalues
1028    // types.tsv: global_State.twups → Vec<GcRef<LuaState>>
1029    pub twups: Vec<GcRef<LuaState>>,
1030
1031    // C: lua_CFunction panic — panic handler; Phase B
1032    // types.tsv: global_State.panic → Option<lua_CFunction>
1033    pub panic: Option<LuaCFunction>,
1034
1035    // C: struct lua_State *mainthread
1036    // types.tsv: global_State.mainthread → GcRef<LuaState>
1037    // TODO(port): self-referential Rc cycle; Phase D GC handles cycles properly
1038    pub mainthread: Option<GcRef<LuaState>>,
1039
1040    /// Registry of all live coroutine threads, keyed by `ThreadId`. Phase E-1
1041    /// replaces the `thread_token` placeholder with a real id-indexed map so
1042    /// `coroutine.create` allocates a fresh `LuaState`, registers it, and
1043    /// returns a value that resolves back to the same state on every
1044    /// `coroutine.status` / `coroutine.resume` call.
1045    ///
1046    /// Each entry pairs the per-thread `LuaState` with the canonical
1047    /// `GcRef<LuaThread>` value, so two `LuaValue::Thread` pushes of the
1048    /// same id share `GcRef::ptr_eq` identity. The main thread is NOT
1049    /// stored here — its `LuaState` is owned externally by the embedder.
1050    /// `main_thread_id` is reserved as `0` and a `LuaValue::Thread`
1051    /// carrying id `0` is recognized as the main thread by lookup helpers.
1052    pub threads: std::collections::HashMap<u64, ThreadRegistryEntry>,
1053
1054    /// Cached `LuaValue::Thread` payload for the main thread (id 0).
1055    /// Built once during `new_state` so every `push_thread` on the main
1056    /// thread shares the same `GcRef<LuaThread>` and thus compares
1057    /// pointer-equal under `LuaValue::PartialEq`.
1058    pub main_thread_value: GcRef<lua_types::value::LuaThread>,
1059
1060    /// Identity of the currently-running thread. `0` (main) until a
1061    /// coroutine resume swaps it in slice 02b. The Phase E-1 slice
1062    /// always leaves this at `main_thread_id` because resume is not yet
1063    /// implemented.
1064    pub current_thread_id: u64,
1065
1066    /// Identity of the main thread. Convention: `0`. Held as a field so
1067    /// the lookup helpers can read it without hard-coding the constant.
1068    pub main_thread_id: u64,
1069
1070    /// Monotonic counter handing out fresh ids in `new_thread`. Starts
1071    /// at `1` because `0` is reserved for the main thread.
1072    pub next_thread_id: u64,
1073
1074    // C: TString *memerrmsg — preallocated OOM error message
1075    // types.tsv: global_State.memerrmsg → GcRef<LuaString>
1076    pub memerrmsg: GcRef<LuaString>,
1077
1078    // C: TString *tmname[TM_N] — tag-method names indexed by TMS enum
1079    // types.tsv: global_State.tmname → [GcRef<LuaString>; TM_N]
1080    // TODO(port): TM_N constant and TagMethod enum come from ltm.c → tagmethods.rs
1081    pub tmname: Vec<GcRef<LuaString>>,
1082
1083    // C: struct Table *mt[LUA_NUMTYPES] — per-type metatables
1084    // types.tsv: global_State.mt → [Option<GcRef<LuaTable>>; LUA_NUMTYPES]
1085    pub mt: [Option<GcRef<LuaTable>>; LUA_NUMTYPES],
1086
1087    // C: TString *strcache[STRCACHE_N][STRCACHE_M] — string cache for luaS_new
1088    // types.tsv: global_State.strcache → [[GcRef<LuaString>; STRCACHE_M]; STRCACHE_N]
1089    pub strcache: [[GcRef<LuaString>; STRCACHE_M]; STRCACHE_N],
1090
1091    /// Stable intern map for the public [`LuaString`] type. Distinct from
1092    /// `strt` (which keys internal `LuaStringImpl`) because the parser and
1093    /// stdlib need pointer-equality across `intern_str` calls so
1094    /// `GcRef::ptr_eq` can resolve variable identity. Without this map each
1095    /// call allocates a fresh `GcRef` and locals/upvalues fail to resolve.
1096    pub interned_lt: std::collections::HashMap<Box<[u8]>, GcRef<LuaString>>,
1097
1098    // C: lua_WarnFunction warnf — warning function sink
1099    // types.tsv: global_State.warnf → Option<Box<dyn FnMut(&[u8], bool)>>
1100    pub warnf: Option<Box<dyn FnMut(&[u8], bool)>>,
1101    // C: void *ud_warn — folded into the `warnf` closure capture; removed
1102
1103    /// Registry of native `LuaCFunction` pointers. Lua-types cannot reference
1104    /// `LuaState`, so `LuaClosure::LightC` carries a `usize` index into this
1105    /// vector instead of the real function pointer. `push_c_function`
1106    /// registers the function and stores the resulting index in the closure.
1107    pub c_functions: Vec<LuaCFunction>,
1108
1109    /// Phase-D heap. Owns the allgc intrusive list and runs collections.
1110    /// During Phase A-C this is `paused=true`, so allocations don't auto-
1111    /// register and `step` is a no-op. Phase D-1d wires `unpause()` after
1112    /// state initialization, at which point `step` runs during VM dispatch.
1113    pub heap: lua_gc::Heap,
1114
1115    /// Phase E-3 cross-thread open-upvalue mirror. Maps `(thread_id, stack_idx)`
1116    /// to the live value of an open upvalue whose home thread is currently
1117    /// suspended while another thread runs. `coroutine.resume` snapshots the
1118    /// parent's open upvalues into this map before yielding control to the
1119    /// child, and reads the (possibly mutated) values back into the parent's
1120    /// stack when the child suspends or returns. From the running thread's
1121    /// perspective, `upvalue_get` / `upvalue_set` consult the mirror whenever
1122    /// an open upvalue's `thread_id` does not match `current_thread_id`.
1123    ///
1124    /// This avoids a stack refactor: the parent's `LuaState` is held by a
1125    /// `&mut` reference up the call stack during resume, so its stack cannot
1126    /// be reached directly through any `Rc<RefCell<_>>`. The mirror is the
1127    /// shared scratchpad that bridges the gap for the duration of a resume.
1128    pub cross_thread_upvals: std::collections::HashMap<(u64, StackIdx), LuaValue>,
1129
1130    /// Phase F-1.a workaround for GC use-after-free across coroutine boundaries.
1131    /// When `aux_resume` switches to a child thread, the parent's live stack
1132    /// values would otherwise become unreachable to the tracer for the duration
1133    /// of the resume (the parent `LuaState` is held only as a stack-borrowed
1134    /// `&mut` up the call chain and is not part of any traced root set). To
1135    /// keep those values alive, `aux_resume` pushes a snapshot of the parent
1136    /// stack here before transferring control, and pops it on suspension or
1137    /// completion. The tracer visits every snapshot as a GC root via the
1138    /// `Trace for GlobalState` impl in `trace_impls.rs`.
1139    ///
1140    /// Phase F-2.b added a reachability-driven thread sweep that supersedes
1141    /// most of this, but the snapshot still guards values that live only on
1142    /// the parent's stack (i.e. not yet rooted by any thread node).
1143    pub suspended_parent_stacks: Vec<Vec<LuaValue>>,
1144
1145    /// Open-upvalue handles belonging to the same suspended parent windows as
1146    /// `suspended_parent_stacks`. Stack snapshots keep the pointed-to values
1147    /// alive; this roots the `UpVal` objects themselves so a GC inside the
1148    /// child coroutine cannot sweep entries still present in the parent's
1149    /// `openupval` list.
1150    pub suspended_parent_open_upvals: Vec<Vec<GcRef<UpVal>>>,
1151}
1152
1153impl GlobalState {
1154    /// Total live bytes allocated (GCdebt + totalbytes).
1155    ///
1156    /// C: `gettotalbytes(g)` macro → `cast(lu_mem, (g)->totalbytes + (g)->GCdebt)`
1157    /// macros.tsv: `gettotalbytes → g.total_bytes()`
1158    pub fn total_bytes(&self) -> usize {
1159        (self.totalbytes + self.gc_debt) as usize
1160    }
1161
1162    /// Look up the coroutine `LuaState` registered under `id`. Returns
1163    /// `None` for the main-thread id (the main `LuaState` is owned by
1164    /// the embedder, not stored in `threads`) and for ids that were
1165    /// never issued or have already been closed.
1166    pub fn get_thread(&self, id: u64) -> Option<&ThreadRegistryEntry> {
1167        self.threads.get(&id)
1168    }
1169
1170    /// Return the canonical `GcRef<LuaThread>` for `id`. For the main
1171    /// thread that's `main_thread_value`; for a coroutine it's the
1172    /// value stored in the registry. Returns `None` if `id` is unknown.
1173    pub fn thread_value_for(&self, id: u64) -> Option<GcRef<lua_types::value::LuaThread>> {
1174        if id == self.main_thread_id {
1175            Some(self.main_thread_value.clone())
1176        } else {
1177            self.threads.get(&id).map(|e| e.value.clone())
1178        }
1179    }
1180
1181    /// Returns `true` when the state has been fully initialized.
1182    ///
1183    /// C: `completestate(g)` macro → `ttisnil(&g->nilvalue)`
1184    /// macros.tsv: `completestate → g.is_complete()`
1185    ///
1186    /// PORT NOTE: C uses `g->nilvalue` being nil as the "complete" signal.
1187    /// We replicate the same logic: `nilvalue == Nil` means complete.
1188    pub fn is_complete(&self) -> bool {
1189        // C: ttisnil(&g->nilvalue)
1190        matches!(self.nilvalue, LuaValue::Nil)
1191    }
1192
1193    /// Returns the "current white" GC color bitmask.
1194    ///
1195    /// C: `luaC_white(g)` macro.
1196    /// macros.tsv: `luaC_white → g.current_white()`
1197    ///
1198    /// PORT NOTE: GC color management deferred to Phase D; always returns
1199    /// the initial white bit.
1200    pub fn current_white(&self) -> u8 {
1201        self.currentwhite
1202    }
1203
1204    /// Returns the "other white" GC color bitmask.
1205    ///
1206    /// C: `otherwhite(g)` macro.
1207    /// macros.tsv: `otherwhite → g.other_white()`
1208    pub fn other_white(&self) -> u8 {
1209        // TODO(port): Phase D — toggle white bit properly
1210        self.currentwhite ^ 0x03
1211    }
1212
1213    /// Returns `true` if the GC is in generational mode.
1214    ///
1215    /// C: `isdecGCmodegen(g)` macro.
1216    /// macros.tsv: `isdecGCmodegen → g.is_gen_mode()`
1217    pub fn is_gen_mode(&self) -> bool {
1218        self.gckind == GcKind::Generational as u8
1219    }
1220
1221    /// Returns `true` if the GC is currently running.
1222    ///
1223    /// C: `gcrunning(g)` macro.
1224    /// macros.tsv: `gcrunning → g.gc_running()`
1225    pub fn gc_running(&self) -> bool {
1226        self.gcstp == 0
1227    }
1228
1229    /// Returns `true` while the GC is in its propagation phase.
1230    ///
1231    /// C: `keepinvariant(g)` macro.
1232    /// macros.tsv: `keepinvariant → g.keep_invariant()`
1233    pub fn keep_invariant(&self) -> bool {
1234        // TODO(port): Phase D — check gcstate for propagation phases
1235        false
1236    }
1237
1238    /// Returns `true` while the GC is in a sweep phase.
1239    ///
1240    /// C: `issweepphase(g)` macro.
1241    /// macros.tsv: `issweepphase → g.is_sweep_phase()`
1242    pub fn is_sweep_phase(&self) -> bool {
1243        // TODO(port): Phase D — check gcstate for sweep states (GCSswpallgc etc.)
1244        false
1245    }
1246
1247    // ── Phase-B stubs ─────────────────────────────────────────────────────────
1248    pub fn gc_debt(&self) -> isize { self.gc_debt }
1249    pub fn set_gc_debt(&mut self, d: isize) { self.gc_debt = d; }
1250    pub fn gc_at_pause(&self) -> bool { self.gcstate == 0 }
1251    pub fn gc_pause_param(&self) -> u8 { self.gcpause }
1252    pub fn set_gc_pause_param(&mut self, p: u8) { self.gcpause = p; }
1253    pub fn gc_stepmul_param(&self) -> u8 { self.gcstepmul }
1254    pub fn set_gc_stepmul_param(&mut self, p: u8) { self.gcstepmul = p; }
1255    pub fn set_gc_genmajormul(&mut self, p: u8) { self.genmajormul = p; }
1256    pub fn gc_stop_flags(&self) -> u8 { self.gcstp }
1257    pub fn set_gc_stop_flags(&mut self, f: u8) { self.gcstp = f; }
1258    pub fn stop_gc_internal(&mut self) -> u8 {
1259        let old = self.gcstp;
1260        self.gcstp |= GCSTPGC;
1261        old
1262    }
1263    pub fn set_gc_stop_user(&mut self) {
1264        // C: g->gcstp = GCSTPUSR;  (lapi.c:1143)
1265        // GCSTPUSR (lgc.h:155) = 1 — bit set when GC is stopped by user (lua_gc(L, LUA_GCSTOP)).
1266        self.gcstp = GCSTPUSR;
1267    }
1268    pub fn clear_gc_stop(&mut self) { self.gcstp = 0; }
1269    /// C: `gcrunning(g)` in `lgc.h`.
1270    pub fn is_gc_running(&self) -> bool { self.gcstp == 0 }
1271    /// True when the GC has been disabled internally (state setup, mid-GC,
1272    /// or while closing); user-stop via `collectgarbage("stop")` does NOT
1273    /// set this bit, so `lua_gc` continues to honour Count/Step/etc.
1274    ///
1275    /// C: `g->gcstp & GCSTPGC` (lapi.c:1137).
1276    pub fn is_gc_stopped_internally(&self) -> bool { (self.gcstp & GCSTPGC) != 0 }
1277
1278    /// Returns the interned `__xxx` name string for tag method `tm`, or
1279    /// `None` if `tmname` has not yet been initialised (early bootstrap).
1280    ///
1281    /// C: `G(L)->tmname[tm]` (lookup via macro in ltm.h).
1282    /// macros.tsv: `getshrstr(G(L)->tmname[tm]) → g.tm_name(tm)`.
1283    ///
1284    /// PORT NOTE: The lua-vm crate carries two distinct `TagMethod` enums
1285    /// (one in `lua-types`, one in `crate::tagmethods`) with identical
1286    /// `#[repr(u8)]` ordering. The [`TmIndex`] trait bridges them so callers
1287    /// from either side can index `tmname` uniformly.
1288    pub fn tm_name<T: TmIndex>(&self, tm: T) -> Option<GcRef<LuaString>> {
1289        self.tmname.get(tm.tm_index()).cloned()
1290    }
1291}
1292
1293/// Discriminant-to-index conversion for the two parallel `TagMethod` enums.
1294///
1295/// Both `lua_types::tagmethod::TagMethod` and `crate::tagmethods::TagMethod`
1296/// are `#[repr(u8)]` with the same ORDER TM layout, so casting through `u8`
1297/// yields the correct `GlobalState.tmname` index for either type.
1298pub trait TmIndex: Copy {
1299    fn tm_index(self) -> usize;
1300}
1301impl TmIndex for lua_types::tagmethod::TagMethod {
1302    fn tm_index(self) -> usize { self as u8 as usize }
1303}
1304impl TmIndex for crate::tagmethods::TagMethod {
1305    fn tm_index(self) -> usize { self as u8 as usize }
1306}
1307impl TmIndex for usize {
1308    fn tm_index(self) -> usize { self }
1309}
1310impl TmIndex for u8 {
1311    fn tm_index(self) -> usize { self as usize }
1312}
1313
1314use lua_types::tagmethod::TagMethod;
1315
1316// ─── LuaState ────────────────────────────────────────────────────────────────
1317
1318/// Per-thread Lua execution state.
1319///
1320/// C: `struct lua_State` in `lstate.h`.
1321/// types.tsv: `lua_State → LuaState`
1322///
1323/// All stack-pointer fields in C (`StkIdRel`, `StkId`) become `StackIdx` (u32
1324/// index into `stack: Vec<StackValue>`).  The C intrusive `CallInfo` linked list
1325/// becomes `call_info: Vec<CallInfo>` indexed by `CallInfoIdx`.
1326pub struct LuaState {
1327    // ── Thread status ──
1328
1329    // C: lu_byte status — thread status (LUA_OK / LUA_YIELD / LUA_ERR*)
1330    // types.tsv: lua_State.status → u8
1331    pub status: u8,
1332
1333    // C: lu_byte allowhook — hook-enabled flag
1334    // types.tsv: lua_State.allowhook → bool
1335    pub allowhook: bool,
1336
1337    // C: unsigned short nci — number of CallInfo entries in use
1338    // types.tsv: lua_State.nci → u32
1339    pub nci: u32,
1340
1341    // ── Stack ──
1342
1343    // C: StkIdRel top — first free stack slot
1344    // types.tsv: lua_State.top → StackIdx
1345    pub top: StackIdx,
1346
1347    // C: StkIdRel stack_last — end-of-stack sentinel (stack.p + BASIC_STACK_SIZE)
1348    // types.tsv: lua_State.stack_last → StackIdx (redundant once Vec; kept for parity)
1349    pub stack_last: StackIdx,
1350
1351    // C: StkIdRel stack — the stack base pointer; in Rust this is the Vec itself
1352    // types.tsv: lua_State.stack → Vec<StackValue>
1353    pub stack: Vec<StackValue>,
1354
1355    // ── Call info ──
1356
1357    // C: CallInfo *ci — current call frame; raw pointer in C
1358    // types.tsv: lua_State.ci → CallInfoIdx
1359    pub ci: CallInfoIdx,
1360
1361    // C: CallInfo base_ci — bottom CallInfo (C→Lua entry); element 0 of the Vec
1362    // types.tsv: lua_State.base_ci → CallInfo  (Vec element 0)
1363    // PORT NOTE: In Rust, base_ci is call_info[0]. There is no separate field.
1364    pub call_info: Vec<CallInfo>,
1365
1366    // ── Upvalues / to-be-closed ──
1367
1368    // C: UpVal *openupval — open upvalue list (was intrusive; now a Vec)
1369    // types.tsv: lua_State.openupval → Vec<GcRef<UpVal>>
1370    pub openupval: Vec<GcRef<UpVal>>,
1371
1372    // C: StkIdRel tbclist — to-be-closed list (was StkIdRel pointer; now Vec of idx)
1373    // types.tsv: lua_State.tbclist → Vec<StackIdx>
1374    pub tbclist: Vec<StackIdx>,
1375
1376    // ── Global state ──
1377
1378    // C: global_State *l_G — pointer to shared GlobalState
1379    // types.tsv: lua_State.l_G → (accessed via method)
1380    // PORT NOTE: Rc<RefCell<>> for shared ownership across coroutine threads.
1381    pub(crate) global: Rc<RefCell<GlobalState>>,
1382
1383    // ── Hooks ──
1384
1385    // C: volatile lua_Hook hook
1386    // types.tsv: lua_State.hook → Option<Box<dyn FnMut(&mut LuaState, &LuaDebug)>>
1387    pub hook: Option<Box<dyn FnMut(&mut LuaState, &crate::debug::LuaDebug)>>,
1388
1389    // C: volatile l_signalT hookmask
1390    // types.tsv: lua_State.hookmask → u8
1391    pub hookmask: u8,
1392
1393    // C: int basehookcount
1394    // types.tsv: lua_State.basehookcount → i32
1395    pub basehookcount: i32,
1396
1397    // C: int hookcount
1398    // types.tsv: lua_State.hookcount → i32
1399    pub hookcount: i32,
1400
1401    // ── Error handling ──
1402
1403    // C: struct lua_longjmp *errorJmp — C longjmp recovery point
1404    // types.tsv: lua_State.errorJmp → (removed; replaced by Result<T, LuaError>)
1405    // PORT NOTE: Entirely removed. The `?` operator replaces setjmp/longjmp.
1406
1407    // C: ptrdiff_t errfunc — error-handler stack position (0 = none)
1408    // types.tsv: lua_State.errfunc → isize
1409    pub errfunc: isize,
1410
1411    // ── C-call depth ──
1412
1413    // C: l_uint32 nCcalls — packed (recursion_count | non_yieldable_count << 16)
1414    // types.tsv: lua_State.nCcalls → u32
1415    pub nCcalls: u32,
1416
1417    // ── Debug / hooks ──
1418
1419    // C: int oldpc — last pc traced (for hooks)
1420    // types.tsv: lua_State.oldpc → u32
1421    pub oldpc: u32,
1422
1423    // ── GC color (Phase D) ──
1424
1425    // C: lu_byte marked — GC color/age bits; Phase D only
1426    // types.tsv: GCObject.marked → u8
1427    pub marked: u8,
1428
1429    /// Owner thread id for this `LuaState`, cached as a plain `u64` so the
1430    /// hot path of `upvalue_get` can compare against an open upvalue's
1431    /// `thread_id` without taking a `RefCell::borrow` on the shared
1432    /// `GlobalState`.
1433    ///
1434    /// Invariant: while this `LuaState` is the actively running thread,
1435    /// `GlobalState::current_thread_id == self.cached_thread_id`. This is
1436    /// maintained structurally by `new_state`/`new_thread` (which set
1437    /// `cached_thread_id` to the thread's own id once at construction)
1438    /// combined with the coroutine resume protocol: `coro_lib::resume`
1439    /// writes `co_state.global.current_thread_id = co_id` before the
1440    /// coroutine runs, and restores `parent_thread_id` on yield/return.
1441    /// Because each thread caches its own id (not the global's id), the
1442    /// invariant survives every context switch without an explicit refresh
1443    /// at the resume site.
1444    pub cached_thread_id: u64,
1445
1446}
1447
1448impl LuaState {
1449    /// Access the process-wide `GlobalState` immutably.
1450    ///
1451    /// C: `G(L)` macro → `state.global()`.
1452    /// macros.tsv: `G → state.global()`
1453    ///
1454    /// PORT NOTE: Returns `std::cell::Ref<GlobalState>` because GlobalState is held in
1455    /// `Rc<RefCell<...>>`. Call sites that do `state.global().field` should work fine
1456    /// via `Deref`. Callers must not hold the `Ref` across a `global_mut()` call.
1457    pub fn global(&self) -> std::cell::Ref<'_, GlobalState> {
1458        self.global.borrow()
1459    }
1460
1461    /// Access the process-wide `GlobalState` mutably.
1462    ///
1463    /// C: `G(L)` + indirect write → `state.global_mut()`.
1464    /// macros.tsv: `G → state.global()` (writes use `state.global_mut()`)
1465    pub fn global_mut(&self) -> std::cell::RefMut<'_, GlobalState> {
1466        self.global.borrow_mut()
1467    }
1468
1469    /// Clone the `Rc` handle to the GlobalState for sharing with a new coroutine.
1470    ///
1471    /// Used in `new_thread` to give the child thread access to the same GlobalState.
1472    pub fn global_rc(&self) -> Rc<RefCell<GlobalState>> {
1473        Rc::clone(&self.global)
1474    }
1475
1476    /// Return the current C-call recursion depth (lower 16 bits of `nCcalls`).
1477    ///
1478    /// C: `getCcalls(L)` macro → `(L)->nCcalls & 0xffff`
1479    /// macros.tsv: `getCcalls → state.c_calls()`
1480    pub fn c_calls(&self) -> u32 {
1481        self.nCcalls & 0xffff
1482    }
1483
1484    /// Increment the non-yieldable call count (upper 16 bits of `nCcalls`).
1485    ///
1486    /// C: `incnny(L)` macro → `(L)->nCcalls += 0x10000`
1487    /// macros.tsv: `incnny → state.inc_nny()`
1488    pub fn inc_nny(&mut self) {
1489        self.nCcalls += 0x10000;
1490    }
1491
1492    /// Decrement the non-yieldable call count.
1493    ///
1494    /// C: `decnny(L)` macro → `(L)->nCcalls -= 0x10000`
1495    /// macros.tsv: `decnny → state.dec_nny()`
1496    pub fn dec_nny(&mut self) {
1497        self.nCcalls -= 0x10000;
1498    }
1499
1500    /// Returns `true` if the thread can yield (no non-yieldable frames on the stack).
1501    ///
1502    /// C: `yieldable(L)` macro → `((L)->nCcalls & 0xffff0000) == 0`
1503    /// macros.tsv: `yieldable → state.is_yieldable()`
1504    pub fn is_yieldable(&self) -> bool {
1505        (self.nCcalls & 0xffff0000) == 0
1506    }
1507
1508    /// Reset the hook countdown to the baseline.
1509    ///
1510    /// C: `resethookcount(L)` macro → `L->hookcount = L->basehookcount`
1511    /// macros.tsv: `resethookcount → state.reset_hook_count()`
1512    pub fn reset_hook_count(&mut self) {
1513        self.hookcount = self.basehookcount;
1514    }
1515
1516    /// Returns the current stack capacity (slots between base and stack_last).
1517    ///
1518    /// C: `stacksize(th)` macro → `cast_int((th)->stack_last.p - (th)->stack.p)`
1519    /// macros.tsv: `stacksize → state.stack_size()`
1520    pub fn stack_size(&self) -> usize {
1521        self.stack_last.0 as usize
1522    }
1523
1524    /// Push a value onto the stack, incrementing `top`.
1525    ///
1526    /// C: `*L->top++ = val` (various push patterns)
1527    /// macros.tsv: `api_incr_top → gone — state.push() already increments`
1528    #[inline(always)]
1529    pub fn push(&mut self, val: LuaValue) {
1530        let top = self.top.0 as usize;
1531        if top < self.stack.len() {
1532            self.stack[top] = StackValue { val, tbc_delta: 0 };
1533        } else {
1534            self.stack.push(StackValue { val, tbc_delta: 0 });
1535        }
1536        self.top = StackIdx(self.top.0 + 1);
1537    }
1538
1539    /// Pop the top value from the stack, decrementing `top`.
1540    ///
1541    /// C: `L->top--` + dereference.
1542    #[inline(always)]
1543    pub fn pop(&mut self) -> LuaValue {
1544        if self.top.0 == 0 {
1545            return LuaValue::Nil;
1546        }
1547        self.top = StackIdx(self.top.0 - 1);
1548        self.stack[self.top.0 as usize].val.clone()
1549    }
1550
1551    /// Retrieve the value at the given stack index without removing it.
1552    ///
1553    /// C: `s2v(L->stack.p + idx)` / stack slot access.
1554    /// macros.tsv: `s2v → state.stack_at(idx)` → returns `&LuaValue`
1555    #[inline(always)]
1556    pub fn stack_val(&self, idx: StackIdx) -> &LuaValue {
1557        &self.stack[idx.0 as usize].val
1558    }
1559
1560    /// Write a value to a specific stack slot.
1561    #[inline(always)]
1562    pub fn set_stack_val(&mut self, idx: StackIdx, val: LuaValue) {
1563        self.stack[idx.0 as usize].val = val;
1564    }
1565
1566    /// Returns a no-op GC handle.
1567    ///
1568    /// C: Various `luaC_*` calls → `state.gc().*`
1569    /// macros.tsv: `luaC_checkGC → state.gc().check_step()`, etc.
1570    ///
1571    /// PORT NOTE: In Phases A–C the GC is `Rc`-based and all GC operations are
1572    /// no-ops. Phase D replaces this with real GC logic in `lua-gc`.
1573    pub fn gc(&mut self) -> GcHandle<'_> {
1574        GcHandle { _state: self }
1575    }
1576
1577    /// Create a new empty table and register it with the GC.
1578    ///
1579    /// C: `luaH_new(L)` → `state.new_table()` returning `GcRef<LuaTable>`
1580    /// macros.tsv: `lua_newtable → state.new_table()`
1581    pub fn new_table(&mut self) -> GcRef<LuaTable> {
1582        // TODO(port): register with GC tracking (state.global_mut().allgc) in Phase D
1583        GcRef::new(LuaTable::placeholder())
1584    }
1585
1586    /// Intern a byte string in the global string pool.
1587    ///
1588    /// In C, short strings (≤ LUAI_MAXSHORTLEN = 40 bytes) are interned globally
1589    /// via `luaS_newlstr`, while long strings allocate a fresh TString each
1590    /// call so distinct long strings keep distinct object identity (observable
1591    /// via `string.format("%p", s)`). The parser separately deduplicates
1592    /// long-string literals within a single chunk through `luaX_newstring`'s
1593    /// `ls->h` anchor table.
1594    ///
1595    /// C: `luaS_newlstr` (and `luaS_new`, which calls `luaS_newlstr`)
1596    /// macros.tsv: `luaS_new → state.intern_str(s)`
1597    pub fn intern_str(&mut self, bytes: &[u8]) -> Result<GcRef<LuaString>, LuaError> {
1598        if bytes.len() <= crate::string::MAX_SHORT_LEN {
1599            if let Some(existing) = self.global().interned_lt.get(bytes) {
1600                return Ok(existing.clone());
1601            }
1602            let _local = crate::string::new(self, bytes)?;
1603            let new_ref = GcRef::new(LuaString::from_bytes(bytes.to_vec()));
1604            self.global_mut()
1605                .interned_lt
1606                .insert(bytes.to_vec().into_boxed_slice(), new_ref.clone());
1607            Ok(new_ref)
1608        } else {
1609            let new_ref = GcRef::new(LuaString::from_bytes(bytes.to_vec()));
1610            // PORT NOTE: Phase-B byte tracking for `collectgarbage("count")`.
1611            // C-Lua's `luaC_newobj` calls `luaM_malloc`, which adds
1612            // `sizeof(TString) + len + 1` to `g->GCdebt`. Phases A–C bypass
1613            // that allocator, so without explicit accounting the Lua-visible
1614            // memory total never reflects string payload — gc.lua's
1615            // string-keys-in-weak-tables block depends on observing the >8MB
1616            // jump after allocating two 4MB strings. Short strings are
1617            // interned (bounded in size) so they are not tracked here.
1618            // `reclaim_dead_long_strings` later subtracts the size back out
1619            // when the underlying `Rc` is dropped.
1620            let size = bytes.len()
1621                + std::mem::size_of::<LuaString>()
1622                + std::mem::size_of::<usize>();
1623            let mut g = self.global_mut();
1624            g.gc_debt += size as isize;
1625            g.gc_tracked_long_strings
1626                .push((new_ref.downgrade(), size));
1627            Ok(new_ref)
1628        }
1629    }
1630
1631    /// Returns the current CallInfo index (the active call frame).
1632    #[inline(always)]
1633    pub fn top_idx(&self) -> StackIdx {
1634        self.top
1635    }
1636}
1637
1638// ─── Phase-B stub methods ─────────────────────────────────────────────────────
1639//
1640// The methods in the impl blocks below were referenced by api.rs, debug.rs,
1641// do_.rs, vm.rs, tagmethods.rs etc. during Phase A. Each body is a `todo!()`
1642// pinned to a phase-b task; once the corresponding C function is faithfully
1643// ported the stub will be replaced. Signatures are inferred from call sites
1644// and should be treated as Phase-B-grade approximations.
1645
1646impl LuaState {
1647    #[inline(always)]
1648    pub fn get_at(&self, idx: impl Into<StackIdxConv>) -> LuaValue {
1649        let i: StackIdx = idx.into().0;
1650        match self.stack.get(i.0 as usize) {
1651            Some(slot) => slot.val.clone(),
1652            None => LuaValue::Nil,
1653        }
1654    }
1655    #[inline(always)]
1656    pub fn set_at(&mut self, idx: impl Into<StackIdxConv>, v: LuaValue) {
1657        let i: StackIdx = idx.into().0;
1658        self.stack[i.0 as usize].val = v;
1659    }
1660
1661    /// Clear stack slots in `[start, end)` without changing `top`.
1662    ///
1663    /// Internal call setup reserves space up to `ci.top`; while GC tracing is
1664    /// conservative over that range, the unused tail must not retain stale
1665    /// collectable values from previous frames.
1666    pub fn clear_stack_range(&mut self, start: StackIdx, end: StackIdx) {
1667        if end.0 <= start.0 {
1668            return;
1669        }
1670        let end_u = end.0 as usize;
1671        if end_u > self.stack.len() {
1672            self.stack.resize_with(end_u, StackValue::default);
1673        }
1674        for i in start.0..end.0 {
1675            self.stack[i as usize].val = LuaValue::Nil;
1676            self.stack[i as usize].tbc_delta = 0;
1677        }
1678    }
1679    /// Hot-path accessor: returns `Some(i)` only when the stack slot at `idx`
1680    /// holds a `LuaValue::Int(i)`. Returns `None` for any other tag (including
1681    /// out-of-bounds, which behaves as `Nil`).
1682    ///
1683    /// C: `ttisinteger(s2v(slot)) ? ivalue(s2v(slot)) : 0` paired with the
1684    /// `ttisinteger` predicate that gates the integer arithmetic fast path in
1685    /// `lvm.c`'s `op_arith_aux` macro. Avoids the full `LuaValue` clone that
1686    /// `get_at` performs — the operand is only needed for its `i64` payload.
1687    #[inline]
1688    pub fn get_int_at(&self, idx: impl Into<StackIdxConv>) -> Option<i64> {
1689        let i: StackIdx = idx.into().0;
1690        match self.stack.get(i.0 as usize) {
1691            Some(slot) => match &slot.val {
1692                LuaValue::Int(v) => Some(*v),
1693                _ => None,
1694            },
1695            None => None,
1696        }
1697    }
1698    /// Hot-path accessor: returns `Some((a, b))` only when both stack slots
1699    /// at `rb` and `rc` hold integers. Equivalent to two `get_int_at` calls
1700    /// but is shaped so the arithmetic opcode dispatch arms can pattern-match
1701    /// the common case with a single `if let`.
1702    ///
1703    /// C: the paired `ttisinteger(v1) && ttisinteger(v2)` check at the top of
1704    /// the `op_arith_aux` macro.
1705    #[inline]
1706    pub fn get_int_pair_at(
1707        &self,
1708        rb: impl Into<StackIdxConv>,
1709        rc: impl Into<StackIdxConv>,
1710    ) -> Option<(i64, i64)> {
1711        let ib = self.get_int_at(rb)?;
1712        let ic = self.get_int_at(rc)?;
1713        Some((ib, ic))
1714    }
1715    /// Hot-path accessor: returns `Some(f)` when the slot holds a `Float(f)`
1716    /// or coerces an `Int(i)` to `f64`. Returns `None` for any other tag.
1717    /// No `LuaValue` clone — only the primitive payload travels back.
1718    ///
1719    /// C: the `tonumberns(o, n)` macro inlined for stack-resident operands.
1720    #[inline]
1721    pub fn get_num_at(&self, idx: impl Into<StackIdxConv>) -> Option<f64> {
1722        let i: StackIdx = idx.into().0;
1723        match self.stack.get(i.0 as usize) {
1724            Some(slot) => match &slot.val {
1725                LuaValue::Float(f) => Some(*f),
1726                LuaValue::Int(v) => Some(*v as f64),
1727                _ => None,
1728            },
1729            None => None,
1730        }
1731    }
1732    /// Hot-path accessor: returns `Some(f)` only when the slot holds a
1733    /// `LuaValue::Float(f)`. Does NOT coerce integers; the integer branch is
1734    /// the caller's responsibility. Used by opcode arms that have already
1735    /// ruled out the integer fast path.
1736    #[inline]
1737    pub fn get_float_at(&self, idx: impl Into<StackIdxConv>) -> Option<f64> {
1738        let i: StackIdx = idx.into().0;
1739        match self.stack.get(i.0 as usize) {
1740            Some(slot) => match &slot.val {
1741                LuaValue::Float(f) => Some(*f),
1742                _ => None,
1743            },
1744            None => None,
1745        }
1746    }
1747    /// Hot-path accessor: pair version of `get_num_at` — returns `Some((a,b))`
1748    /// when both slots coerce to `f64` (Float or Int), `None` if either does
1749    /// not. Used by the float fast path of the arith opcodes.
1750    ///
1751    /// C: paired `tonumberns(v1, n1) && tonumberns(v2, n2)` from `op_arith_aux`.
1752    #[inline]
1753    pub fn get_num_pair_at(
1754        &self,
1755        rb: impl Into<StackIdxConv>,
1756        rc: impl Into<StackIdxConv>,
1757    ) -> Option<(f64, f64)> {
1758        let nb = self.get_num_at(rb)?;
1759        let nc = self.get_num_at(rc)?;
1760        Some((nb, nc))
1761    }
1762    /// Set `top` to an absolute stack index. Grows the backing stack vector
1763    /// (filling new slots with `Nil`) when `idx` is past `stack.len()`, but
1764    /// never clobbers existing slots between the old top and the new top —
1765    /// VM opcodes (Call, ForPrep, etc.) write registers via `set_at` and then
1766    /// raise `top` to signal "these are now live"; nil-filling here would
1767    /// erase the just-written values.
1768    ///
1769    /// C: internal `L->top.p = newtop` assignment. The `for (; diff > 0; …)
1770    /// setnilvalue(s2v(L->top.p++))` clear loop in `lua_settop` (lapi.c) is
1771    /// part of the public API path and lives in `api::set_top` instead.
1772    /// PORT NOTE: callers pass an absolute `StackIdx`, not the relative `idx`
1773    /// of the public `lua_settop`. The to-be-closed (`tbclist`) close path
1774    /// is Phase E and not handled here.
1775    #[inline(always)]
1776    pub fn set_top(&mut self, idx: impl Into<StackIdxConv>) {
1777        let new_top: StackIdx = idx.into().0;
1778        let new_top_u = new_top.0 as usize;
1779        if new_top_u > self.stack.len() {
1780            self.stack.resize_with(new_top_u, StackValue::default);
1781        }
1782        self.top = new_top;
1783    }
1784    /// Primitive "set top index" — just writes `self.top`, no nil-fill.
1785    ///
1786    /// C: tail of `lua_settop` (lapi.c) — `L->top.p = newtop;`
1787    /// PORT NOTE: callers (`api.rs::set_top`, `raw_set`, etc.) pre-nil-fill or
1788    /// only shrink, so this routine intentionally does no clearing or resizing.
1789    /// The to-be-closed (`tbclist`) close path is Phase E.
1790    #[inline(always)]
1791    pub fn set_top_idx(&mut self, idx: impl Into<StackIdxConv>) {
1792        let new_top: StackIdx = idx.into().0;
1793        self.top = new_top;
1794    }
1795    /// Decrement `top` by 1 (saturating at zero).
1796    ///
1797    /// C: `L->top.p--` — drop one slot from the stack without reading it.
1798    #[inline(always)]
1799    pub fn dec_top(&mut self) {
1800        if self.top.0 > 0 {
1801            self.top = StackIdx(self.top.0 - 1);
1802        }
1803    }
1804    #[inline(always)]
1805    pub fn pop_n(&mut self, n: usize) {
1806        let cur = self.top.0 as usize;
1807        let new = cur.saturating_sub(n);
1808        self.top = StackIdx(new as u32);
1809    }
1810    /// Returns the value at the given stack index without removing it.
1811    ///
1812    /// C: `s2v(L->stack.p + idx)` for a fixed absolute index.
1813    #[inline(always)]
1814    pub fn peek_at(&mut self, idx: impl Into<StackIdxConv>) -> LuaValue {
1815        let i: StackIdx = idx.into().0;
1816        match self.stack.get(i.0 as usize) {
1817            Some(slot) => slot.val.clone(),
1818            None => LuaValue::Nil,
1819        }
1820    }
1821    /// Returns the value just below `top` (the topmost live slot) without
1822    /// removing it.
1823    ///
1824    /// C: `s2v(L->top.p - 1)`.
1825    #[inline(always)]
1826    pub fn peek_top(&mut self) -> LuaValue {
1827        if self.top.0 == 0 {
1828            return LuaValue::Nil;
1829        }
1830        self.stack[(self.top.0 - 1) as usize].val.clone()
1831    }
1832    /// Returns the topmost slot interpreted as a string. Panics if the slot
1833    /// is not a `LuaValue::Str`. Callers (e.g. `luaO_pushvfstring`) guarantee
1834    /// the value has been pushed as an interned string immediately prior.
1835    ///
1836    /// C: `getstr(tsvalue(s2v(L->top.p - 1)))`.
1837    pub fn peek_string_at_top(&mut self) -> GcRef<LuaString> {
1838        match self.peek_top() {
1839            LuaValue::Str(s) => s,
1840            _ => panic!("peek_string_at_top: top of stack is not a string"),
1841        }
1842    }
1843    /// Mutable reference to the value at the given stack slot.
1844    ///
1845    /// C: `s2v(L->stack.p + idx)` used as an lvalue.
1846    pub fn stack_at(&mut self, idx: impl Into<StackIdxConv>) -> &mut LuaValue {
1847        let i: StackIdx = idx.into().0;
1848        &mut self.stack[i.0 as usize].val
1849    }
1850    /// Writes `Nil` to the given stack slot.
1851    ///
1852    /// C: `setnilvalue(s2v(L->stack.p + idx))`.
1853    pub fn stack_set_nil(&mut self, idx: impl Into<StackIdxConv>) {
1854        let i: StackIdx = idx.into().0;
1855        let slot = i.0 as usize;
1856        if slot < self.stack.len() {
1857            self.stack[slot].val = LuaValue::Nil;
1858        }
1859    }
1860    /// Resizes the underlying stack vector to `size` slots, padding new slots
1861    /// with `StackValue::default()` (which is `Nil`). Returns `Ok(())` on
1862    /// success — `Vec::resize_with` in Rust does not have a fallible path the
1863    /// way `luaM_reallocvector` does in C, so the `Result` is here for
1864    /// signature parity with future fallible allocators.
1865    ///
1866    /// C: `luaM_reallocvector(L, L->stack.p, oldsize+EXTRA_STACK,
1867    ///                         newsize+EXTRA_STACK, StackValue)`.
1868    pub fn stack_resize(&mut self, size: usize) -> Result<(), LuaError> {
1869        self.stack.resize_with(size, StackValue::default);
1870        Ok(())
1871    }
1872    pub fn stack_available(&mut self) -> usize {
1873        (self.stack_last.0 as usize).saturating_sub(self.top.0 as usize)
1874    }
1875    pub fn check_stack(&mut self, n: i32) -> Result<(), LuaError> {
1876        let free = (self.stack_last.0 as i32) - (self.top.0 as i32);
1877        if free <= n {
1878            self.grow_stack(n, true)?;
1879        }
1880        Ok(())
1881    }
1882    /// Inherent method wrapper around the free function `do_::grow_stack`,
1883    /// preserving the historical `Result<(), LuaError>` signature used by
1884    /// `check_stack` and other VM call sites. The bool returned by the
1885    /// underlying implementation distinguishes soft failure (when
1886    /// `raise_error` is false) from success; that distinction is dropped here
1887    /// because every current caller passes `raise_error = true` and only
1888    /// cares about error propagation.
1889    ///
1890    /// C: `int luaD_growstack(lua_State *L, int n, int raiseerror)`.
1891    pub fn grow_stack(&mut self, n: i32, raise_error: bool) -> Result<(), LuaError> {
1892        crate::do_::grow_stack(self, n, raise_error).map(|_| ())
1893    }
1894
1895    #[inline(always)]
1896    pub fn get_ci(&self, idx: CallInfoIdx) -> &CallInfo { &self.call_info[idx.as_usize()] }
1897    #[inline(always)]
1898    pub fn get_ci_mut(&mut self, idx: CallInfoIdx) -> &mut CallInfo { &mut self.call_info[idx.as_usize()] }
1899    #[inline(always)]
1900    pub fn current_call_info(&self) -> &CallInfo { &self.call_info[self.ci.as_usize()] }
1901    #[inline(always)]
1902    pub fn current_call_info_mut(&mut self) -> &mut CallInfo { let i = self.ci.as_usize(); &mut self.call_info[i] }
1903    #[inline(always)]
1904    pub fn current_ci_idx(&self) -> CallInfoIdx { self.ci }
1905    pub fn call_stack_mut(&mut self) -> &mut Vec<CallInfo> { &mut self.call_info }
1906    #[inline(always)]
1907    pub fn next_ci(&mut self) -> Result<CallInfoIdx, LuaError> {
1908        match self.call_info[self.ci.as_usize()].next {
1909            Some(idx) => Ok(idx),
1910            None => Ok(extend_ci(self)),
1911        }
1912    }
1913    #[inline(always)]
1914    pub fn prev_ci(&self, idx: CallInfoIdx) -> Option<CallInfoIdx> { self.call_info[idx.as_usize()].previous }
1915    pub fn get_prev_ci(&self, idx: CallInfoIdx) -> Option<&CallInfo> {
1916        self.call_info[idx.as_usize()]
1917            .previous
1918            .map(|p| &self.call_info[p.as_usize()])
1919    }
1920    #[inline(always)]
1921    pub fn is_base_ci(&self, idx: CallInfoIdx) -> bool { idx.as_usize() == 0 }
1922    #[inline(always)]
1923    pub fn is_current_ci(&self, idx: CallInfoIdx) -> bool { idx == self.ci }
1924    pub fn ci_next_func(&self, idx: CallInfoIdx) -> StackIdx {
1925        let next = self.call_info[idx.as_usize()]
1926            .next
1927            .expect("ci_next_func: no next CallInfo");
1928        self.call_info[next.as_usize()].func
1929    }
1930    #[inline(always)]
1931    pub fn ci_top(&self, idx: CallInfoIdx) -> StackIdx { self.call_info[idx.as_usize()].top }
1932    #[inline(always)]
1933    pub fn ci_trap(&mut self, idx: CallInfoIdx) -> bool {
1934        if let CallInfoFrame::Lua { trap, .. } = self.call_info[idx.as_usize()].u {
1935            trap
1936        } else {
1937            false
1938        }
1939    }
1940    #[inline(always)]
1941    pub fn ci_savedpc(&self, idx: CallInfoIdx) -> u32 { self.call_info[idx.as_usize()].saved_pc() }
1942    #[inline(always)]
1943    pub fn set_ci_savedpc(&mut self, idx: CallInfoIdx, pc: u32) {
1944        self.call_info[idx.as_usize()].set_saved_pc(pc);
1945    }
1946    #[inline(always)]
1947    pub fn set_ci_previous(&mut self, idx: CallInfoIdx) {
1948        self.ci = self.call_info[idx.as_usize()]
1949            .previous
1950            .expect("set_ci_previous: returning frame has no previous CallInfo");
1951    }
1952    #[inline(always)]
1953    pub fn ci_previous(&self, idx: CallInfoIdx) -> Option<CallInfoIdx> { self.call_info[idx.as_usize()].previous }
1954    #[inline(always)]
1955    pub fn ci_adjust_func(&mut self, idx: CallInfoIdx, delta: i32) {
1956        let ci = &mut self.call_info[idx.as_usize()];
1957        ci.func = StackIdx((ci.func.0 as i32 - delta) as u32);
1958    }
1959    #[inline(always)]
1960    pub fn ci_base(&self, idx: CallInfoIdx) -> StackIdx { self.call_info[idx.as_usize()].func + 1 }
1961    #[inline(always)]
1962    pub fn ci_is_fresh(&self, idx: CallInfoIdx) -> bool {
1963        (self.call_info[idx.as_usize()].callstatus & CIST_FRESH) != 0
1964    }
1965    #[inline(always)]
1966    pub fn ci_lua_closure(&self, idx: CallInfoIdx) -> Option<GcRef<lua_types::closure::LuaLClosure>> {
1967        let func_idx = self.call_info[idx.as_usize()].func;
1968        match self.get_at(func_idx) {
1969            LuaValue::Function(lua_types::closure::LuaClosure::Lua(cl)) => Some(cl),
1970            _ => None,
1971        }
1972    }
1973    #[inline(always)]
1974    pub fn ci_nextraargs(&self, idx: CallInfoIdx) -> i32 {
1975        self.call_info[idx.as_usize()].nextra_args()
1976    }
1977    #[inline(always)]
1978    pub fn ci_nres(&self, idx: CallInfoIdx) -> i32 {
1979        self.call_info[idx.as_usize()].u2.value
1980    }
1981    #[inline(always)]
1982    pub fn ci_nres_set(&mut self, idx: CallInfoIdx, n: i32) {
1983        self.call_info[idx.as_usize()].u2.value = n;
1984    }
1985    #[inline(always)]
1986    pub fn ci_nresults(&self, idx: CallInfoIdx) -> i32 { self.call_info[idx.as_usize()].nresults as i32 }
1987    pub fn ci_prev_instruction(&self, idx: CallInfoIdx) -> lua_types::opcode::Instruction {
1988        let pc = self.call_info[idx.as_usize()].saved_pc();
1989        let cl = self.ci_lua_closure(idx)
1990            .expect("ci_prev_instruction: CallInfo does not hold a Lua closure");
1991        cl.proto.code[(pc - 1) as usize]
1992    }
1993    pub fn ci_prev2_instruction(&self, idx: CallInfoIdx) -> lua_types::opcode::Instruction {
1994        let pc = self.call_info[idx.as_usize()].saved_pc();
1995        let cl = self.ci_lua_closure(idx)
1996            .expect("ci_prev2_instruction: CallInfo does not hold a Lua closure");
1997        cl.proto.code[(pc - 2) as usize]
1998    }
1999    pub fn ci_skip_next_instruction(&mut self, idx: CallInfoIdx) {
2000        let pc = self.call_info[idx.as_usize()].saved_pc();
2001        self.call_info[idx.as_usize()].set_saved_pc(pc + 1);
2002    }
2003    pub fn ci_step_pc_back(&mut self, idx: CallInfoIdx) {
2004        let pc = self.call_info[idx.as_usize()].saved_pc();
2005        self.call_info[idx.as_usize()].set_saved_pc(pc - 1);
2006    }
2007    pub fn get_ci_pcrel(&mut self, idx: CallInfoIdx) -> u32 {
2008        self.call_info[idx.as_usize()].saved_pc().saturating_sub(1)
2009    }
2010    pub fn get_ci_u2_funcidx(&mut self, idx: CallInfoIdx) -> i32 {
2011        self.call_info[idx.as_usize()].u2.value
2012    }
2013    pub fn get_ci_u2_nres(&mut self, idx: CallInfoIdx) -> i32 {
2014        self.call_info[idx.as_usize()].u2.value
2015    }
2016    pub fn get_ci_u2_nyield(&mut self, idx: CallInfoIdx) -> i32 {
2017        self.call_info[idx.as_usize()].u2.value
2018    }
2019    pub fn get_ci_vararg_info(&mut self, idx: CallInfoIdx) -> (bool, i32, i32) {
2020        let nextraargs = self.call_info[idx.as_usize()].nextra_args();
2021        match self.ci_lua_closure(idx) {
2022            Some(cl) => (cl.proto.is_vararg, nextraargs, cl.proto.numparams as i32),
2023            None => (false, nextraargs, 0),
2024        }
2025    }
2026    pub fn get_ci_lua_proto_numparams(&mut self, idx: CallInfoIdx) -> u8 {
2027        self.ci_lua_closure(idx)
2028            .map(|cl| cl.proto.numparams)
2029            .unwrap_or(0)
2030    }
2031    pub fn set_ci_u2_nres(&mut self, idx: CallInfoIdx, n: i32) {
2032        self.call_info[idx.as_usize()].u2.value = n;
2033    }
2034    pub fn set_ci_u2_nyield(&mut self, idx: CallInfoIdx, n: i32) {
2035        self.call_info[idx.as_usize()].u2.value = n;
2036    }
2037    pub fn set_ci_transfer_info(&mut self, idx: CallInfoIdx, ftransfer: u16, ntransfer: u16) {
2038        let ci = &mut self.call_info[idx.as_usize()];
2039        ci.u2.ftransfer = ftransfer;
2040        ci.u2.ntransfer = ntransfer;
2041    }
2042    pub fn shrink_ci(&mut self) { shrink_ci(self) }
2043    pub fn check_c_stack(&mut self) -> Result<(), LuaError> { check_c_stack(self) }
2044
2045    pub fn status(&mut self) -> LuaStatus { LuaStatus::from_raw(self.status as i32) }
2046    pub fn errfunc(&mut self) -> isize { self.errfunc }
2047    pub fn old_pc(&mut self) -> u32 { self.oldpc }
2048    pub fn set_old_pc(&mut self, pc: u32) { self.oldpc = pc; }
2049    pub fn set_oldpc(&mut self, pc: u32) { self.oldpc = pc; }
2050    pub fn _hook_call_noargs(&mut self) {}
2051    pub fn hook(&self) -> Option<&Box<dyn FnMut(&mut LuaState, &crate::debug::LuaDebug)>> {
2052        self.hook.as_ref()
2053    }
2054    pub fn has_hook(&mut self) -> bool { self.hook.is_some() }
2055    pub fn hook_count(&mut self) -> i32 { self.hookcount }
2056    pub fn set_hook_count(&mut self, n: i32) { self.hookcount = n; }
2057    pub fn hook_mask(&self) -> u8 { self.hookmask }
2058    pub fn set_hook_mask(&mut self, m: u8) { self.hookmask = m; }
2059    pub fn base_hook_count(&self) -> i32 { self.basehookcount }
2060    pub fn set_base_hook_count(&mut self, n: i32) { self.basehookcount = n; }
2061    pub fn set_hook(&mut self, h: Option<Box<dyn FnMut(&mut LuaState, &crate::debug::LuaDebug)>>) {
2062        self.hook = h;
2063    }
2064    pub fn call_hook_event(&mut self, event: i32, line: i32) -> Result<(), LuaError> {
2065        crate::do_::hook(self, event, line, 0, 0)
2066    }
2067
2068    pub fn registry_value(&self) -> LuaValue { self.global().l_registry.clone() }
2069    pub fn registry_get(&self, key: usize) -> LuaValue {
2070        let reg = self.global().l_registry.clone();
2071        match reg {
2072            LuaValue::Table(t) => t.get(&LuaValue::Int(key as i64)),
2073            _ => LuaValue::Nil,
2074        }
2075    }
2076
2077    pub fn new_string(&mut self, bytes: &[u8]) -> Result<GcRef<LuaString>, LuaError> { self.intern_or_create_str(bytes) }
2078
2079    // ── Phase D-1a: state-owned allocation API ──────────────────────────────
2080    // These methods are the canonical allocation surface. They wrap
2081    // `GcRef::new` today; at D-1e they route through `state.global.heap.allocate`.
2082    // Callers must reach them through `&mut LuaState`, which mirrors C-Lua's
2083    // requirement that every allocation passes `lua_State *L`.
2084
2085    /// Allocate a new Lua function prototype.
2086    ///
2087    /// Caller mutates the returned proto in place (it's behind GcRef, which is
2088    /// Rc during Phase D-1; mutable access via `Rc::get_mut` only works while
2089    /// no other GcRefs alias it — true at construction).
2090    pub fn new_proto(&mut self) -> GcRef<LuaProto> {
2091        GcRef::new(LuaProto::placeholder())
2092    }
2093
2094    /// Allocate a Lua-side closure (compiled function + upvalue slots).
2095    pub fn new_lclosure(&mut self, proto: GcRef<LuaProto>, nupvals: usize) -> GcRef<LuaClosureLua> {
2096        let mut upvals = Vec::with_capacity(nupvals);
2097        for _ in 0..nupvals {
2098            upvals.push(std::cell::Cell::new(self.new_upval_closed(LuaValue::Nil)));
2099        }
2100        GcRef::new(LuaClosureLua { proto, upvals })
2101    }
2102
2103    /// Allocate a closed upvalue holding the given value.
2104    pub fn new_upval_closed(&mut self, v: LuaValue) -> GcRef<UpVal> {
2105        GcRef::new(UpVal::closed(v))
2106    }
2107
2108    /// Allocate an open upvalue referring to a thread's stack slot.
2109    pub fn new_upval_open(&mut self, thread_id: usize, level: StackIdx) -> GcRef<UpVal> {
2110        GcRef::new(UpVal::open(thread_id, level))
2111    }
2112    /// Mirrors `luaS_newlstr`: short strings are interned globally so equal
2113    /// content shares a single TString; long strings (> LUAI_MAXSHORTLEN = 40)
2114    /// always create a fresh TString without interning. This is what lets
2115    /// `string.format("%p", "long" .. "concat")` differ from a same-content
2116    /// literal — concat must produce a new object even when the literal already
2117    /// lives in the lexer's constant pool.
2118    pub fn intern_or_create_str(&mut self, bytes: &[u8]) -> Result<GcRef<LuaString>, LuaError> {
2119        self.intern_str(bytes)
2120    }
2121    pub fn new_userdata(&mut self, _size: usize, _nuvalue: usize) -> Result<GcRef<LuaUserData>, LuaError> {
2122        Err(LuaError::runtime(format_args!("new_userdata not implemented in this Phase-B build; use new_userdata_typed instead")))
2123    }
2124    pub fn new_c_closure(&mut self, _f: LuaCFunction, _n: i32) -> Result<LuaClosure, LuaError> {
2125        Err(LuaError::runtime(format_args!("new_c_closure not implemented in this Phase-B build; use push_cclosure in lua_vm::api instead")))
2126    }
2127    pub fn push_closure(
2128        &mut self,
2129        proto_idx: usize,
2130        ci: CallInfoIdx,
2131        base: StackIdx,
2132        ra: StackIdx,
2133    ) -> Result<(), LuaError> {
2134        let parent_cl = self.ci_lua_closure(ci).expect(
2135            "push_closure: current frame is not a Lua closure",
2136        );
2137        let child_proto = parent_cl.proto.p[proto_idx].clone();
2138        let nup = child_proto.upvalues.len();
2139        let mut upvals: Vec<std::cell::Cell<GcRef<UpVal>>> = Vec::with_capacity(nup);
2140        for i in 0..nup {
2141            let desc = &child_proto.upvalues[i];
2142            let uv = if desc.instack {
2143                let level = base + desc.idx as i32;
2144                crate::func::find_upval(self, level)
2145            } else {
2146                parent_cl.upval(desc.idx as usize)
2147            };
2148            upvals.push(std::cell::Cell::new(uv));
2149        }
2150        // TODO(D-1c-bridge): upvals are pre-populated from parent frame; state.new_lclosure
2151        // fills with fresh Nil upvals which would drop the captured bindings.
2152        let new_cl = GcRef::new(LuaClosureLua {
2153            proto: child_proto,
2154            upvals,
2155        });
2156        self.set_at(ra, LuaValue::Function(LuaClosure::Lua(new_cl)));
2157        Ok(())
2158    }
2159    pub fn new_tbc_upval(&mut self, idx: StackIdx) -> Result<(), LuaError> {
2160        crate::func::new_tbc_upval(self, idx)
2161    }
2162
2163    /// Read an open or closed upvalue.
2164    ///
2165    /// Closed upvalues own their value and read trivially. Open upvalues
2166    /// point at a stack slot on the home thread that captured them.
2167    ///
2168    /// Resolution order for an open upvalue whose home is not the current
2169    /// thread:
2170    ///
2171    /// 1. If the home thread is registered in `GlobalState::threads` and
2172    ///    its `RefCell` is currently borrowable, read straight from its
2173    ///    stack. This is the path used when the main thread reads a
2174    ///    closure created inside a now-suspended coroutine, or when one
2175    ///    coroutine reads an upvalue homed on a sibling suspended
2176    ///    coroutine.
2177    /// 2. Otherwise fall back to `GlobalState::cross_thread_upvals`. This
2178    ///    is the path used while inside a `coroutine.resume`: the parent
2179    ///    thread's `LuaState` is held by an outer `&mut` and is not
2180    ///    reachable through any `Rc<RefCell<_>>`, so `aux_resume`
2181    ///    snapshots the parent's open upvalues into the mirror across the
2182    ///    resume boundary.
2183    #[inline(always)]
2184    pub fn upvalue_get(&self, cl: &GcRef<LuaClosureLua>, n: usize) -> LuaValue {
2185        let uv = cl.upval(n);
2186        let (thread_id, idx) = match uv.try_open_payload() {
2187            Some(p) => p,
2188            None => return *uv.closed_value(),
2189        };
2190        let current = self.cached_thread_id;
2191        let tid = thread_id as u64;
2192        if tid == current {
2193            return self.stack[idx.0 as usize].val;
2194        }
2195        self.upvalue_get_cross_thread(tid, idx)
2196    }
2197
2198    #[cold]
2199    #[inline(never)]
2200    fn upvalue_get_cross_thread(&self, tid: u64, idx: StackIdx) -> LuaValue {
2201        let entry_rc = {
2202            let g = self.global();
2203            g.threads.get(&tid).map(|e| e.state.clone())
2204        };
2205        if let Some(rc) = entry_rc {
2206            if let Ok(home_state) = rc.try_borrow() {
2207                return home_state.get_at(idx);
2208            }
2209        }
2210        let g = self.global();
2211        match g.cross_thread_upvals.get(&(tid, idx)) {
2212            Some(v) => *v,
2213            None => LuaValue::Nil,
2214        }
2215    }
2216    /// Write an open or closed upvalue.
2217    ///
2218    /// Mirrors [`upvalue_get`]: open upvalues homed on the current thread
2219    /// write through `self.stack`. For cross-thread open upvalues, the
2220    /// home thread's stack is written directly when its `RefCell` is
2221    /// borrowable, otherwise the write lands in
2222    /// `GlobalState::cross_thread_upvals` (the active-resume case where
2223    /// the home thread is borrow-locked further up the call stack).
2224    #[inline(always)]
2225    pub fn upvalue_set(&mut self, cl: &GcRef<LuaClosureLua>, n: usize, val: LuaValue) -> Result<(), LuaError> {
2226        let uv = cl.upval(n);
2227        match uv.try_open_payload() {
2228            Some((thread_id, idx)) => {
2229                let tid = thread_id as u64;
2230                let current = self.cached_thread_id;
2231                if tid == current {
2232                    self.stack[idx.0 as usize].val = val;
2233                    return Ok(());
2234                }
2235                return self.upvalue_set_cross_thread(tid, idx, val);
2236            }
2237            None => {
2238                uv.set_closed_value(val);
2239            }
2240        }
2241        Ok(())
2242    }
2243
2244    #[cold]
2245    #[inline(never)]
2246    fn upvalue_set_cross_thread(
2247        &mut self,
2248        tid: u64,
2249        idx: StackIdx,
2250        val: LuaValue,
2251    ) -> Result<(), LuaError> {
2252        let entry_rc = {
2253            let g = self.global();
2254            g.threads.get(&tid).map(|e| e.state.clone())
2255        };
2256        if let Some(rc) = entry_rc {
2257            if let Ok(mut home_state) = rc.try_borrow_mut() {
2258                home_state.set_at(idx, val);
2259                return Ok(());
2260            }
2261        }
2262        let mut g = self.global_mut();
2263        g.cross_thread_upvals.insert((tid, idx), val);
2264        Ok(())
2265    }
2266
2267    pub fn protected_call_raw(&mut self, func: StackIdx, nresults: i32, errfunc: StackIdx) -> Result<(), LuaError> {
2268        let ef = errfunc.0 as isize;
2269        let status = crate::do_::pcall(
2270            self,
2271            |s| s.call_no_yield(func, nresults),
2272            func,
2273            ef,
2274        );
2275        match status {
2276            LuaStatus::Ok => Ok(()),
2277            LuaStatus::ErrSyntax => {
2278                let err_val = self.get_at(func);
2279                self.set_top(func);
2280                Err(LuaError::Syntax(err_val))
2281            }
2282            LuaStatus::Yield => {
2283                self.set_top(func);
2284                Err(LuaError::Yield)
2285            }
2286            _ => {
2287                let err_val = self.get_at(func);
2288                self.set_top(func);
2289                Err(LuaError::Runtime(err_val))
2290            }
2291        }
2292    }
2293    pub fn protected_parser(&mut self, z: crate::zio::ZIO, name: &[u8], mode: Option<&[u8]>) -> LuaStatus {
2294        crate::do_::protected_parser(self, z, name, mode)
2295    }
2296    pub fn do_call(&mut self, func: StackIdx, nresults: i32) -> Result<(), LuaError> {
2297        crate::do_::call(self, func, nresults)
2298    }
2299    pub fn do_call_no_yield(&mut self, func: StackIdx, nresults: i32) -> Result<(), LuaError> {
2300        crate::do_::callnoyield(self, func, nresults)
2301    }
2302    pub fn call_no_yield(&mut self, func: StackIdx, nresults: i32) -> Result<(), LuaError> {
2303        crate::do_::callnoyield(self, func, nresults)
2304    }
2305    pub fn call_at(&mut self, func: StackIdx, nresults: i32) -> Result<(), LuaError> {
2306        crate::do_::call(self, func, nresults)
2307    }
2308    #[inline(always)]
2309    pub fn precall(&mut self, func: StackIdx, nresults: i32) -> Result<Option<CallInfoIdx>, LuaError> {
2310        crate::do_::precall(self, func, nresults)
2311    }
2312    #[inline(always)]
2313    pub fn pretailcall(
2314        &mut self,
2315        ci: CallInfoIdx,
2316        func: StackIdx,
2317        narg1: i32,
2318        delta: i32,
2319    ) -> Result<i32, LuaError> {
2320        crate::do_::pretailcall(self, ci, func, narg1, delta)
2321    }
2322    #[inline(always)]
2323    pub fn poscall<N: TryInto<i32>>(&mut self, ci: CallInfoIdx, nres: N) -> Result<(), LuaError>
2324    where
2325        <N as TryInto<i32>>::Error: std::fmt::Debug,
2326    {
2327        let n = nres.try_into().expect("poscall: nres out of i32 range");
2328        crate::do_::poscall(self, ci, n)
2329    }
2330    pub fn adjust_results(&mut self, nresults: i32) {
2331        const LUA_MULTRET: i32 = -1;
2332        if nresults <= LUA_MULTRET {
2333            let ci_idx = self.ci.as_usize();
2334            if self.call_info[ci_idx].top.0 < self.top.0 {
2335                self.call_info[ci_idx].top = self.top;
2336            }
2337        }
2338    }
2339    pub fn adjust_varargs(
2340        &mut self,
2341        ci: CallInfoIdx,
2342        nfixparams: i32,
2343        cl: &GcRef<lua_types::closure::LuaLClosure>,
2344    ) -> Result<(), LuaError> {
2345        crate::tagmethods::adjust_varargs(self, nfixparams, ci, &cl.0.proto)
2346    }
2347    pub fn get_varargs(
2348        &mut self,
2349        ci: CallInfoIdx,
2350        ra: StackIdx,
2351        n: i32,
2352    ) -> Result<i32, LuaError> {
2353        crate::tagmethods::get_varargs(self, ci, ra, n)?;
2354        Ok(0)
2355    }
2356
2357    pub fn close_upvals(&mut self, level: StackIdx) -> Result<(), LuaError> {
2358        crate::func::close_upval(self, level);
2359        Ok(())
2360    }
2361    pub fn close_upvals_status(&mut self, level: StackIdx, _status: i32) -> Result<(), LuaError> {
2362        crate::func::close_upval(self, level);
2363        Ok(())
2364    }
2365    pub fn close_upvals_from_base(&mut self, ci: CallInfoIdx) -> Result<(), LuaError> {
2366        let base = self.ci_base(ci);
2367        crate::func::close_upval(self, base);
2368        Ok(())
2369    }
2370
2371    pub fn arith_op(&mut self, op: i32, p1: &LuaValue, p2: &LuaValue) -> Result<LuaValue, LuaError> {
2372        let arith_op = match op {
2373            0  => lua_types::arith::ArithOp::Add,
2374            1  => lua_types::arith::ArithOp::Sub,
2375            2  => lua_types::arith::ArithOp::Mul,
2376            3  => lua_types::arith::ArithOp::Mod,
2377            4  => lua_types::arith::ArithOp::Pow,
2378            5  => lua_types::arith::ArithOp::Div,
2379            6  => lua_types::arith::ArithOp::Idiv,
2380            7  => lua_types::arith::ArithOp::Band,
2381            8  => lua_types::arith::ArithOp::Bor,
2382            9  => lua_types::arith::ArithOp::Bxor,
2383            10 => lua_types::arith::ArithOp::Shl,
2384            11 => lua_types::arith::ArithOp::Shr,
2385            12 => lua_types::arith::ArithOp::Unm,
2386            13 => lua_types::arith::ArithOp::Bnot,
2387            _  => return Err(LuaError::runtime(format_args!("invalid arith op {}", op))),
2388        };
2389        let mut res = LuaValue::Nil;
2390        if crate::object::raw_arith(self, arith_op, p1, p2, &mut res)? {
2391            Ok(res)
2392        } else {
2393            Err(LuaError::arith_error(p1, p2, "perform arithmetic on"))
2394        }
2395    }
2396    pub fn concat(&mut self, n: i32) -> Result<(), LuaError> {
2397        crate::vm::concat(self, n)
2398    }
2399    pub fn less_than(&mut self, l: &LuaValue, r: &LuaValue) -> Result<bool, LuaError> {
2400        crate::vm::less_than(self, l, r)
2401    }
2402    pub fn less_equal(&mut self, l: &LuaValue, r: &LuaValue) -> Result<bool, LuaError> {
2403        crate::vm::less_equal(self, l, r)
2404    }
2405    pub fn equal_obj(&self, _ctx: Option<&LuaValue>, l: &LuaValue, r: &LuaValue) -> bool {
2406        crate::vm::equal_obj(None, l, r).unwrap_or(false)
2407    }
2408    pub fn equal_obj_with_tm(&mut self, l: &LuaValue, r: &LuaValue) -> Result<bool, LuaError> {
2409        crate::vm::equal_obj(Some(self), l, r)
2410    }
2411    pub fn obj_len(&mut self, v: &LuaValue) -> Result<LuaValue, LuaError> {
2412        match v {
2413            LuaValue::Table(_) => {
2414                let mt = self.table_metatable(v);
2415                let tm = self.fast_tm_table(mt.as_ref(), TagMethod::Len);
2416                if matches!(tm, LuaValue::Nil) {
2417                    let n = self.table_length(v)?;
2418                    return Ok(LuaValue::Int(n));
2419                }
2420                self.push(LuaValue::Nil);
2421                let slot = StackIdx(self.top.0 - 1);
2422                crate::tagmethods::call_tm_res(self, tm, v.clone(), v.clone(), slot)?;
2423                Ok(self.pop())
2424            }
2425            LuaValue::Str(s) => Ok(LuaValue::Int(s.len() as i64)),
2426            other => {
2427                let tm = crate::tagmethods::get_tm_by_obj(self, other, crate::tagmethods::TagMethod::Len);
2428                if matches!(tm, LuaValue::Nil) {
2429                    return Err(LuaError::type_error(other, "get length of"));
2430                }
2431                self.push(LuaValue::Nil);
2432                let slot = StackIdx(self.top.0 - 1);
2433                crate::tagmethods::call_tm_res(self, tm, v.clone(), v.clone(), slot)?;
2434                Ok(self.pop())
2435            }
2436        }
2437    }
2438    pub fn obj_to_string(&mut self, idx: i32) -> Result<GcRef<LuaString>, LuaError> {
2439        let slot: StackIdx = if idx > 0 {
2440            let ci_func = self.current_call_info().func;
2441            ci_func + idx
2442        } else {
2443            debug_assert!(idx != 0, "invalid index");
2444            StackIdx((self.top_idx().0 as i32 + idx) as u32)
2445        };
2446        let val = self.get_at(slot);
2447        match val {
2448            LuaValue::Str(s) => Ok(s),
2449            LuaValue::Int(_) | LuaValue::Float(_) => {
2450                let s = crate::object::num_to_string(self, &val)?;
2451                self.set_at(slot, LuaValue::Str(s.clone()));
2452                Ok(s)
2453            }
2454            _ => Err(LuaError::type_error(&val, "convert to string")),
2455        }
2456    }
2457    pub fn coerce_to_string(&mut self, idx: StackIdx) -> Result<GcRef<LuaString>, LuaError> {
2458        let val = self.get_at(idx);
2459        match val {
2460            LuaValue::Str(s) => Ok(s),
2461            LuaValue::Int(_) | LuaValue::Float(_) => {
2462                let s = crate::object::num_to_string(self, &val)?;
2463                self.set_at(idx, LuaValue::Str(s.clone()));
2464                Ok(s)
2465            }
2466            _ => Err(LuaError::type_error(&val, "convert to string")),
2467        }
2468    }
2469    pub fn str_to_num(&mut self, s: &[u8]) -> Option<(LuaValue, usize)> {
2470        let mut out = LuaValue::Nil;
2471        let sz = crate::object::str2num(s, &mut out);
2472        if sz == 0 { None } else { Some((out, sz)) }
2473    }
2474
2475    pub fn fast_get(&mut self, t: &LuaValue, k: &LuaValue) -> Result<Option<LuaValue>, LuaError> {
2476        let LuaValue::Table(tbl) = t else { return Ok(None); };
2477        let v = tbl.get(k);
2478        if matches!(v, LuaValue::Nil) { Ok(None) } else { Ok(Some(v)) }
2479    }
2480    pub fn fast_get_int(&mut self, t: &LuaValue, k: i64) -> Result<Option<LuaValue>, LuaError> {
2481        let LuaValue::Table(tbl) = t else { return Ok(None); };
2482        let v = tbl.get_int(k);
2483        if matches!(v, LuaValue::Nil) { Ok(None) } else { Ok(Some(v)) }
2484    }
2485    pub fn fast_get_short_str(&mut self, t: &LuaValue, k: &LuaValue) -> Result<Option<LuaValue>, LuaError> {
2486        let LuaValue::Table(tbl) = t else { return Ok(None); };
2487        let LuaValue::Str(s) = k else { return Ok(None); };
2488        let v = tbl.get_short_str(s);
2489        if matches!(v, LuaValue::Nil) { Ok(None) } else { Ok(Some(v)) }
2490    }
2491    pub fn fast_tm_table(&mut self, t: Option<&GcRef<LuaTable>>, tm: TagMethod) -> LuaValue {
2492        let Some(mt) = t else { return LuaValue::Nil; };
2493        debug_assert!((tm as u8) <= TagMethod::Eq as u8);
2494        let ename = self.global().tmname[tm as usize].clone();
2495        mt.get_short_str(&ename)
2496    }
2497    pub fn fast_tm_ud(&mut self, u: &GcRef<LuaUserData>, tm: TagMethod) -> LuaValue {
2498        // C: fasttm(L, uvalue(o)->metatable, event) — read the userdata's
2499        // metatable then index by the interned `__xxx` name.
2500        let mt = u.metatable();
2501        self.fast_tm_table(mt.as_ref(), tm)
2502    }
2503
2504    pub fn table_get_with_tm(&mut self, t: &LuaValue, k: &LuaValue) -> Result<LuaValue, LuaError> {
2505        // Fast path: when the table has no metatable, `__index` can never
2506        // fire — so we can return the raw slot value (Nil if absent) without
2507        // routing through finish_get's push/pop scaffolding. Halves the
2508        // get-hot-path cost on tables without metamethods, which is the
2509        // common case in table.remove/insert shift loops and most user code.
2510        if let LuaValue::Table(tbl) = t {
2511            if tbl.metatable().is_none() {
2512                return Ok(tbl.get(k));
2513            }
2514        }
2515        if let Some(v) = self.fast_get(t, k)? {
2516            return Ok(v);
2517        }
2518        let res = self.top_idx();
2519        self.push(LuaValue::Nil);
2520        crate::vm::finish_get(self, t.clone(), k.clone(), res, true, None)?;
2521        let value = self.get_at(res);
2522        self.pop();
2523        Ok(value)
2524    }
2525    /// Set `t[k] = v` with `__newindex` metamethod awareness.
2526    ///
2527    /// Fast path: when the table has no metatable, `__newindex` can never
2528    /// fire, so the existence check via `fast_get` is pure waste —
2529    /// `try_raw_set` handles both "key exists" and "key absent" cases via
2530    /// a single lookup internally. Removing the `fast_get` halves the
2531    /// lookups per set on the metamethod-free path (table.remove/insert
2532    /// hot loops, most user code).
2533    ///
2534    /// The GC backward barrier is invoked before the store (with `&v`)
2535    /// instead of after; the barrier only inspects the value's color, not
2536    /// its location, so the order is semantically equivalent to upstream
2537    /// C-Lua and lets us move `v` straight into `table_raw_set` without
2538    /// the extra `v.clone()` that the post-store ordering forced.
2539    #[inline]
2540    pub fn table_set_with_tm(&mut self, t: &LuaValue, k: LuaValue, v: LuaValue) -> Result<(), LuaError> {
2541        if let LuaValue::Table(tbl) = t {
2542            if tbl.metatable().is_none() {
2543                self.gc_barrier_back(t, &v);
2544                return self.table_raw_set(t, k, v);
2545            }
2546        }
2547        if self.fast_get(t, &k)?.is_some() {
2548            self.gc_barrier_back(t, &v);
2549            return self.table_raw_set(t, k, v);
2550        }
2551        crate::vm::finish_set(self, t.clone(), k, v, true, None, None)
2552    }
2553    #[inline]
2554    pub fn table_raw_set(&mut self, t: &LuaValue, k: LuaValue, v: LuaValue) -> Result<(), LuaError> {
2555        let LuaValue::Table(tbl) = t else {
2556            return Err(LuaError::type_error(t, "index"));
2557        };
2558        let tbl = tbl.clone();
2559        tbl.raw_set(self, k, v)
2560    }
2561    #[inline]
2562    pub fn table_array_set(&mut self, t: &LuaValue, idx: usize, v: LuaValue) -> Result<(), LuaError> {
2563        let LuaValue::Table(tbl) = t else {
2564            return Err(LuaError::type_error(t, "index"));
2565        };
2566        let tbl = tbl.clone();
2567        tbl.raw_set_int(self, idx as i64 + 1, v)
2568    }
2569    pub fn table_ensure_array(&mut self, t: &LuaValue, n: usize) -> Result<(), LuaError> {
2570        let LuaValue::Table(tbl) = t else {
2571            return Err(LuaError::type_error(t, "index"));
2572        };
2573        if n > tbl.array_len() {
2574            tbl.resize(self, n, 0)?;
2575        }
2576        Ok(())
2577    }
2578    pub fn table_length(&mut self, t: &LuaValue) -> Result<i64, LuaError> {
2579        let LuaValue::Table(tbl) = t else {
2580            return Err(LuaError::type_error(t, "get length of"));
2581        };
2582        Ok(tbl.getn() as i64)
2583    }
2584    pub fn table_metatable(&mut self, v: &LuaValue) -> Option<GcRef<LuaTable>> {
2585        match v {
2586            LuaValue::Table(t) => t.metatable(),
2587            LuaValue::UserData(u) => u.metatable(),
2588            other => {
2589                let idx = other.base_type() as usize;
2590                self.global().mt[idx].clone()
2591            }
2592        }
2593    }
2594    pub fn table_resize(&mut self, t: &GcRef<LuaTable>, na: usize, nh: usize) -> Result<(), LuaError> {
2595        t.resize(self, na, nh)
2596    }
2597    pub fn table_getn(&self, t: &GcRef<LuaTable>) -> i64 {
2598        // PORT NOTE: C's `luaH_getn` returns a boundary i such that t[i] is
2599        // present and t[i+1] is absent (or 0 if t[1] is absent), exploiting the
2600        // hybrid array+hash layout. Phase B's LuaTable (lua-types/src/value.rs)
2601        // is a flat Vec<(K,V)> with no array part, so we linearly probe integer
2602        // keys starting at 1. The rich array+hash impl in
2603        // crates/lua-vm/src/table.rs lights up in Phase D.
2604        // PERF(port): O(n) linear scan with O(n) lookups → O(n²); Phase D fixes.
2605        let mut i: i64 = 1;
2606        loop {
2607            let v = t.get_int(i);
2608            if matches!(v, LuaValue::Nil) {
2609                return i - 1;
2610            }
2611            i += 1;
2612        }
2613    }
2614
2615    pub fn try_bin_tm(&mut self, p1: &LuaValue, p1_idx: Option<StackIdx>, p2: &LuaValue, p2_idx: Option<StackIdx>, res: StackIdx, tm: lua_types::tagmethod::TagMethod) -> Result<(), LuaError> {
2616        let event = crate::tagmethods::TagMethod::from_u8(tm as u8);
2617        crate::tagmethods::try_bin_tm(self, p1, p1_idx, p2, p2_idx, res, event)
2618    }
2619    pub fn try_bin_i_tm(&mut self, p1: &LuaValue, p1_idx: Option<StackIdx>, imm: i64, flip: bool, res: StackIdx, tm: lua_types::tagmethod::TagMethod) -> Result<(), LuaError> {
2620        let event = crate::tagmethods::TagMethod::from_u8(tm as u8);
2621        crate::tagmethods::try_bini_tm(self, p1, p1_idx, imm, flip, res, event)
2622    }
2623    pub fn try_bin_assoc_tm(&mut self, p1: &LuaValue, p1_idx: Option<StackIdx>, p2: &LuaValue, p2_idx: Option<StackIdx>, flip: bool, res: StackIdx, tm: lua_types::tagmethod::TagMethod) -> Result<(), LuaError> {
2624        let event = crate::tagmethods::TagMethod::from_u8(tm as u8);
2625        crate::tagmethods::try_bin_assoc_tm(self, p1, p1_idx, p2, p2_idx, flip, res, event)
2626    }
2627    pub fn try_concat_tm(&mut self, _p1: &LuaValue, _p2: &LuaValue) -> Result<(), LuaError> {
2628        crate::tagmethods::try_concat_tm(self)
2629    }
2630    pub fn call_tm(&mut self, f: LuaValue, p1: &LuaValue, p2: &LuaValue, p3: &LuaValue) -> Result<(), LuaError> {
2631        crate::tagmethods::call_tm(self, f, p1.clone(), p2.clone(), p3.clone())
2632    }
2633    pub fn call_tm_res(&mut self, f: LuaValue, p1: &LuaValue, p2: &LuaValue, res: StackIdx) -> Result<(), LuaError> {
2634        crate::tagmethods::call_tm_res(self, f, p1.clone(), p2.clone(), res)
2635    }
2636    pub fn call_tm_res_bool(&mut self, f: LuaValue, p1: &LuaValue, p2: &LuaValue) -> Result<bool, LuaError> {
2637        let res = self.top_idx();
2638        self.push(LuaValue::Nil);
2639        crate::tagmethods::call_tm_res(self, f, p1.clone(), p2.clone(), res)?;
2640        let result = self.get_at(res).clone();
2641        self.pop();
2642        Ok(!matches!(result, LuaValue::Nil | LuaValue::Bool(false)))
2643    }
2644    pub fn call_order_tm(&mut self, p1: &LuaValue, p2: &LuaValue, tm: lua_types::tagmethod::TagMethod) -> Result<bool, LuaError> {
2645        let event = crate::tagmethods::TagMethod::from_u8(tm as u8);
2646        crate::tagmethods::call_order_tm(self, p1, p2, event)
2647    }
2648    pub fn call_order_i_tm(&mut self, p1: &LuaValue, v2: i64, flip: bool, isfloat: bool, tm: lua_types::tagmethod::TagMethod) -> Result<bool, LuaError> {
2649        let event = crate::tagmethods::TagMethod::from_u8(tm as u8);
2650        crate::tagmethods::call_orderi_tm(self, p1, v2 as i32, flip, isfloat, event)
2651    }
2652
2653    #[inline(always)]
2654    pub fn proto_code(&self, cl: &GcRef<lua_types::closure::LuaLClosure>, pc: u32) -> lua_types::opcode::Instruction {
2655        cl.proto.code[pc as usize]
2656    }
2657    #[inline(always)]
2658    pub fn proto_const(&self, cl: &GcRef<lua_types::closure::LuaLClosure>, idx: usize) -> LuaValue {
2659        cl.proto.k[idx].clone()
2660    }
2661    /// Hot-path accessor: returns `Some(i)` only when the constant pool entry
2662    /// at `idx` is an `Int`. Avoids the full `LuaValue` clone that
2663    /// `proto_const` performs.
2664    ///
2665    /// C: `ttisinteger(&k[idx]) ? ivalue(&k[idx]) : 0` inside the K-form
2666    /// arithmetic opcode macros (`op_arithK`).
2667    #[inline(always)]
2668    pub fn proto_const_int(&self, cl: &GcRef<lua_types::closure::LuaLClosure>, idx: usize) -> Option<i64> {
2669        match &cl.proto.k[idx] {
2670            LuaValue::Int(v) => Some(*v),
2671            _ => None,
2672        }
2673    }
2674    /// Hot-path accessor: returns `Some(f)` for `Float(f)` or `Int(i)` (coerced)
2675    /// constants. Avoids the full `LuaValue` clone. Used by the float fast
2676    /// path of `OP_ADDK`/`OP_SUBK`/`OP_MULK`/`OP_DIVK`/`OP_POWK`.
2677    #[inline(always)]
2678    pub fn proto_const_num(&self, cl: &GcRef<lua_types::closure::LuaLClosure>, idx: usize) -> Option<f64> {
2679        match &cl.proto.k[idx] {
2680            LuaValue::Float(f) => Some(*f),
2681            LuaValue::Int(v) => Some(*v as f64),
2682            _ => None,
2683        }
2684    }
2685    pub fn get_proto_instr(&self, ci: CallInfoIdx, pc: u32) -> lua_types::opcode::Instruction {
2686        let cl = self.ci_lua_closure(ci)
2687            .expect("get_proto_instr: CallInfo does not hold a Lua closure");
2688        cl.proto.code[pc as usize]
2689    }
2690    /// C: `int luaG_tracecall(lua_State *L)` — wrapper that returns the trap
2691    /// flag as `bool` (C returns `int` 0/1).
2692    ///
2693    /// The C function reads `L->ci` directly, so the `_idx` argument is unused;
2694    /// the VM passes its locally tracked `ci` for symmetry with `trace_exec`.
2695    pub fn trace_call(&mut self, _idx: CallInfoIdx) -> Result<bool, LuaError> {
2696        Ok(crate::debug::trace_call(self)? != 0)
2697    }
2698    /// C: `int luaG_traceexec(lua_State *L, const Instruction *pc)` — wrapper
2699    /// returning `bool` for the trap flag. `_idx` is unused for the same reason
2700    /// as `trace_call`; `pc` is the 0-based index of the next instruction.
2701    pub fn trace_exec(&mut self, _idx: CallInfoIdx, pc: u32) -> Result<bool, LuaError> {
2702        Ok(crate::debug::trace_exec(self, pc)? != 0)
2703    }
2704    pub fn hook_call(&mut self, idx: CallInfoIdx) -> Result<(), LuaError> {
2705        crate::do_::hookcall(self, idx)
2706    }
2707    #[inline(always)]
2708    fn gc_step_flags(&self) -> Option<(bool, bool)> {
2709        let g = self.global();
2710        if !g.is_gc_running() {
2711            return None;
2712        }
2713        let should_collect = g.heap.would_collect();
2714        let has_finalizers = !g.to_be_finalized.is_empty();
2715        if should_collect || has_finalizers {
2716            Some((should_collect, has_finalizers))
2717        } else {
2718            None
2719        }
2720    }
2721
2722    #[inline(always)]
2723    pub fn gc_check_step(&mut self) {
2724        if !self.allowhook {
2725            return;
2726        }
2727        let Some((should_collect, has_finalizers)) = self.gc_step_flags() else {
2728            return;
2729        };
2730        if should_collect {
2731            self.gc().check_step();
2732        }
2733        if has_finalizers || !self.global().to_be_finalized.is_empty() {
2734            crate::api::run_pending_finalizers(self);
2735        }
2736    }
2737    #[inline(always)]
2738    pub fn gc_cond_step(&mut self) {
2739        if !self.allowhook {
2740            return;
2741        }
2742        let Some((should_collect, has_finalizers)) = self.gc_step_flags() else {
2743            return;
2744        };
2745        if should_collect {
2746            self.gc().check_step();
2747        }
2748        if has_finalizers || !self.global().to_be_finalized.is_empty() {
2749            crate::api::run_pending_finalizers(self);
2750        }
2751    }
2752    pub fn gc_barrier_back<T, U>(&mut self, _t: T, _v: U) { /* phase-b no-op */ }
2753    pub fn gc_barrier_upval<T, U, V>(&mut self, _cl: T, _uv: U, _v: V) { /* phase-b no-op */ }
2754    /// C: `(G(L)->mainthread == L)` — true if `self` is the main thread.
2755    ///
2756    /// Phase E-1: compares `GlobalState::current_thread_id` against
2757    /// `main_thread_id`. Coroutine resume (slice 02b) is what will swap
2758    /// `current_thread_id` in and out; until then the running thread is
2759    /// always the main thread and this returns `true`.
2760    pub fn is_main_thread(&mut self) -> bool {
2761        let g = self.global();
2762        g.current_thread_id == g.main_thread_id
2763    }
2764    pub fn obj_type_name<'v>(&self, v: &'v LuaValue) -> std::borrow::Cow<'static, [u8]> {
2765        match v {
2766            LuaValue::LightUserData(_) => std::borrow::Cow::Borrowed(b"light userdata"),
2767            LuaValue::Table(t) => {
2768                if let Some(mt) = t.metatable() {
2769                    if let LuaValue::Str(s) = mt.get_str_bytes(b"__name") {
2770                        return std::borrow::Cow::Owned(s.as_bytes().to_vec());
2771                    }
2772                }
2773                std::borrow::Cow::Borrowed(crate::tagmethods::type_name(v.base_type()))
2774            }
2775            LuaValue::UserData(u) => {
2776                if let Some(mt) = u.metatable() {
2777                    if let LuaValue::Str(s) = mt.get_str_bytes(b"__name") {
2778                        return std::borrow::Cow::Owned(s.as_bytes().to_vec());
2779                    }
2780                }
2781                std::borrow::Cow::Borrowed(crate::tagmethods::type_name(v.base_type()))
2782            }
2783            _ => std::borrow::Cow::Borrowed(crate::tagmethods::type_name(v.base_type())),
2784        }
2785    }
2786
2787    pub fn full_type_name(&mut self, v: &LuaValue) -> Result<Vec<u8>, LuaError> {
2788        crate::tagmethods::obj_type_name(self, v)
2789    }
2790    pub fn emit_warning(&mut self, _msg: &[u8], _to_cont: bool) { warning(self, _msg, _to_cont) }
2791}
2792
2793// ─── GcHandle — no-op GC facade ───────────────────────────────────────────────
2794
2795/// A short-lived handle returned by `state.gc()` for GC operations.
2796///
2797/// In Phases A–C all methods are no-ops. Phase D replaces with real GC.
2798pub struct GcHandle<'a> {
2799    _state: &'a mut LuaState,
2800}
2801
2802/// Composite root passed to `Heap::full_collect`. The Phase-A workaround in
2803/// `new_state` leaves `GlobalState.mainthread = None` (to break the
2804/// self-referential Rc cycle pre-D), so the running thread's stack and
2805/// openupval list are not reachable from `GlobalState::trace`. Wrapping both
2806/// references in a single `Trace`-implementing root injects the active
2807/// thread as a second mark source for the duration of the collection.
2808struct CollectRoots<'a> {
2809    global: &'a GlobalState,
2810    thread: &'a LuaState,
2811}
2812
2813impl<'a> lua_gc::Trace for CollectRoots<'a> {
2814    fn trace(&self, m: &mut lua_gc::Marker) {
2815        self.global.trace(m);
2816        self.thread.trace(m);
2817    }
2818}
2819
2820fn trace_reachable_threads(
2821    global: &GlobalState,
2822    _current_thread_id: u64,
2823    marker: &mut lua_gc::Marker,
2824) {
2825    use lua_gc::Trace;
2826
2827    loop {
2828        let visited_before = marker.visited_count();
2829        for (id, entry) in global.threads.iter() {
2830            if thread_entry_marked_alive(marker, *id, entry) {
2831                if let Ok(thread) = entry.state.try_borrow() {
2832                    thread.trace(marker);
2833                }
2834            }
2835        }
2836        marker.drain_gray_queue();
2837        if marker.visited_count() == visited_before {
2838            break;
2839        }
2840    }
2841}
2842
2843fn thread_entry_marked_alive(
2844    marker: &lua_gc::Marker,
2845    id: u64,
2846    entry: &ThreadRegistryEntry,
2847) -> bool {
2848    marker.is_visited(entry.value.identity()) && entry.value.id == id
2849}
2850
2851fn close_open_upvalues_for_unreachable_threads(
2852    global: &GlobalState,
2853    marker: &mut lua_gc::Marker,
2854) {
2855    use lua_gc::Trace;
2856
2857    let mut closed_values = Vec::<LuaValue>::new();
2858    for (id, entry) in global.threads.iter() {
2859        if entry.value.id != *id {
2860            continue;
2861        }
2862        if thread_entry_marked_alive(marker, *id, entry) {
2863            continue;
2864        }
2865        let Ok(thread) = entry.state.try_borrow() else {
2866            continue;
2867        };
2868        for uv in thread.openupval.iter() {
2869            if !marker.is_visited(uv.identity()) {
2870                continue;
2871            }
2872            let Some((thread_id, idx)) = uv.try_open_payload() else {
2873                continue;
2874            };
2875            if thread_id as u64 != *id {
2876                continue;
2877            }
2878            let value = thread.get_at(idx);
2879            uv.close_with(value.clone());
2880            closed_values.push(value);
2881        }
2882    }
2883    for value in closed_values {
2884        value.trace(marker);
2885    }
2886    marker.drain_gray_queue();
2887}
2888
2889impl<'a> GcHandle<'a> {
2890    /// C: `luaC_checkGC(L)` — conditional GC step.
2891    /// macros.tsv: `luaC_checkGC → state.gc().check_step()`
2892    ///
2893    /// Phase D-2: drives implicit collection when the heap's byte threshold
2894    /// is exceeded. Without this hook, loops that allocate without an
2895    /// explicit `collectgarbage()` call (e.g. `closure.lua`'s
2896    /// `while x[1] do local a = A..A end` GC-driven loop) never settle.
2897    pub fn check_step(&self) {
2898        if !self._state.global().is_gc_running() {
2899            return;
2900        }
2901        self.collect_via_heap(/* force = */ false);
2902    }
2903
2904    /// C: `luaC_fullgc(L, isemergency)` — full collection.
2905    /// macros.tsv: `luaC_fullgc → state.gc().full_collect()`
2906    pub fn full_collect(&self) {
2907        self.collect_via_heap(/* force = */ true);
2908    }
2909
2910    /// Shared driver behind both `full_collect` (force-collect) and
2911    /// `check_step` (collect only if heap byte threshold exceeded).
2912    ///
2913    /// Snapshots the weak-tables registry, invokes the heap's collect path
2914    /// with a post-mark weak-prune hook, and rebuilds the registry by
2915    /// retaining only entries whose target was reachable. The same hook
2916    /// works for both modes — the heap short-circuits when force=false and
2917    /// the threshold isn't met.
2918    fn collect_via_heap(&self, force: bool) {
2919        use lua_gc::Trace;
2920        let state_ref: &LuaState = &*self._state;
2921
2922        // Fast path: when the caller did not force a collection, skip all
2923        // the snapshot work (3 Vec allocations + 3 HashSet allocations) if
2924        // the heap is paused or under threshold — a `step()` in that state
2925        // is a no-op, so the snapshot would be pure waste. Called millions
2926        // of times per recursive workload via `gc_check_step` in `precall`.
2927        if !force {
2928            let g = state_ref.global.borrow();
2929            if !g.heap.would_collect() {
2930                return;
2931            }
2932        }
2933
2934        // Snapshot weak tables BEFORE the collect. `identity()` reads only
2935        // the pointer address — safe even on still-dangling weak handles —
2936        // and dedup by identity keeps the iteration linear.
2937        let weak_tables_snapshot: Vec<lua_types::gc::GcRef<lua_types::value::LuaTable>> = {
2938            let g = state_ref.global.borrow();
2939            let mut seen = std::collections::HashSet::<usize>::new();
2940            g.weak_tables_registry
2941                .iter()
2942                .filter_map(|w| w.upgrade())
2943                .filter(|t| seen.insert(t.identity()))
2944                .collect()
2945        };
2946
2947        // Snapshot pending finalizers. `GlobalState::trace` deliberately
2948        // does NOT root these — that's how the post-mark hook below can
2949        // distinguish "still reachable from program state" from "only kept
2950        // alive by the finalizer registry."
2951        let pending_snapshot: Vec<lua_types::gc::GcRef<lua_types::value::LuaTable>> = {
2952            let g = state_ref.global.borrow();
2953            g.pending_finalizers.clone()
2954        };
2955
2956        // Snapshot tracked long-string identities + byte sizes BEFORE the
2957        // collect. The post-mark hook compares each identity against the
2958        // marker's visited set; anything not visited is unreachable and
2959        // its bytes get reclaimed from `gc_debt` after the heap collect
2960        // returns. Bare `usize` is safe to carry across the hook — long
2961        // strings use `new_uncollected` so the pointer never dangles.
2962        let long_string_snapshot: Vec<(usize, usize)> = {
2963            let g = state_ref.global.borrow();
2964            g.gc_tracked_long_strings
2965                .iter()
2966                .map(|(w, sz)| (w.0.identity(), *sz))
2967                .collect()
2968        };
2969
2970        let alive_ids: std::cell::RefCell<std::collections::HashSet<usize>> =
2971            std::cell::RefCell::new(std::collections::HashSet::new());
2972        let newly_unreachable: std::cell::RefCell<Vec<lua_types::gc::GcRef<lua_types::value::LuaTable>>> =
2973            std::cell::RefCell::new(Vec::new());
2974        let dead_long_strings: std::cell::RefCell<std::collections::HashSet<usize>> =
2975            std::cell::RefCell::new(std::collections::HashSet::new());
2976        let alive_thread_ids: std::cell::RefCell<std::collections::HashSet<u64>> =
2977            std::cell::RefCell::new(std::collections::HashSet::new());
2978        let collect_ran = std::cell::Cell::new(false);
2979
2980        {
2981            let global = state_ref.global.borrow();
2982            global.heap.unpause();
2983            let roots = CollectRoots { global: &*global, thread: state_ref };
2984            let hook = |marker: &mut lua_gc::Marker| {
2985                collect_ran.set(true);
2986                trace_reachable_threads(&*global, global.current_thread_id, marker);
2987                close_open_upvalues_for_unreachable_threads(&*global, marker);
2988                loop {
2989                    let visited_before = marker.visited_count();
2990                    for t in &weak_tables_snapshot {
2991                        let t_id = t.identity();
2992                        if !marker.is_visited(t_id) {
2993                            continue;
2994                        }
2995                        let to_mark = t.ephemeron_values_to_mark(
2996                            &|id| marker.is_visited(id),
2997                        );
2998                        for v in &to_mark {
2999                            v.trace(marker);
3000                        }
3001                    }
3002                    marker.drain_gray_queue();
3003                    if marker.visited_count() == visited_before {
3004                        break;
3005                    }
3006                }
3007                for pf in &pending_snapshot {
3008                    if !marker.is_visited(pf.identity()) {
3009                        marker.mark(pf.0);
3010                        newly_unreachable.borrow_mut().push(pf.clone());
3011                    }
3012                }
3013                marker.drain_gray_queue();
3014                loop {
3015                    let visited_before = marker.visited_count();
3016                    for t in &weak_tables_snapshot {
3017                        let t_id = t.identity();
3018                        if !marker.is_visited(t_id) {
3019                            continue;
3020                        }
3021                        let to_mark = t.ephemeron_values_to_mark(
3022                            &|id| marker.is_visited(id),
3023                        );
3024                        for v in &to_mark {
3025                            v.trace(marker);
3026                        }
3027                    }
3028                    marker.drain_gray_queue();
3029                    if marker.visited_count() == visited_before {
3030                        break;
3031                    }
3032                }
3033                for t in &weak_tables_snapshot {
3034                    let id = t.identity();
3035                    if marker.is_visited(id) {
3036                        let to_mark = t.prune_weak_dead(&|id| marker.is_visited(id));
3037                        for v in &to_mark {
3038                            v.trace(marker);
3039                        }
3040                        alive_ids.borrow_mut().insert(id);
3041                    }
3042                }
3043                marker.drain_gray_queue();
3044                // Long-string Phase-B reclaim. With `new_uncollected`
3045                // allocation, long strings never enter the heap's sweep
3046                // path, so we rely on the marker's visited set: any
3047                // tracked long-string identity that wasn't reached by mark
3048                // is unreferenced and its bytes can be returned to
3049                // `gc_debt`. Done here (inside the hook) so it sees the
3050                // visited set BEFORE drop of the marker.
3051                {
3052                    let mut dead = dead_long_strings.borrow_mut();
3053                    for (id, _sz) in &long_string_snapshot {
3054                        if !marker.is_visited(*id) {
3055                            dead.insert(*id);
3056                        }
3057                    }
3058                }
3059                {
3060                    let mut alive = alive_thread_ids.borrow_mut();
3061                    for (id, entry) in global.threads.iter() {
3062                        if thread_entry_marked_alive(marker, *id, entry) {
3063                            alive.insert(*id);
3064                        }
3065                    }
3066                }
3067            };
3068            if force {
3069                global.heap.full_collect_with_post_mark(&roots, hook);
3070            } else {
3071                global.heap.step_with_post_mark(&roots, hook);
3072            }
3073        }
3074
3075        if !collect_ran.get() {
3076            return;
3077        }
3078
3079        // After collect, drop weak-table-registry entries whose target was
3080        // swept. Without this filter the registry leaks one dangling
3081        // `GcWeak<LuaTable>` per dead weak table; the next collect would
3082        // upgrade those handles (current placeholder GcWeak always returns
3083        // Some) and the prune walk would deref freed memory.
3084        let alive_set = alive_ids.into_inner();
3085        let promote: Vec<lua_types::gc::GcRef<lua_types::value::LuaTable>> =
3086            newly_unreachable.into_inner();
3087        let promote_ids: std::collections::HashSet<usize> =
3088            promote.iter().map(|t| t.identity()).collect();
3089        let dead_ls_ids = dead_long_strings.into_inner();
3090        let alive_thread_ids = alive_thread_ids.into_inner();
3091        let mut g = state_ref.global.borrow_mut();
3092        g.weak_tables_registry
3093            .retain(|w| alive_set.contains(&w.0.identity()));
3094        let main_thread_id = g.main_thread_id;
3095        g.threads.retain(|id, _| alive_thread_ids.contains(id));
3096        g.cross_thread_upvals
3097            .retain(|(id, _), _| *id == main_thread_id || alive_thread_ids.contains(id));
3098        // Move newly-unreachable finalizables from `pending_finalizers` to
3099        // `to_be_finalized`. The latter is rooted by `GlobalState::trace`,
3100        // so these tables remain alive until their `__gc` runs.
3101        g.pending_finalizers
3102            .retain(|t| !promote_ids.contains(&t.identity()));
3103        g.to_be_finalized.extend(promote);
3104        // Reclaim long-string byte accounting for entries the marker said
3105        // were unreachable. The underlying `Gc<LuaString>` was allocated
3106        // via `new_uncollected` and stays live in process memory; only
3107        // `gc_debt` is adjusted so `collectgarbage("count")` reflects the
3108        // drop in user-visible live bytes.
3109        if !dead_ls_ids.is_empty() {
3110            let mut freed: isize = 0;
3111            g.gc_tracked_long_strings.retain(|(w, sz)| {
3112                if dead_ls_ids.contains(&w.0.identity()) {
3113                    freed += *sz as isize;
3114                    false
3115                } else {
3116                    true
3117                }
3118            });
3119            g.gc_debt -= freed;
3120        }
3121    }
3122
3123    /// Phase-B stub for `luaC_step(L)`.
3124    pub fn step(&self) { /* phase-b no-op */ }
3125
3126    /// Run one budgeted incremental step of the GC.
3127    ///
3128    /// `work_units` is the number of GC work units the step is allowed to
3129    /// perform (one gray trace, one sweep visit, or one phase transition).
3130    /// Returns `true` if the step completed a cycle and the collector is
3131    /// now in the `Pause` state; `false` otherwise.
3132    ///
3133    /// Mirrors `collect_via_heap` for the post-mark weak-table /
3134    /// finalizer-promotion logic, but only the atomic-phase transition will
3135    /// invoke the snapshot-walking hook — propagate and sweep steps reuse
3136    /// the snapshot but never execute it. The snapshot is rebuilt on every
3137    /// call; the cost is `O(weak_tables_registry)` per step.
3138    pub fn incremental_step(&self, work_units: isize) -> bool {
3139        use lua_gc::{StepBudget, StepOutcome, Trace};
3140        let state_ref: &LuaState = &*self._state;
3141
3142        let weak_tables_snapshot: Vec<lua_types::gc::GcRef<lua_types::value::LuaTable>> = {
3143            let g = state_ref.global.borrow();
3144            let mut seen = std::collections::HashSet::<usize>::new();
3145            g.weak_tables_registry
3146                .iter()
3147                .filter_map(|w| w.upgrade())
3148                .filter(|t| seen.insert(t.identity()))
3149                .collect()
3150        };
3151
3152        let pending_snapshot: Vec<lua_types::gc::GcRef<lua_types::value::LuaTable>> = {
3153            let g = state_ref.global.borrow();
3154            g.pending_finalizers.clone()
3155        };
3156
3157        let long_string_snapshot: Vec<(usize, usize)> = {
3158            let g = state_ref.global.borrow();
3159            g.gc_tracked_long_strings
3160                .iter()
3161                .map(|(w, sz)| (w.0.identity(), *sz))
3162                .collect()
3163        };
3164
3165        let alive_ids: std::cell::RefCell<std::collections::HashSet<usize>> =
3166            std::cell::RefCell::new(std::collections::HashSet::new());
3167        let newly_unreachable: std::cell::RefCell<Vec<lua_types::gc::GcRef<lua_types::value::LuaTable>>> =
3168            std::cell::RefCell::new(Vec::new());
3169        let dead_long_strings: std::cell::RefCell<std::collections::HashSet<usize>> =
3170            std::cell::RefCell::new(std::collections::HashSet::new());
3171        let alive_thread_ids: std::cell::RefCell<std::collections::HashSet<u64>> =
3172            std::cell::RefCell::new(std::collections::HashSet::new());
3173        let atomic_ran = std::cell::Cell::new(false);
3174
3175        let outcome = {
3176            let global = state_ref.global.borrow();
3177            global.heap.unpause();
3178            let roots = CollectRoots { global: &*global, thread: state_ref };
3179            let hook = |marker: &mut lua_gc::Marker| {
3180                atomic_ran.set(true);
3181                trace_reachable_threads(&*global, global.current_thread_id, marker);
3182                close_open_upvalues_for_unreachable_threads(&*global, marker);
3183                loop {
3184                    let visited_before = marker.visited_count();
3185                    for t in &weak_tables_snapshot {
3186                        let t_id = t.identity();
3187                        if !marker.is_visited(t_id) {
3188                            continue;
3189                        }
3190                        let to_mark = t.ephemeron_values_to_mark(
3191                            &|id| marker.is_visited(id),
3192                        );
3193                        for v in &to_mark {
3194                            v.trace(marker);
3195                        }
3196                    }
3197                    marker.drain_gray_queue();
3198                    if marker.visited_count() == visited_before {
3199                        break;
3200                    }
3201                }
3202                for pf in &pending_snapshot {
3203                    if !marker.is_visited(pf.identity()) {
3204                        marker.mark(pf.0);
3205                        newly_unreachable.borrow_mut().push(pf.clone());
3206                    }
3207                }
3208                marker.drain_gray_queue();
3209                loop {
3210                    let visited_before = marker.visited_count();
3211                    for t in &weak_tables_snapshot {
3212                        let t_id = t.identity();
3213                        if !marker.is_visited(t_id) {
3214                            continue;
3215                        }
3216                        let to_mark = t.ephemeron_values_to_mark(
3217                            &|id| marker.is_visited(id),
3218                        );
3219                        for v in &to_mark {
3220                            v.trace(marker);
3221                        }
3222                    }
3223                    marker.drain_gray_queue();
3224                    if marker.visited_count() == visited_before {
3225                        break;
3226                    }
3227                }
3228                for t in &weak_tables_snapshot {
3229                    let id = t.identity();
3230                    if marker.is_visited(id) {
3231                        let to_mark = t.prune_weak_dead(&|id| marker.is_visited(id));
3232                        for v in &to_mark {
3233                            v.trace(marker);
3234                        }
3235                        alive_ids.borrow_mut().insert(id);
3236                    }
3237                }
3238                marker.drain_gray_queue();
3239                {
3240                    let mut dead = dead_long_strings.borrow_mut();
3241                    for (id, _sz) in &long_string_snapshot {
3242                        if !marker.is_visited(*id) {
3243                            dead.insert(*id);
3244                        }
3245                    }
3246                }
3247                {
3248                    let mut alive = alive_thread_ids.borrow_mut();
3249                    for (id, entry) in global.threads.iter() {
3250                        if thread_entry_marked_alive(marker, *id, entry) {
3251                            alive.insert(*id);
3252                        }
3253                    }
3254                }
3255            };
3256            let budget = StepBudget::from_work(work_units);
3257            global.heap.incremental_step_with_post_mark(&roots, budget, hook)
3258        };
3259
3260        if atomic_ran.get() {
3261            let alive_set = alive_ids.into_inner();
3262            let promote: Vec<lua_types::gc::GcRef<lua_types::value::LuaTable>> =
3263                newly_unreachable.into_inner();
3264            let promote_ids: std::collections::HashSet<usize> =
3265                promote.iter().map(|t| t.identity()).collect();
3266            let dead_ls_ids = dead_long_strings.into_inner();
3267            let alive_thread_ids = alive_thread_ids.into_inner();
3268            let mut g = state_ref.global.borrow_mut();
3269            g.weak_tables_registry
3270                .retain(|w| alive_set.contains(&w.0.identity()));
3271            let main_thread_id = g.main_thread_id;
3272            g.threads.retain(|id, _| alive_thread_ids.contains(id));
3273            g.cross_thread_upvals
3274                .retain(|(id, _), _| *id == main_thread_id || alive_thread_ids.contains(id));
3275            g.pending_finalizers
3276                .retain(|t| !promote_ids.contains(&t.identity()));
3277            g.to_be_finalized.extend(promote);
3278            if !dead_ls_ids.is_empty() {
3279                let mut freed: isize = 0;
3280                g.gc_tracked_long_strings.retain(|(w, sz)| {
3281                    if dead_ls_ids.contains(&w.0.identity()) {
3282                        freed += *sz as isize;
3283                        false
3284                    } else {
3285                        true
3286                    }
3287                });
3288                g.gc_debt -= freed;
3289            }
3290        }
3291
3292        matches!(outcome, StepOutcome::Paused)
3293    }
3294
3295    /// Run only the weak-table atomic cleanup used by a generational step.
3296    ///
3297    /// C-Lua's `genstep` performs young/full generational work and includes
3298    /// weak-table clearing at the atomic boundary. This heap does not model
3299    /// ages yet; this mark-only pass gives explicit generational steps the
3300    /// weak cleanup they need without sweeping objects from suspended threads.
3301    pub fn prune_weak_tables_mark_only(&self) {
3302        use lua_gc::Trace;
3303        let state_ref: &LuaState = &*self._state;
3304
3305        let weak_tables_snapshot: Vec<lua_types::gc::GcRef<lua_types::value::LuaTable>> = {
3306            let g = state_ref.global.borrow();
3307            let mut seen = std::collections::HashSet::<usize>::new();
3308            g.weak_tables_registry
3309                .iter()
3310                .filter_map(|w| w.upgrade())
3311                .filter(|t| seen.insert(t.identity()))
3312                .collect()
3313        };
3314
3315        let global = state_ref.global.borrow();
3316        global.heap.unpause();
3317        let roots = CollectRoots { global: &*global, thread: state_ref };
3318        let hook = |marker: &mut lua_gc::Marker| {
3319            trace_reachable_threads(&*global, global.current_thread_id, marker);
3320            loop {
3321                let visited_before = marker.visited_count();
3322                for t in &weak_tables_snapshot {
3323                    let t_id = t.identity();
3324                    if !marker.is_visited(t_id) {
3325                        continue;
3326                    }
3327                    let to_mark = t.ephemeron_values_to_mark(
3328                        &|id| marker.is_visited(id),
3329                    );
3330                    for v in &to_mark {
3331                        v.trace(marker);
3332                    }
3333                }
3334                marker.drain_gray_queue();
3335                if marker.visited_count() == visited_before {
3336                    break;
3337                }
3338            }
3339            for t in &weak_tables_snapshot {
3340                if marker.is_visited(t.identity()) {
3341                    let to_mark = t.prune_weak_dead(&|id| marker.is_visited(id));
3342                    for v in &to_mark {
3343                        v.trace(marker);
3344                    }
3345                }
3346            }
3347        };
3348        global.heap.mark_only_with_post_mark(&roots, hook);
3349    }
3350
3351    /// Set the GC kind (incremental/generational).
3352    ///
3353    /// C: `luaC_changemode(L, newmode)` in `lgc.c` — in Phases A–C the heap
3354    /// itself is `Rc`-based, so the only observable effect is the mode flag
3355    /// returned by `lua_gc(LUA_GCGEN)` / `lua_gc(LUA_GCINC)` on the next call.
3356    pub fn change_mode(&self, mode: GcKind) {
3357        self._state.global_mut().gckind = mode as u8;
3358    }
3359
3360    /// Phase-B stub for `luaC_fix(L, o)` — pin an object so GC won't collect it.
3361    pub fn fix_object<T: lua_gc::Trace + 'static>(&self, _o: &GcRef<T>) { /* phase-b no-op */ }
3362
3363    /// Free all collectable objects (called during state teardown).
3364    ///
3365    /// C: `luaC_freeallobjects(L)` in `lgc.c`.
3366    /// PORT NOTE: In Phases A–C, Rc drop chains handle deallocation automatically.
3367    pub fn free_all_objects(&self) {
3368        // PORT NOTE: Phase A–C no-op; Rc::drop handles deallocation
3369    }
3370
3371    /// GC write barrier for a TValue.
3372    ///
3373    /// C: `luaC_barrier(L, p, v)`.
3374    /// macros.tsv: `luaC_barrier → state.gc().barrier(p, v)` — no-op in Phases A–C
3375    pub fn barrier(&self, _p: &dyn std::any::Any, _v: &LuaValue) {}
3376
3377    /// Backward write barrier.
3378    ///
3379    /// C: `luaC_barrierback(L, p, v)`.
3380    /// macros.tsv: `luaC_barrierback → state.gc().barrier_back(p, v)` — no-op
3381    pub fn barrier_back(&self, _p: &dyn std::any::Any, _v: &LuaValue) {}
3382
3383    /// Object write barrier.
3384    ///
3385    /// C: `luaC_objbarrier(L, o, v)`.
3386    /// macros.tsv: `luaC_objbarrier → state.gc().obj_barrier(p, o)` — no-op
3387    pub fn obj_barrier(&self, _p: &dyn std::any::Any, _o: &dyn std::any::Any) {}
3388
3389    /// Backward object write barrier.
3390    ///
3391    /// C: `luaC_objbarrierback(L, p, o)` — no-op in Phases A–C
3392    pub fn obj_barrier_back(&self, _p: &dyn std::any::Any, _o: &dyn std::any::Any) {}
3393}
3394
3395// ─── Functions from lstate.c ──────────────────────────────────────────────────
3396
3397// C: static unsigned int luai_makeseed(lua_State *L)
3398//
3399// PORT NOTE: `luai_makeseed` in C mixed ASLR entropy (pointer addresses of a
3400// heap var, stack var, and code symbol) with the current time via `luaS_hash`.
3401// In Rust, raw pointer addresses require `unsafe` which is forbidden outside
3402// lua-gc/lua-coro.  Phase A uses time-only entropy.  The hash is computed via
3403// `crate::string::hash_bytes` to match the Lua FNV-style algorithm.
3404fn make_seed() -> u32 {
3405    // C: unsigned int h = cast_uint(time(NULL));
3406    use std::time::{SystemTime, UNIX_EPOCH};
3407    let t = SystemTime::now()
3408        .duration_since(UNIX_EPOCH)
3409        .map(|d| d.as_secs() as u32)
3410        .unwrap_or(0);
3411
3412    // TODO(port): mix in ASLR entropy (pointer to heap / stack / code).
3413    // Requires a short `unsafe` block to cast references to usize.
3414    // The entropy improvement is important for hash DoS resistance (CVE-class).
3415    // Phase B should add this via a platform-specific helper in lua-gc or via
3416    // the `getrandom` crate if it is added as a dependency.
3417
3418    // C: return luaS_hash(buff, p, h)
3419    // For Phase A, just hash the time bytes against itself.
3420    crate::string::hash_bytes(&t.to_le_bytes(), t)
3421}
3422
3423/// Adjust `GCdebt` to `debt` while preserving the `totalbytes + GCdebt` invariant.
3424///
3425/// C: `void luaE_setdebt(global_State *g, l_mem debt)` — LUAI_FUNC (pub(crate))
3426///
3427/// ```c
3428/// // C: void luaE_setdebt(global_State *g, l_mem debt) {
3429/// //   l_mem tb = gettotalbytes(g);
3430/// //   lua_assert(tb > 0);
3431/// //   if (debt < tb - MAX_LMEM)
3432/// //     debt = tb - MAX_LMEM;
3433/// //   g->totalbytes = tb - debt;
3434/// //   g->GCdebt = debt;
3435/// // }
3436/// ```
3437pub(crate) fn set_debt(g: &mut GlobalState, mut debt: isize) {
3438    // C: l_mem tb = gettotalbytes(g);
3439    let tb = g.total_bytes() as isize;
3440    // C: lua_assert(tb > 0);
3441    debug_assert!(tb > 0);
3442    // C: if (debt < tb - MAX_LMEM) debt = tb - MAX_LMEM;
3443    // macros.tsv: MAX_LMEM → isize::MAX
3444    if debt < tb.saturating_sub(isize::MAX) {
3445        debt = tb - isize::MAX;
3446    }
3447    // C: g->totalbytes = tb - debt;
3448    g.totalbytes = tb - debt;
3449    // C: g->GCdebt = debt;
3450    g.gc_debt = debt;
3451}
3452
3453/// Sweep the Phase-B long-string tracker and decrement `gc_debt` by the
3454/// recorded byte count of any entry whose underlying `Rc` has been dropped.
3455///
3456/// PORT NOTE: Phase D will replace this with the real allocator's per-object
3457/// accounting through `luaM_realloc`. For now, long-string creation pushes a
3458/// `(Weak, size)` pair onto `gc_tracked_long_strings`, and this helper
3459/// reclaims the bytes lazily — at every `collectgarbage("count")` query and
3460/// at the end of `collectgarbage("collect")` — so the Lua-visible memory
3461/// total reflects live string bytes rather than peak allocation.
3462pub(crate) fn reclaim_dead_long_strings(g: &mut GlobalState) {
3463    let mut freed: isize = 0;
3464    g.gc_tracked_long_strings.retain(|(w, sz)| {
3465        if w.strong_count() == 0 {
3466            freed += *sz as isize;
3467            false
3468        } else {
3469            true
3470        }
3471    });
3472    g.gc_debt -= freed;
3473}
3474
3475/// Deprecated no-op that returns `LUAI_MAXCCALLS`.
3476///
3477/// C: `LUA_API int lua_setcstacklimit(lua_State *L, unsigned int limit)` (pub)
3478///
3479/// ```c
3480/// // C: LUA_API int lua_setcstacklimit(lua_State *L, unsigned int limit) {
3481/// //   UNUSED(L); UNUSED(limit);
3482/// //   return LUAI_MAXCCALLS;  /* warning?? */
3483/// // }
3484/// ```
3485pub fn set_c_stack_limit(_state: &mut LuaState, _limit: u32) -> i32 {
3486    // C: UNUSED(L); UNUSED(limit);
3487    let _ = (_state, _limit);
3488    // C: return LUAI_MAXCCALLS;
3489    LUAI_MAXCCALLS as i32
3490}
3491
3492/// Allocate a fresh `CallInfo` beyond the current frame and return its index.
3493///
3494/// C: `CallInfo *luaE_extendCI(lua_State *L)` — LUAI_FUNC (pub(crate))
3495///
3496/// ```c
3497/// // C: CallInfo *luaE_extendCI(lua_State *L) {
3498/// //   CallInfo *ci;
3499/// //   lua_assert(L->ci->next == NULL);
3500/// //   ci = luaM_new(L, CallInfo);
3501/// //   L->ci->next = ci;
3502/// //   ci->previous = L->ci;
3503/// //   ci->next = NULL;
3504/// //   ci->u.l.trap = 0;
3505/// //   L->nci++;
3506/// //   return ci;
3507/// // }
3508/// ```
3509pub(crate) fn extend_ci(state: &mut LuaState) -> CallInfoIdx {
3510    // C: lua_assert(L->ci->next == NULL);
3511    debug_assert!(
3512        state.call_info[state.ci.0 as usize].next.is_none(),
3513        "extend_ci: current ci already has a cached next frame"
3514    );
3515
3516    let current_idx = state.ci;
3517    // C: ci = luaM_new(L, CallInfo);
3518    // macros.tsv: luaM_new → Box::new(T::default()) — here we push onto the Vec
3519    let new_idx = CallInfoIdx(state.call_info.len() as u32);
3520
3521    state.call_info.push(CallInfo {
3522        // C: ci->previous = L->ci;
3523        previous: Some(current_idx),
3524        // C: ci->next = NULL;
3525        next: None,
3526        // C: ci->u.l.trap = 0;
3527        u: CallInfoFrame::lua_default(),
3528        ..CallInfo::default()
3529    });
3530
3531    // C: L->ci->next = ci;
3532    state.call_info[current_idx.0 as usize].next = Some(new_idx);
3533
3534    // C: L->nci++;
3535    state.nci += 1;
3536
3537    new_idx
3538}
3539
3540/// Free all cached (unused) `CallInfo` frames beyond the current frame.
3541///
3542/// C: `static void freeCI(lua_State *L)` (private)
3543///
3544/// ```c
3545/// // C: static void freeCI(lua_State *L) {
3546/// //   CallInfo *ci = L->ci;
3547/// //   CallInfo *next = ci->next;
3548/// //   ci->next = NULL;
3549/// //   while ((ci = next) != NULL) {
3550/// //     next = ci->next;
3551/// //     luaM_free(L, ci);
3552/// //     L->nci--;
3553/// //   }
3554/// // }
3555/// ```
3556///
3557/// PORT NOTE: In C, each `CallInfo` is an independent heap allocation freed by
3558/// `luaM_free`.  In Rust, all `CallInfo` entries live in `state.call_info: Vec<CallInfo>`.
3559/// We walk the link chain to count removals (updating `nci`), then truncate the Vec.
3560/// This is safe as long as all free entries have indices greater than `state.ci`.
3561fn free_ci(state: &mut LuaState) {
3562    let ci_idx = state.ci.0 as usize;
3563
3564    // C: CallInfo *next = ci->next; ci->next = NULL;
3565    let mut next_opt = state.call_info[ci_idx].next.take();
3566
3567    // C: while ((ci = next) != NULL) { next = ci->next; luaM_free(L, ci); L->nci--; }
3568    while let Some(idx) = next_opt {
3569        next_opt = state.call_info[idx.0 as usize].next;
3570        // C: L->nci--;
3571        state.nci = state.nci.saturating_sub(1);
3572    }
3573
3574    // Truncate: drop all entries beyond the current ci.
3575    // TODO(port): verify invariant that all cached frames have contiguous indices > state.ci
3576    state.call_info.truncate(ci_idx + 1);
3577}
3578
3579/// Free approximately half of the cached `CallInfo` frames beyond the current frame.
3580///
3581/// C: `void luaE_shrinkCI(lua_State *L)` — LUAI_FUNC (pub(crate))
3582///
3583/// ```c
3584/// // C: void luaE_shrinkCI(lua_State *L) {
3585/// //   CallInfo *ci = L->ci->next;
3586/// //   CallInfo *next;
3587/// //   if (ci == NULL) return;
3588/// //   while ((next = ci->next) != NULL) {
3589/// //     CallInfo *next2 = next->next;
3590/// //     ci->next = next2;
3591/// //     L->nci--;
3592/// //     luaM_free(L, next);
3593/// //     if (next2 == NULL) break;
3594/// //     else { next2->previous = ci; ci = next2; }
3595/// //   }
3596/// // }
3597/// ```
3598///
3599/// PORT NOTE: The C code removes every other node from the free-list chain by
3600/// pointer manipulation.  In Rust, removing elements from the middle of a `Vec`
3601/// shifts subsequent elements and invalidates `CallInfoIdx` values that point
3602/// past the removal site.  For Phase A, we approximate by halving the free count
3603/// via truncation.  TODO(port): Phase B should implement a proper free-list
3604/// pool (e.g., a slab) that allows O(1) element removal without index
3605/// invalidation.
3606pub(crate) fn shrink_ci(state: &mut LuaState) {
3607    let ci_idx = state.ci.0 as usize;
3608
3609    // C: CallInfo *ci = L->ci->next;
3610    // C: if (ci == NULL) return;
3611    if state.call_info[ci_idx].next.is_none() {
3612        return;
3613    }
3614
3615    let free_count = state.call_info.len().saturating_sub(ci_idx + 1);
3616    if free_count <= 1 {
3617        return;
3618    }
3619
3620    // Remove every other cached frame (halve the free list).
3621    // PERF(port): truncation is O(n) copy for the drop; a slab allocator
3622    // would be O(1) — profile in Phase B.
3623    let keep = free_count / 2;
3624    let removed = free_count - keep;
3625    let new_len = ci_idx + 1 + keep;
3626    state.call_info.truncate(new_len);
3627    state.nci = state.nci.saturating_sub(removed as u32);
3628
3629    // Terminate the now-last cached frame.
3630    if let Some(last) = state.call_info.last_mut() {
3631        last.next = None;
3632    }
3633}
3634
3635/// Check whether the C-call depth has reached its limit and raise an error if so.
3636///
3637/// C: `void luaE_checkcstack(lua_State *L)` — LUAI_FUNC (pub(crate))
3638///
3639/// ```c
3640/// // C: void luaE_checkcstack(lua_State *L) {
3641/// //   if (getCcalls(L) == LUAI_MAXCCALLS)
3642/// //     luaG_runerror(L, "C stack overflow");
3643/// //   else if (getCcalls(L) >= (LUAI_MAXCCALLS / 10 * 11))
3644/// //     luaD_throw(L, LUA_ERRERR);
3645/// // }
3646/// ```
3647pub(crate) fn check_c_stack(state: &mut LuaState) -> Result<(), LuaError> {
3648    // C: if (getCcalls(L) == LUAI_MAXCCALLS) luaG_runerror(L, "C stack overflow");
3649    // macros.tsv: getCcalls → state.c_calls()
3650    // error_sites.tsv: luaG_runerror → return Err(LuaError::runtime(format_args!(...)))
3651    if state.c_calls() == LUAI_MAXCCALLS {
3652        return Err(LuaError::runtime(format_args!("C stack overflow")));
3653    }
3654    // C: else if (getCcalls(L) >= (LUAI_MAXCCALLS / 10 * 11)) luaD_throw(L, LUA_ERRERR);
3655    // error_sites.tsv: luaD_throw(L, LUA_ERRERR) → return Err(LuaError::with_status(LuaStatus::ErrErr))
3656    if state.c_calls() >= (LUAI_MAXCCALLS / 10 * 11) {
3657        // TODO(port): LuaError::with_status takes a LuaStatus enum, not a raw i32.
3658        // The exact constructor shape depends on lua-types/error.rs in Phase B.
3659        return Err(LuaError::runtime(format_args!(
3660            "error while handling stack overflow (C stack overflow)"
3661        )));
3662    }
3663    Ok(())
3664}
3665
3666/// Increment the C-call depth counter, checking for overflow.
3667///
3668/// C: `LUAI_FUNC void luaE_incCstack(lua_State *L)` — pub(crate)
3669///
3670/// ```c
3671/// // C: LUAI_FUNC void luaE_incCstack(lua_State *L) {
3672/// //   L->nCcalls++;
3673/// //   if (l_unlikely(getCcalls(L) >= LUAI_MAXCCALLS))
3674/// //     luaE_checkcstack(L);
3675/// // }
3676/// ```
3677pub fn inc_c_stack(state: &mut LuaState) -> Result<(), LuaError> {
3678    // C: L->nCcalls++;
3679    state.nCcalls += 1;
3680    // C: if (l_unlikely(getCcalls(L) >= LUAI_MAXCCALLS)) luaE_checkcstack(L);
3681    // macros.tsv: l_unlikely → x (drop branch hint); getCcalls → state.c_calls()
3682    if state.c_calls() >= LUAI_MAXCCALLS {
3683        check_c_stack(state)?;
3684    }
3685    Ok(())
3686}
3687
3688// C: static void stack_init(lua_State *L1, lua_State *L)
3689//
3690// PORT NOTE: In C, `L` is a separate thread used only for memory allocation
3691// (via `luaM_newvector`).  In Rust we don't have a custom allocator; all
3692// allocation goes through the global Rust allocator.  The function takes only
3693// the new thread (`thread`) and ignores the caller.
3694fn stack_init(thread: &mut LuaState) {
3695    // C: L1->stack.p = luaM_newvector(L, BASIC_STACK_SIZE + EXTRA_STACK, StackValue);
3696    // macros.tsv: luaM_newvector → vec![T::default(); n]
3697    let total_slots = BASIC_STACK_SIZE + EXTRA_STACK;
3698    thread.stack = vec![StackValue::default(); total_slots];
3699
3700    // C: L1->tbclist.p = L1->stack.p;  (tbclist = stack base sentinel = "no tbc vars")
3701    // types.tsv: lua_State.tbclist → Vec<StackIdx>
3702    // PORT NOTE: In C, tbclist.p = stack.p is a sentinel meaning "no tbc vars".
3703    // In Rust the Vec is empty when there are no tbc variables.
3704    thread.tbclist = Vec::new();
3705
3706    // C: for (i = 0; i < BASIC_STACK_SIZE + EXTRA_STACK; i++)
3707    //      setnilvalue(s2v(L1->stack.p + i));  /* erase new stack */
3708    // macros.tsv: setnilvalue → *o = LuaValue::Nil
3709    // Already initialized to LuaValue::Nil via StackValue::default().
3710
3711    // C: L1->top.p = L1->stack.p;  (top = stack base = index 0)
3712    thread.top = StackIdx(0);
3713
3714    // C: L1->stack_last.p = L1->stack.p + BASIC_STACK_SIZE;
3715    thread.stack_last = StackIdx(BASIC_STACK_SIZE as u32);
3716
3717    // C: ci = &L1->base_ci;
3718    // C: ci->next = ci->previous = NULL;
3719    // C: ci->callstatus = CIST_C;
3720    // C: ci->func.p = L1->top.p;     → func = current top = StackIdx(0)
3721    // C: ci->u.c.k = NULL;
3722    // C: ci->nresults = 0;
3723    // C: setnilvalue(s2v(L1->top.p)); → stack[0] = Nil (the "function" entry)
3724    // C: L1->top.p++;                  → top becomes 1
3725    // C: ci->top.p = L1->top.p + LUA_MINSTACK;  → ci.top = 1 + LUA_MINSTACK
3726    // C: L1->ci = ci;                  → ci = CallInfoIdx(0)
3727
3728    let base_ci = CallInfo {
3729        func: StackIdx(0),
3730        top: StackIdx(1 + LUA_MINSTACK as u32),
3731        previous: None,
3732        next: None,
3733        callstatus: CIST_C,
3734        nresults: 0,
3735        u: CallInfoFrame::c_default(),
3736        u2: CallInfoExtra::default(),
3737    };
3738
3739    if thread.call_info.is_empty() {
3740        thread.call_info.push(base_ci);
3741    } else {
3742        thread.call_info[0] = base_ci;
3743        thread.call_info.truncate(1);
3744    }
3745
3746    // C: setnilvalue(s2v(L1->top.p));  stack[top=0] = Nil (function entry for ci)
3747    thread.stack[0] = StackValue { val: LuaValue::Nil, tbc_delta: 0 };
3748
3749    // C: L1->top.p++;
3750    thread.top = StackIdx(1);
3751
3752    // C: L1->ci = ci;
3753    thread.ci = CallInfoIdx(0);
3754}
3755
3756// C: static void freestack(lua_State *L)
3757fn free_stack(state: &mut LuaState) {
3758    // C: if (L->stack.p == NULL) return;  /* stack not completely built yet */
3759    if state.stack.is_empty() {
3760        return;
3761    }
3762    // C: L->ci = &L->base_ci; freeCI(L);
3763    state.ci = CallInfoIdx(0);
3764    free_ci(state);
3765    // C: lua_assert(L->nci == 0);
3766    debug_assert_eq!(state.nci, 0, "nci should be 0 after free_ci");
3767    // C: luaM_freearray(L, L->stack.p, stacksize(L) + EXTRA_STACK);
3768    // macros.tsv: luaM_freearray → (Rust's Drop handles deallocation; drop the call)
3769    state.stack.clear();
3770    state.stack.shrink_to_fit();
3771}
3772
3773// C: static void init_registry(lua_State *L, global_State *g)
3774fn init_registry(state: &mut LuaState) -> Result<(), LuaError> {
3775    // C: Table *registry = luaH_new(L);
3776    // macros.tsv: luaH_new → state.new_table()
3777    let registry = state.new_table();
3778
3779    // C: sethvalue(L, &g->l_registry, registry);
3780    // macros.tsv: sethvalue → *o = LuaValue::Table(x.clone())
3781    state.global_mut().l_registry = LuaValue::Table(registry.clone());
3782
3783    // C: luaH_resize(L, registry, LUA_RIDX_LAST, 0);
3784    // macros.tsv: luaH_resize → t.resize(state, na, nh)?
3785    // TODO(port): registry is a GcRef<LuaTable> (Rc); calling methods requires borrow_mut()
3786    // For Phase A, use RefCell interior mutability on LuaTable, or accept the limitation.
3787    // Using Rc::get_mut is not available because of possible aliasing.
3788    // TODO(port): LuaTable resize requires &mut access through Rc — needs RefCell<LuaTable>
3789    //   or a redesign in Phase B.
3790
3791    // C: setthvalue(L, &registry->array[LUA_RIDX_MAINTHREAD - 1], L);
3792    // macros.tsv: setthvalue → *o = LuaValue::Thread(x.clone())
3793    // TODO(port): cannot create GcRef<LuaState> to self (self-referential Rc).
3794    // In Phase E this would be resolved once coroutine threads are GcRef-tracked.
3795    // For Phase A: leave registry[LUA_RIDX_MAINTHREAD-1] as Nil and add a TODO.
3796    // TODO(port): set registry[LUA_RIDX_MAINTHREAD - 1] = LuaValue::Thread(main_thread_gcref)
3797
3798    // C: sethvalue(L, &registry->array[LUA_RIDX_GLOBALS - 1], luaH_new(L));
3799    // PORT NOTE (phase-b-reconcile): The lua-types LuaTable placeholder is
3800    // storage-less, so we can't actually persist the globals table inside
3801    // the registry via array_set. Store it in a direct GlobalState field
3802    // and patch get_global_table to read it from there. Symmetric for the
3803    // _LOADED module cache. Once the LuaTable placeholder reconciles, the
3804    // canonical registry storage takes over and these fields disappear.
3805    let globals = state.new_table();
3806    state.global_mut().globals = LuaValue::Table(globals);
3807    let loaded = state.new_table();
3808    state.global_mut().loaded = LuaValue::Table(loaded);
3809
3810    Ok(())
3811}
3812
3813// C: static void f_luaopen(lua_State *L, void *ud)
3814fn lua_open(state: &mut LuaState) -> Result<(), LuaError> {
3815    // C: UNUSED(ud);
3816    // C: stack_init(L, L);
3817    stack_init(state);
3818    // C: init_registry(L, g);
3819    init_registry(state)?;
3820    // C: luaS_init(L);
3821    crate::string::init(state)?;
3822    // C: luaT_init(L);
3823    crate::tagmethods::init(state)?;
3824    // C: luaX_init(L);
3825    // TODO(port): luaX_init lives in the lua-lex crate; cross-crate call needed in Phase B
3826    // C: g->gcstp = 0; /* allow gc */
3827    state.global_mut().gcstp = 0;
3828    state.global().heap.unpause();
3829    // C: setnilvalue(&g->nilvalue); /* now state is complete */
3830    // macros.tsv: setnilvalue → *o = LuaValue::Nil
3831    // PORT NOTE: setting nilvalue = Nil signals completestate() → is_complete() = true
3832    state.global_mut().nilvalue = LuaValue::Nil;
3833    // C: luai_userstateopen(L); → no-op; drop
3834    // macros.tsv: luai_userstateopen → (extension hook, no-op default; drop)
3835    Ok(())
3836}
3837
3838// C: static void preinit_thread(lua_State *L, global_State *g)
3839fn preinit_thread(thread: &mut LuaState, global: Rc<RefCell<GlobalState>>) {
3840    // C: G(L) = g;
3841    thread.global = global;
3842    // C: L->stack.p = NULL;
3843    thread.stack = Vec::new();
3844    // C: L->ci = NULL; — sentinel: empty call_info
3845    thread.call_info = Vec::new();
3846    // PORT NOTE: We initialize ci to 0 but call_info is empty; stack_init() must be
3847    // called before any use of call_info.
3848    thread.ci = CallInfoIdx(0);
3849    // C: L->nci = 0;
3850    thread.nci = 0;
3851    // C: L->twups = L; /* thread has no upvalues */
3852    // PORT NOTE: In C, L->twups = L is a self-reference sentinel meaning "no open upvals".
3853    // In Rust, GlobalState.twups is a Vec<GcRef<LuaState>>; absence from that Vec is the
3854    // sentinel.  The per-thread `twups` field is removed (types.tsv: lua_State.twups → removed).
3855    // C: L->nCcalls = 0;
3856    thread.nCcalls = 0;
3857    // C: L->errorJmp = NULL; — replaced by Result<T, LuaError>; no field
3858    // C: L->hook = NULL;
3859    thread.hook = None;
3860    // C: L->hookmask = 0;
3861    thread.hookmask = 0;
3862    // C: L->basehookcount = 0;
3863    thread.basehookcount = 0;
3864    // C: L->allowhook = 1;
3865    thread.allowhook = true;
3866    // C: resethookcount(L); → L->hookcount = L->basehookcount
3867    // macros.tsv: resethookcount → state.reset_hook_count()
3868    thread.hookcount = thread.basehookcount;
3869    // C: L->openupval = NULL;
3870    thread.openupval = Vec::new();
3871    // C: L->status = LUA_OK;
3872    thread.status = LuaStatus::Ok as u8;
3873    // C: L->errfunc = 0;
3874    thread.errfunc = 0;
3875    // C: L->oldpc = 0;
3876    thread.oldpc = 0;
3877}
3878
3879// C: static void close_state(lua_State *L)
3880fn close_state(state: &mut LuaState) {
3881    // C: global_State *g = G(L);
3882    let is_complete = state.global().is_complete();
3883
3884    // C: if (!completestate(g)) luaC_freeallobjects(L); /* just collect its objects */
3885    if !is_complete {
3886        // macros.tsv: luaC_freeallobjects via GcHandle
3887        state.gc().free_all_objects();
3888    } else {
3889        // C: L->ci = &L->base_ci;  /* unwind CallInfo list */
3890        state.ci = CallInfoIdx(0);
3891        // C: luaD_closeprotected(L, 1, LUA_OK);  /* close all upvalues */
3892        // TODO(port): crate::do_::close_protected(state, StackIdx(1), LuaStatus::Ok)
3893        // Ignoring result here because we are in teardown (same as C behavior).
3894        // C: luaC_freeallobjects(L);  /* collect all objects */
3895        state.gc().free_all_objects();
3896        // C: luai_userstateclose(L); → no-op; drop
3897        // macros.tsv: luai_userstateclose → (extension hook; drop)
3898    }
3899
3900    // C: luaM_freearray(L, G(L)->strt.hash, G(L)->strt.size);
3901    // macros.tsv: luaM_freearray → (Rust's Drop handles deallocation; drop the call)
3902    state.global_mut().strt = StringPool::default();
3903
3904    // C: freestack(L);
3905    free_stack(state);
3906
3907    // C: lua_assert(gettotalbytes(g) == sizeof(LG));
3908    // PORT NOTE: C-specific memory accounting assertion; not applicable in Rust.
3909
3910    // C: (*g->frealloc)(g->ud, fromstate(L), sizeof(LG), 0);  /* free main block */
3911    // PORT NOTE: Custom allocator freed LG here. Rust's allocator (via Drop) handles
3912    // deallocation of GlobalState and LuaState automatically.
3913}
3914
3915/// Create a new coroutine thread sharing the same GlobalState as the caller.
3916///
3917/// Pushes the new thread onto the caller's stack and returns `Ok(())`.
3918///
3919/// C: `LUA_API lua_State *lua_newthread(lua_State *L)` (pub)
3920///
3921/// ```c
3922/// // C: LUA_API lua_State *lua_newthread(lua_State *L) {
3923/// //   global_State *g = G(L);
3924/// //   GCObject *o;
3925/// //   lua_State *L1;
3926/// //   lua_lock(L); luaC_checkGC(L);
3927/// //   o = luaC_newobjdt(L, LUA_TTHREAD, sizeof(LX), offsetof(LX, l));
3928/// //   L1 = gco2th(o);
3929/// //   setthvalue2s(L, L->top.p, L1); api_incr_top(L);
3930/// //   preinit_thread(L1, g);
3931/// //   ... (copy hook settings, extra space, stack_init) ...
3932/// //   lua_unlock(L); return L1;
3933/// // }
3934/// ```
3935/// Allocate a fresh coroutine `LuaState`, register it under a new
3936/// `ThreadId`, and push the resulting `LuaValue::Thread(value)` onto
3937/// `state`'s stack.
3938///
3939/// If `initial_body` is `Some(f)`, `f` is also pushed onto the new
3940/// thread's stack so that `coroutine.status` reports `"suspended"`
3941/// rather than `"dead"`. The full cross-thread `xmove` from caller to
3942/// coroutine arrives in slice 02b; `co_create` uses `initial_body` to
3943/// stage the body without needing a real `xmove`.
3944pub fn new_thread(state: &mut LuaState, initial_body: Option<LuaValue>) -> Result<(), LuaError> {
3945    state.gc().check_step();
3946
3947    // C: o = luaC_newobjdt(L, LUA_TTHREAD, sizeof(LX), offsetof(LX, l));
3948    // C: L1 = gco2th(o);
3949    // PORT NOTE: In C, the new thread is GC-allocated as part of the allgc list.
3950    // In Rust (Phase A), we create a plain LuaState; Phase D will wire GC registration.
3951    // TODO(port): allocate via state.gc().new_obj(LuaType::Thread, ...) in Phase D
3952
3953    let global_rc = state.global_rc();
3954    let hookmask = state.hookmask;
3955    let basehookcount = state.basehookcount;
3956
3957    let reserved_id = {
3958        let mut g = state.global_mut();
3959        let id = g.next_thread_id;
3960        g.next_thread_id += 1;
3961        id
3962    };
3963
3964    let mut new_thread = LuaState {
3965        status: LuaStatus::Ok as u8,
3966        allowhook: true,
3967        nci: 0,
3968        top: StackIdx(0),
3969        stack_last: StackIdx(0),
3970        stack: Vec::new(),
3971        ci: CallInfoIdx(0),
3972        call_info: Vec::new(),
3973        openupval: Vec::new(),
3974        tbclist: Vec::new(),
3975        global: global_rc.clone(),
3976        hook: None,
3977        hookmask: 0,
3978        basehookcount: 0,
3979        hookcount: 0,
3980        errfunc: 0,
3981        nCcalls: 0,
3982        oldpc: 0,
3983        marked: 0,
3984        cached_thread_id: reserved_id,
3985    };
3986
3987    // C: preinit_thread(L1, g);
3988    preinit_thread(&mut new_thread, global_rc);
3989
3990    // C: L1->hookmask = L->hookmask;
3991    new_thread.hookmask = hookmask;
3992    // C: L1->basehookcount = L->basehookcount;
3993    new_thread.basehookcount = basehookcount;
3994    // C: L1->hook = L->hook;
3995    // TODO(port): lua_Hook is Box<dyn FnMut(...)>; not Clone.
3996    // Sharing a hook between threads would require Arc<Mutex<...>> (Phase E debug).
3997    // C: resethookcount(L1);
3998    new_thread.reset_hook_count();
3999
4000    // C: memcpy(lua_getextraspace(L1), lua_getextraspace(g->mainthread), LUA_EXTRASPACE);
4001    // macros.tsv: lua_getextraspace → state.extra_space_mut() → &mut [u8]
4002    // TODO(port): LuaState.extra_space field not yet defined; Phase B
4003
4004    // C: luai_userstatethread(L, L1); → no-op; drop
4005    // macros.tsv: luai_userstatethread → (extension hook; drop)
4006
4007    // C: stack_init(L1, L);
4008    stack_init(&mut new_thread);
4009
4010    if let Some(body) = initial_body {
4011        new_thread.push(body);
4012    }
4013
4014    let thread_ref: Rc<RefCell<LuaState>> = Rc::new(RefCell::new(new_thread));
4015
4016    let value = {
4017        let mut g = state.global_mut();
4018        let id = reserved_id;
4019        let value = GcRef::new(lua_types::value::LuaThread::new(id));
4020        g.threads.insert(
4021            id,
4022            ThreadRegistryEntry { state: thread_ref, value: value.clone() },
4023        );
4024        value
4025    };
4026
4027    state.push(LuaValue::Thread(value));
4028
4029    Ok(())
4030}
4031
4032/// Free all resources held by a coroutine thread.
4033///
4034/// C: `void luaE_freethread(lua_State *L, lua_State *L1)` — LUAI_FUNC (pub(crate))
4035///
4036/// ```c
4037/// // C: void luaE_freethread(lua_State *L, lua_State *L1) {
4038/// //   LX *l = fromstate(L1);
4039/// //   luaF_closeupval(L1, L1->stack.p);  /* close all upvalues */
4040/// //   lua_assert(L1->openupval == NULL);
4041/// //   luai_userstatefree(L, L1);
4042/// //   freestack(L1);
4043/// //   luaM_free(L, l);
4044/// // }
4045/// ```
4046pub(crate) fn free_thread(caller: &mut LuaState, thread: &mut LuaState) {
4047    // C: luaF_closeupval(L1, L1->stack.p);  /* close all upvalues */
4048    // TODO(port): crate::func::close_upval(thread, StackIdx(0)) — lfunc.c → func.rs
4049    let _ = caller; // caller used only for luai_userstatefree (no-op)
4050
4051    // C: lua_assert(L1->openupval == NULL);
4052    // macros.tsv: lua_assert → debug_assert!
4053    debug_assert!(
4054        thread.openupval.is_empty(),
4055        "free_thread: open upvalues remain after close_upval"
4056    );
4057
4058    // C: luai_userstatefree(L, L1); → no-op; drop
4059    // macros.tsv: luai_userstatefree → (extension hook; drop)
4060
4061    // C: freestack(L1);
4062    free_stack(thread);
4063
4064    // C: luaM_free(L, l); → Rust's Drop frees LuaState automatically
4065}
4066
4067/// Reset a thread to its base state, closing all to-be-closed variables.
4068///
4069/// Returns the final status code as an `i32` (mirrors the C API).
4070///
4071/// C: `int luaE_resetthread(lua_State *L, int status)` — LUAI_FUNC (pub(crate))
4072///
4073/// ```c
4074/// // C: int luaE_resetthread(lua_State *L, int status) {
4075/// //   CallInfo *ci = L->ci = &L->base_ci;
4076/// //   setnilvalue(s2v(L->stack.p));
4077/// //   ci->func.p = L->stack.p;
4078/// //   ci->callstatus = CIST_C;
4079/// //   if (status == LUA_YIELD) status = LUA_OK;
4080/// //   L->status = LUA_OK;  /* so it can run __close metamethods */
4081/// //   status = luaD_closeprotected(L, 1, status);
4082/// //   if (status != LUA_OK) luaD_seterrorobj(L, status, L->stack.p + 1);
4083/// //   else L->top.p = L->stack.p + 1;
4084/// //   ci->top.p = L->top.p + LUA_MINSTACK;
4085/// //   luaD_reallocstack(L, cast_int(ci->top.p - L->stack.p), 0);
4086/// //   return status;
4087/// // }
4088/// ```
4089pub fn reset_thread(state: &mut LuaState, status: i32) -> i32 {
4090    // C: CallInfo *ci = L->ci = &L->base_ci;
4091    state.ci = CallInfoIdx(0);
4092    let ci_idx = 0usize;
4093
4094    // C: setnilvalue(s2v(L->stack.p));
4095    // macros.tsv: setnilvalue → *o = LuaValue::Nil; s2v → state.stack_at(idx)
4096    if !state.stack.is_empty() {
4097        state.stack[0].val = LuaValue::Nil;
4098    }
4099
4100    // C: ci->func.p = L->stack.p;
4101    state.call_info[ci_idx].func = StackIdx(0);
4102    // C: ci->callstatus = CIST_C;
4103    state.call_info[ci_idx].callstatus = CIST_C;
4104
4105    // C: if (status == LUA_YIELD) status = LUA_OK;
4106    let mut status = if status == LuaStatus::Yield as i32 {
4107        LuaStatus::Ok as i32
4108    } else {
4109        status
4110    };
4111
4112    // C: L->status = LUA_OK;  /* so it can run __close metamethods */
4113    state.status = LuaStatus::Ok as u8;
4114
4115    // C: status = luaD_closeprotected(L, 1, status);
4116    let close_status = crate::do_::close_protected(
4117        state,
4118        StackIdx(1),
4119        LuaStatus::from_raw(status),
4120    );
4121    status = close_status as i32;
4122
4123    // C: if (status != LUA_OK) luaD_seterrorobj(L, status, L->stack.p + 1);
4124    if status != LuaStatus::Ok as i32 {
4125        // C: luaD_seterrorobj(L, status, L->stack.p + 1);
4126        crate::do_::set_error_obj(state, LuaStatus::from_raw(status), StackIdx(1));
4127    } else {
4128        // C: else L->top.p = L->stack.p + 1;
4129        state.top = StackIdx(1);
4130    }
4131
4132    // C: ci->top.p = L->top.p + LUA_MINSTACK;
4133    let new_ci_top = StackIdx(state.top.0 + LUA_MINSTACK as u32);
4134    state.call_info[ci_idx].top = new_ci_top;
4135
4136    // C: luaD_reallocstack(L, cast_int(ci->top.p - L->stack.p), 0);
4137    // TODO(port): crate::do_::realloc_stack(state, new_ci_top.0 as i32, 0) — ldo.c → do_.rs
4138    // For Phase A, grow the stack if needed to at least new_ci_top slots.
4139    let needed = new_ci_top.0 as usize;
4140    if state.stack.len() < needed {
4141        state.stack.resize(needed, StackValue::default());
4142    }
4143
4144    status
4145}
4146
4147/// Close a coroutine thread from the perspective of another thread.
4148///
4149/// C: `LUA_API int lua_closethread(lua_State *L, lua_State *from)` (pub)
4150///
4151/// ```c
4152/// // C: LUA_API int lua_closethread(lua_State *L, lua_State *from) {
4153/// //   int status;
4154/// //   lua_lock(L);
4155/// //   L->nCcalls = (from) ? getCcalls(from) : 0;
4156/// //   status = luaE_resetthread(L, L->status);
4157/// //   lua_unlock(L);
4158/// //   return status;
4159/// // }
4160/// ```
4161pub fn close_thread(state: &mut LuaState, from: Option<&LuaState>) -> i32 {
4162    // C: lua_lock(L); → no-op
4163    // C: L->nCcalls = (from) ? getCcalls(from) : 0;
4164    // macros.tsv: getCcalls → state.c_calls()
4165    state.nCcalls = match from {
4166        Some(f) => f.c_calls(),
4167        None => 0,
4168    };
4169    // C: status = luaE_resetthread(L, L->status);
4170    let current_status = state.status as i32;
4171    let result = reset_thread(state, current_status);
4172    // C: lua_unlock(L); → no-op
4173    result
4174}
4175
4176/// Deprecated wrapper for `close_thread(L, NULL)`.
4177///
4178/// C: `LUA_API int lua_resetthread(lua_State *L)` (pub, deprecated)
4179///
4180/// ```c
4181/// // C: LUA_API int lua_resetthread(lua_State *L) {
4182/// //   return lua_closethread(L, NULL);
4183/// // }
4184/// ```
4185pub fn reset_thread_api(state: &mut LuaState) -> i32 {
4186    // C: return lua_closethread(L, NULL);
4187    close_thread(state, None)
4188}
4189
4190/// Create a new independent Lua state.  Returns `None` only on OOM.
4191///
4192/// C: `LUA_API lua_State *lua_newstate(lua_Alloc f, void *ud)` (pub)
4193///
4194/// PORT NOTE: The C API takes a custom allocator `(f, ud)`.  The Rust-native API
4195/// uses the global Rust allocator; those parameters are dropped.  Equivalent to
4196/// `LuaState::new()` at the call site.
4197///
4198/// ```c
4199/// // C: LUA_API lua_State *lua_newstate(lua_Alloc f, void *ud) {
4200/// //   int i;
4201/// //   lua_State *L;
4202/// //   global_State *g;
4203/// //   LG *l = cast(LG *, (*f)(ud, NULL, LUA_TTHREAD, sizeof(LG)));
4204/// //   if (l == NULL) return NULL;
4205/// //   L = &l->l.l; g = &l->g;
4206/// //   L->tt = LUA_VTHREAD;
4207/// //   g->currentwhite = bitmask(WHITE0BIT);
4208/// //   L->marked = luaC_white(g);
4209/// //   preinit_thread(L, g);
4210/// //   g->allgc = obj2gco(L);
4211/// //   L->next = NULL;
4212/// //   incnny(L);
4213/// //   g->frealloc = f; g->ud = ud; g->warnf = NULL; g->ud_warn = NULL;
4214/// //   g->mainthread = L; g->seed = luai_makeseed(L);
4215/// //   g->gcstp = GCSTPGC;
4216/// //   ... (zero-init all GC list pointers and tunables) ...
4217/// //   setivalue(&g->nilvalue, 0);  /* signal: state not yet built */
4218/// //   ... (setgcparam tunables) ...
4219/// //   for (i=0; i < LUA_NUMTAGS; i++) g->mt[i] = NULL;
4220/// //   if (luaD_rawrunprotected(L, f_luaopen, NULL) != LUA_OK) {
4221/// //     close_state(L); L = NULL;
4222/// //   }
4223/// //   return L;
4224/// // }
4225/// ```
4226pub fn new_state() -> Option<LuaState> {
4227    // C: LG *l = (*f)(ud, NULL, LUA_TTHREAD, sizeof(LG)); if (l == NULL) return NULL;
4228    // In Rust, allocation failure panics by default; we use Result internally.
4229
4230    // Build a dummy LuaString for memerrmsg and strcache initialization.
4231    // This is a chicken-and-egg problem: GlobalState.memerrmsg needs to be initialized
4232    // before luaS_init, but luaS_init creates the memerrmsg.
4233    // We use a placeholder Rc<LuaString> that will be replaced by luaS_init.
4234    // TODO(port): this is fragile; Phase B should ensure memerrmsg is properly set by luaS_init.
4235    // TODO(D-1c-bridge): allocation outside state context (new_state() free fn — no LuaState yet)
4236    let placeholder_str = GcRef::new(LuaString::placeholder());
4237
4238    // C: g->currentwhite = bitmask(WHITE0BIT);
4239    // macros.tsv: bitmask → (1u32 << b); WHITE0BIT = 0 → 1u8
4240    let initial_white = 1u8 << WHITE0BIT;
4241
4242    // C: setivalue(&g->nilvalue, 0);  /* to signal that state is not yet built */
4243    // macros.tsv: setivalue → *o = LuaValue::Int(x)
4244    // PORT NOTE: non-nil nilvalue signals "state not yet complete"; see is_complete().
4245
4246    let global = GlobalState {
4247        parser_hook: None,
4248        file_loader_hook: None,
4249        file_open_hook: None,
4250        popen_hook: None,
4251        file_remove_hook: None,
4252        file_rename_hook: None,
4253        os_execute_hook: None,
4254        dynlib_load_hook: None,
4255        dynlib_symbol_hook: None,
4256        dynlib_unload_hook: None,
4257        totalbytes: std::mem::size_of::<GlobalState>() as isize,
4258        gc_debt: 0,
4259        gc_estimate: 0,
4260        lastatomic: 0,
4261        strt: StringPool::default(),
4262        l_registry: LuaValue::Nil,
4263        globals: LuaValue::Nil,
4264        loaded: LuaValue::Nil,
4265        // C: setivalue(&g->nilvalue, 0); — non-Nil = incomplete
4266        nilvalue: LuaValue::Int(0),
4267        // C: g->seed = luai_makeseed(L);
4268        seed: make_seed(),
4269        // C: g->currentwhite = bitmask(WHITE0BIT);
4270        currentwhite: initial_white,
4271        // C: g->gcstate = GCSpause;
4272        gcstate: GCS_PAUSE,
4273        // C: g->gckind = KGC_INC;
4274        // macros.tsv: KGC_INC → GcKind::Incremental
4275        gckind: GcKind::Incremental as u8,
4276        // C: g->gcstopem = 0;
4277        gcstopem: false,
4278        // C: g->genminormul = LUAI_GENMINORMUL;
4279        genminormul: LUAI_GENMINORMUL,
4280        // C: setgcparam(g->genmajormul, LUAI_GENMAJORMUL); → g->genmajormul = LUAI_GENMAJORMUL / 4
4281        // macros.tsv: setgcparam → p = v / 4
4282        genmajormul: (LUAI_GENMAJORMUL / 4) as u8,
4283        // C: g->gcstp = GCSTPGC;
4284        gcstp: GCSTPGC,
4285        // C: g->gcemergency = 0;
4286        gcemergency: false,
4287        // C: setgcparam(g->gcpause, LUAI_GCPAUSE);
4288        gcpause: (LUAI_GCPAUSE / 4) as u8,
4289        // C: setgcparam(g->gcstepmul, LUAI_GCMUL);
4290        gcstepmul: (LUAI_GCMUL / 4) as u8,
4291        // C: g->gcstepsize = LUAI_GCSTEPSIZE;
4292        gcstepsize: LUAI_GCSTEPSIZE,
4293        sweepgc_cursor: 0,
4294        weak_tables_registry: Vec::new(),
4295        gc_tracked_long_strings: Vec::new(),
4296        pending_finalizers: Vec::new(),
4297        to_be_finalized: Vec::new(),
4298        // C: g->twups = NULL;
4299        twups: Vec::new(),
4300        // C: g->panic = NULL;
4301        panic: None,
4302        // C: g->mainthread = L; — set after main thread created
4303        mainthread: None,
4304        threads: std::collections::HashMap::new(),
4305        main_thread_value: GcRef::new(lua_types::value::LuaThread::new(0)),
4306        current_thread_id: 0,
4307        main_thread_id: 0,
4308        next_thread_id: 1,
4309        memerrmsg: placeholder_str.clone(),
4310        tmname: Vec::new(),
4311        // C: for (i=0; i < LUA_NUMTAGS; i++) g->mt[i] = NULL;
4312        mt: std::array::from_fn(|_| None),
4313        strcache: std::array::from_fn(|_| {
4314            std::array::from_fn(|_| placeholder_str.clone())
4315        }),
4316        interned_lt: std::collections::HashMap::new(),
4317        warnf: None,
4318        c_functions: Vec::new(),
4319        heap: lua_gc::Heap::new(),
4320        cross_thread_upvals: std::collections::HashMap::new(),
4321        suspended_parent_stacks: Vec::new(),
4322        suspended_parent_open_upvals: Vec::new(),
4323    };
4324
4325    let global_rc = Rc::new(RefCell::new(global));
4326
4327    // C: L->tt = LUA_VTHREAD; — encoded by LuaValue::Thread enum variant
4328    // C: L->marked = luaC_white(g);
4329    // macros.tsv: luaC_white → g.current_white()
4330    let initial_marked = initial_white;
4331
4332    let mut main_thread = LuaState {
4333        status: LuaStatus::Ok as u8,
4334        allowhook: true,
4335        nci: 0,
4336        top: StackIdx(0),
4337        stack_last: StackIdx(0),
4338        stack: Vec::new(),
4339        ci: CallInfoIdx(0),
4340        call_info: Vec::new(),
4341        openupval: Vec::new(),
4342        tbclist: Vec::new(),
4343        global: global_rc.clone(),
4344        hook: None,
4345        hookmask: 0,
4346        basehookcount: 0,
4347        hookcount: 0,
4348        errfunc: 0,
4349        nCcalls: 0,
4350        oldpc: 0,
4351        marked: initial_marked,
4352        cached_thread_id: 0,
4353    };
4354
4355    // C: preinit_thread(L, g);
4356    preinit_thread(&mut main_thread, global_rc.clone());
4357
4358    // C: incnny(L); /* main thread is always non yieldable */
4359    // macros.tsv: incnny → state.inc_nny() → L->nCcalls += 0x10000
4360    main_thread.inc_nny();
4361
4362    // C: g->mainthread = L;
4363    // TODO(port): self-referential Rc cycle; Phase D GC handles cycles.
4364    // For Phase A: skip setting mainthread to avoid the cycle.
4365
4366    // C: g->allgc = obj2gco(L); /* by now, only object is the main thread */
4367    // TODO(port): Phase D — register main_thread in allgc as a GcRef
4368
4369    // C: if (luaD_rawrunprotected(L, f_luaopen, NULL) != LUA_OK) {
4370    //      close_state(L); L = NULL; }
4371    // error_sites.tsv: luaD_rawrunprotected → state.run_protected(|s| f(s, ud))
4372    // PORT NOTE: We call lua_open directly since we're not using the protected-call
4373    // machinery yet (ldo.c is not ported). Errors from lua_open propagate as Err.
4374    match lua_open(&mut main_thread) {
4375        Ok(()) => {}
4376        Err(_) => {
4377            // C: close_state(L); L = NULL;
4378            close_state(&mut main_thread);
4379            return None;
4380        }
4381    }
4382
4383    Some(main_thread)
4384}
4385
4386/// Close the Lua state and free all resources.
4387///
4388/// C: `LUA_API void lua_close(lua_State *L)` (pub)
4389///
4390/// PORT NOTE: In C, `lua_close` gets the main thread via `G(L)->mainthread`
4391/// and closes that regardless of which thread is passed.  In Rust, the caller
4392/// should hold the main `LuaState` and drop it (which triggers `close_state`
4393/// via this function or `Drop`).
4394///
4395/// ```c
4396/// // C: LUA_API void lua_close(lua_State *L) {
4397/// //   lua_lock(L);
4398/// //   L = G(L)->mainthread;  /* only the main thread can be closed */
4399/// //   close_state(L);
4400/// // }
4401/// ```
4402pub fn close(mut state: LuaState) {
4403    // C: lua_lock(L); → no-op; macros.tsv: lua_lock → (drop entirely)
4404    // C: L = G(L)->mainthread;
4405    // PORT NOTE: In Rust, callers must pass the main LuaState directly (or obtain it
4406    // from GlobalState.mainthread).  We do not traverse to the main thread here;
4407    // the caller owns the root state.
4408    // TODO(port): assert that `state` is indeed the main thread before closing
4409    // C: close_state(L);
4410    close_state(&mut state);
4411    // C: state drops here; Rust's Drop frees the LuaState struct
4412}
4413
4414/// Forward a warning message through the configured warning sink.
4415///
4416/// C: `void luaE_warning(lua_State *L, const char *msg, int tocont)` — LUAI_FUNC (pub(crate))
4417///
4418/// ```c
4419/// // C: void luaE_warning(lua_State *L, const char *msg, int tocont) {
4420/// //   lua_WarnFunction wf = G(L)->warnf;
4421/// //   if (wf != NULL) wf(G(L)->ud_warn, msg, tocont);
4422/// // }
4423/// ```
4424pub(crate) fn warning(state: &mut LuaState, msg: &[u8], to_cont: bool) {
4425    // C: lua_WarnFunction wf = G(L)->warnf;
4426    // C: if (wf != NULL) wf(G(L)->ud_warn, msg, tocont);
4427    // types.tsv: global_State.warnf → Option<Box<dyn FnMut(&[u8], bool)>>
4428    // types.tsv: global_State.ud_warn → (removed; folded into the closure)
4429    // PORT NOTE: We must drop the RefMut borrow before calling the closure to avoid
4430    // a potential re-entrant borrow_mut() if the closure calls back into Lua.
4431    // We check for the presence of warnf while holding a borrow, then call it.
4432    // TODO(port): if the warning function needs to call back into state (e.g. to push
4433    // a Lua error), this will panic at runtime due to RefCell re-entry. Phase B should
4434    // design a safe re-entrance pattern (e.g. take + restore the warnf closure).
4435    let has_warnf = state.global().warnf.is_some();
4436    if has_warnf {
4437        // Take the warnf closure out to avoid re-entrant borrow.
4438        let mut warnf = state.global_mut().warnf.take();
4439        if let Some(ref mut f) = warnf {
4440            f(msg, to_cont);
4441        }
4442        // Restore the closure.
4443        state.global_mut().warnf = warnf;
4444    }
4445}
4446
4447/// Emit a warning composed from the error object on top of the stack and a location.
4448///
4449/// C: `void luaE_warnerror(lua_State *L, const char *where)` — LUAI_FUNC (pub(crate))
4450///
4451/// ```c
4452/// // C: void luaE_warnerror(lua_State *L, const char *where) {
4453/// //   TValue *errobj = s2v(L->top.p - 1);
4454/// //   const char *msg = (ttisstring(errobj))
4455/// //                   ? getstr(tsvalue(errobj))
4456/// //                   : "error object is not a string";
4457/// //   luaE_warning(L, "error in ", 1);
4458/// //   luaE_warning(L, where, 1);
4459/// //   luaE_warning(L, " (", 1);
4460/// //   luaE_warning(L, msg, 1);
4461/// //   luaE_warning(L, ")", 0);
4462/// // }
4463/// ```
4464pub(crate) fn warn_error(state: &mut LuaState, where_: &[u8]) {
4465    // C: TValue *errobj = s2v(L->top.p - 1);
4466    // macros.tsv: s2v → state.stack_at(idx)
4467    let top_idx = state.top.0.saturating_sub(1) as usize;
4468    let errobj = state.stack.get(top_idx).map(|sv| sv.val.clone()).unwrap_or(LuaValue::Nil);
4469
4470    // C: const char *msg = (ttisstring(errobj)) ? getstr(tsvalue(errobj)) : "error object is not a string";
4471    // macros.tsv: ttisstring → matches!(o, LuaValue::Str(_))
4472    // macros.tsv: getstr → ts.as_bytes(); tsvalue → o.as_string().expect("not string")
4473    // PORT NOTE: Clone the message bytes to avoid holding a borrow on `state.stack`
4474    // across the subsequent `warning()` calls which mutably borrow `state`.
4475    let msg: Vec<u8> = if let LuaValue::Str(ref s) = errobj {
4476        s.as_bytes().to_vec()
4477    } else {
4478        b"error object is not a string".to_vec()
4479    };
4480
4481    // C: luaE_warning(L, "error in ", 1);
4482    warning(state, b"error in ", true);
4483    // C: luaE_warning(L, where, 1);
4484    warning(state, where_, true);
4485    // C: luaE_warning(L, " (", 1);
4486    warning(state, b" (", true);
4487    // C: luaE_warning(L, msg, 1);
4488    warning(state, &msg, true);
4489    // C: luaE_warning(L, ")", 0);
4490    warning(state, b")", false);
4491}
4492
4493// ──────────────────────────────────────────────────────────────────────────────
4494// PORT STATUS
4495//   source:        src/lstate.c  (445 lines, 25 functions)
4496//                  src/lstate.h  (408 lines; struct definitions merged)
4497//   target_crate:  lua-vm
4498//   confidence:    medium
4499//   todos:         44
4500//   port_notes:    34
4501//   unsafe_blocks: 0   (must be 0 outside explicit unsafe-budget crates)
4502//   notes:         Logic faithfully follows lstate.c. Key structural changes:
4503//                  (1) LX/LG C layout wrappers dropped; GlobalState is Rc<RefCell<>>.
4504//                  (2) CallInfo linked list → Vec<CallInfo> with CallInfoIdx indices;
4505//                      shrink_ci uses truncation rather than node-by-node removal.
4506//                  (3) lua_State.twups self-reference → membership in GlobalState.twups Vec.
4507//                  (4) errorJmp/setjmp → removed; errors use Result<T, LuaError>.
4508//                  (5) Custom allocator (lua_Alloc) → dropped; Rust's allocator handles it.
4509//                  (6) make_seed: ASLR pointer entropy requires unsafe; time-only for Phase A.
4510//                  (7) Perf: LuaState.cached_thread_id stores the thread's own id once at
4511//                      construction; upvalue_get/_set compare against this u64 field
4512//                      instead of borrowing global.current_thread_id on every read.
4513//                      Invariant survives coroutine resume because each thread caches its
4514//                      OWN id, not the global's id (see field doc on cached_thread_id).
4515//                  (8) Perf: LuaTableRefExt::{raw_set, raw_set_int, get, get_int,
4516//                      get_short_str, metatable, as_ptr} and table_{raw,set_with_tm,
4517//                      array_set} carry #[inline] so the per-set dispatch chain
4518//                      collapses into set_i_value / vm.rs OP_SETI callers. The
4519//                      historical reject_invalid_table_key precheck moved into
4520//                      LuaTable::try_raw_set (lua-types) and was dropped at this
4521//                      layer; raw_set now takes the key by value, eliminating a
4522//                      24-byte LuaValue clone per set. gc_barrier_back is invoked
4523//                      before the store in table_set_with_tm (semantically
4524//                      equivalent: the barrier only inspects the value's color,
4525//                      not its location), letting v be moved directly into
4526//                      table_raw_set without an intermediate clone.
4527//                  Key TODOs: luaT_init and luaX_init cross-crate calls (Phase B);
4528//                  init_registry table mutations through Rc (needs RefCell<LuaTable>);
4529//                  luaD_closeprotected/seterrorobj/reallocstack in reset_thread (ldo.c);
4530//                  GcRef<LuaState> self-reference for mainthread (Phase D);
4531//                  LuaString::placeholder() helper needed for GlobalState init;
4532//                  LuaValue and LuaTable should move to object.rs once that lands.
4533// ──────────────────────────────────────────────────────────────────────────────