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