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