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