Skip to main content

lua_vm/
state.rs

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