Skip to main content

lua_vm/
state.rs

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