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