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