Skip to main content

lua_vm/
state.rs

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