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