Skip to main content

luna_core/runtime/
heap.rs

1//! GC heap v1: precise stop-the-world mark & sweep over an intrusive
2//! all-objects list (PUC `allgc` shape). All unsafe object plumbing is
3//! confined to this module and `string`/`table` internals.
4//!
5//! `Gc<T>` safety contract: the runtime is single-threaded; a `Gc` pointer is
6//! valid until a `collect()` call that does not reach it from the given
7//! roots. Callers must root every value they keep across a collect.
8
9use std::fmt;
10use std::ops::Deref;
11use std::ptr::{self, NonNull};
12
13use crate::runtime::function::{LuaClosure, NativeClosure, Proto, UpvalState, Upvalue};
14use crate::runtime::string::{self, LuaStr, StringTable};
15use crate::runtime::table::Table;
16use crate::runtime::userdata::{Userdata, UserdataPayload};
17use crate::runtime::value::Value;
18
19/// Discriminator the GC stores in every [`GcHeader`] so a raw header pointer
20/// can be cast back to the right object kind during tracing and sweeping.
21#[derive(Clone, Copy, PartialEq, Eq, Debug)]
22#[repr(u8)]
23pub enum ObjTag {
24    /// [`crate::runtime::string::LuaStr`].
25    Str,
26    /// [`crate::runtime::table::Table`].
27    Table,
28    /// [`crate::runtime::function::Proto`].
29    Proto,
30    /// [`crate::runtime::function::LuaClosure`].
31    Closure,
32    /// [`crate::runtime::function::Upvalue`].
33    Upvalue,
34    /// [`crate::runtime::function::NativeClosure`].
35    Native,
36    /// [`crate::runtime::coroutine::Coro`].
37    Coro,
38    /// [`crate::runtime::userdata::Userdata`].
39    Userdata,
40}
41
42/// Header prefix on every GC-managed object: intrusive next-link + type tag +
43/// mark bits. Always at offset 0 of the containing struct (`#[repr(C)]`).
44#[repr(C)]
45pub struct GcHeader {
46    next: *mut GcHeader,
47    tag: ObjTag,
48    /// tricolor + finalizer state. PUC `gch.marked` layout (lgc.h):
49    ///   bit 0 WHITE0 — current-white-A
50    ///   bit 1 WHITE1 — current-white-B (the unused white in any given cycle
51    ///                  is the "other-white" / dead-white at sweep time)
52    ///   bit 2 BLACK  — propagated; outgoing refs already traced
53    ///   bit 3 FIN    — registered for `__gc` (tracked in `finalize`)
54    ///   bit 4 FINALIZED — already enqueued or finalized once this lifetime
55    ///   bit 5 DEFERRED  — 5.3 cycle-finalize deferral marker (gc.lua :502)
56    /// Gray = no white bits, no BLACK; that is the in-stack state between the
57    /// time a Marker visits an object and the time it traces it.
58    flags: u8,
59}
60
61const WHITE0: u8 = 1;
62const WHITE1: u8 = 2;
63const BLACK: u8 = 4;
64const WHITE_BITS: u8 = WHITE0 | WHITE1;
65const COLOR_BITS: u8 = WHITE_BITS | BLACK;
66
67/// registered for finalization (`__gc`): the object is tracked in `finalize`.
68const FIN: u8 = 8;
69/// finalization already scheduled/run: never finalize this object again (PUC
70/// FINALIZEDBIT). Set when it moves to `tobefnz`.
71const FINALIZED: u8 = 16;
72/// resurrected once because a reference cycle through a coroutine kept the
73/// finalizable alive (PUC 5.3 gc.lua :502 "two collections are needed to
74/// break cycle"). The next time the object is found unreachable it is moved
75/// to `tobefnz` without re-deferring.
76const DEFERRED: u8 = 32;
77
78#[inline(always)]
79fn is_white(flags: u8) -> bool {
80    flags & WHITE_BITS != 0
81}
82#[inline(always)]
83fn is_black(flags: u8) -> bool {
84    flags & BLACK != 0
85}
86
87/// True when an object header has been reached by marking (gray or black).
88/// `pub(crate)` so other runtime modules (e.g. `Table::refs_contain_unmarked_coro`)
89/// can probe reachability without owning the bit constants. Equivalent to
90/// `isgray(o) || isblack(o)` in PUC.
91pub(crate) fn header_is_marked(h: *mut GcHeader) -> bool {
92    // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
93    unsafe { !is_white((*h).flags) }
94}
95
96impl GcHeader {
97    pub(crate) fn new(tag: ObjTag) -> GcHeader {
98        GcHeader {
99            next: ptr::null_mut(),
100            tag,
101            flags: 0,
102        }
103    }
104}
105
106/// `Copy` handle to a heap-allocated GC-managed object. Layout is a single
107/// `NonNull<T>`; the GC walks reachability via root scanning and intrusive
108/// linkage on [`GcHeader`], not via reference counts.
109pub struct Gc<T> {
110    ptr: NonNull<T>,
111}
112
113impl<T> Clone for Gc<T> {
114    fn clone(&self) -> Self {
115        *self
116    }
117}
118impl<T> Copy for Gc<T> {}
119
120impl<T> Gc<T> {
121    #[doc(hidden)]
122    pub fn from_ptr(p: *mut T) -> Gc<T> {
123        Gc {
124            ptr: NonNull::new(p).expect("gc pointer must be non-null"),
125        }
126    }
127
128    /// Raw pointer to the referent. Always non-null; valid for the lifetime
129    /// of the [`Heap`] that allocated it as long as the object is reachable.
130    pub fn as_ptr(self) -> *mut T {
131        self.ptr.as_ptr()
132    }
133
134    /// Pointer-identity equality (PUC `rawequal` for reference types).
135    pub fn ptr_eq(self, other: Gc<T>) -> bool {
136        self.ptr == other.ptr
137    }
138
139    /// SAFETY: caller must ensure no other live reference to the object and
140    /// no collect() while the borrow is held (single-threaded runtime).
141    ///
142    /// `#[doc(hidden)]` so the documented public surface needs no `unsafe`:
143    /// embedders should not see this in rustdoc. The safe path for mutating
144    /// freshly-allocated tables is the `TableBuilder` / `vm.table_of(...)` API.
145    /// Cross-crate access from `luna` (e.g. `jit_backend`, `capi`) keeps
146    /// working — `#[doc(hidden)] pub` doesn't demote visibility, just docs.
147    #[doc(hidden)]
148    pub unsafe fn as_mut<'a>(self) -> &'a mut T {
149        // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
150        unsafe { &mut *self.ptr.as_ptr() }
151    }
152}
153
154impl<T> Deref for Gc<T> {
155    type Target = T;
156    fn deref(&self) -> &T {
157        // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
158        unsafe { self.ptr.as_ref() }
159    }
160}
161
162impl<T> fmt::Debug for Gc<T> {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        write!(f, "Gc({:p})", self.ptr.as_ptr())
165    }
166}
167
168/// Incremental GC phase.
169///   * `Pause`     — no cycle in progress; all objects current-white.
170///   * `Propagate` — gray queue + propagate-state populated; mutator runs
171///                   alongside `gc_step_propagate(budget)` calls. Born objects
172///                   stamp the current-white; barriers re-gray modified
173///                   parents. Transitions to `Sweep` via `gc_finish_atomic`.
174///   * `Sweep`     — `sweep_cur` is the detached old heap being budget-swept.
175///
176/// `mark_all` (the STW path used by `collect_ex`) sequences start_propagate +
177/// drain_all + finish_atomic inline, never crossing a step boundary.
178#[derive(Clone, Copy, PartialEq, Eq, Debug)]
179enum GcPhase {
180    Pause,
181    Propagate,
182    Sweep,
183}
184
185/// Cross-step traversal state for the incremental Propagate phase. Owned by
186/// `Heap.propagate` (`Some` between `gc_start_propagate` and `gc_finish_atomic`,
187/// `None` otherwise). The gray queue itself lives in `Heap.gray` so write
188/// barriers can push directly without going through the Option.
189struct PropagateState {
190    weak: Vec<*mut Table>,
191    ephemeron: Vec<*mut Table>,
192    cached_protos: Vec<*mut Proto>,
193    no_ephemeron: bool,
194}
195
196/// luna's incremental mark-sweep GC heap. Owns every [`Gc<T>`] allocation
197/// (one per Vm); produces handles via the `new_*` constructors and traces
198/// reachability through registered roots. Holds the string-intern table and
199/// the auto-GC pacing state.
200pub struct Heap {
201    all: *mut GcHeader,
202    strings: StringTable,
203    seed: u32,
204    live: usize,
205    /// approximate allocated bytes — shells only. Each `link()` adds
206    /// `size_of::<T>` for its tag; each `free_obj` subtracts the same.
207    /// Internal Vec/Box growth (table array/hash parts, proto code,
208    /// closure upvals slice) is NOT auto-tracked, so this is a lower
209    /// bound on real memory. PUC's `g->GCtotalbytes` is exact because
210    /// `lmem.c` routes every malloc/free through one helper; luna pays
211    /// for that uniformity in exchange for a smaller, drift-free count.
212    bytes: usize,
213    /// byte threshold at which the VM should run a collection (auto-GC pacing)
214    next_gc: usize,
215    /// PUC `g->currentwhite`: which white bit (WHITE0 or WHITE1) means
216    /// "born / surviving this cycle". The other white is the dead-white that
217    /// sweep collects. Flipped at the end of each mark cycle (`atomic`).
218    current_white: u8,
219    /// Persistent gray queue: holds objects grayed by write barriers between
220    /// the time the marker first reached them and the next propagate step.
221    /// Lives outside `propagate` so barriers can push without going through
222    /// the Option; `gc_step_propagate` and `gc_finish_atomic` drain it.
223    gray: Vec<*mut GcHeader>,
224    /// Incremental traversal state. `Some` between `gc_start_propagate` and
225    /// `gc_finish_atomic` (and inline within `mark_all`); `None` otherwise.
226    propagate: Option<PropagateState>,
227    /// incremental-sweep phase (Pause unless a `step` cycle is mid-sweep)
228    phase: GcPhase,
229    /// the remaining detached object list being swept during `GcPhase::Sweep`;
230    /// survivors are spliced back onto `all`, garbage is freed
231    sweep_cur: *mut GcHeader,
232    /// `collectgarbage("stop")`: auto-GC is suspended while true
233    gc_stopped: bool,
234    /// objects registered for finalization (a live `__gc` metamethod was set);
235    /// parallel-tracked — ownership stays on `all` (PUC `finobj`).
236    finalize: Vec<*mut GcHeader>,
237    /// dead finalizables resurrected this cycle, awaiting their `__gc` call by
238    /// the VM (PUC `tobefnz`). Drained via `take_tobefnz`.
239    tobefnz: Vec<*mut GcHeader>,
240    /// PUC 5.1 has no ephemeron pass: a `__mode='k'` table marks its values
241    /// strongly during traversal, so entries like `a[t]=t` (key and value the
242    /// same fresh object) survive even with nothing else referencing `t`.
243    /// 5.2+ replaced that with ephemeron convergence. gc.lua's "weak tables"
244    /// section in 5.1 asserts 3*lim survivors, 5.4 only 2*lim — the loop2
245    /// pair was retired from the newer test as a result.
246    pub(crate) no_ephemeron: bool,
247    /// PUC 5.3 finalizes a table caught in a cycle through an unreachable
248    /// coroutine one GC round later than the unreachability is detected
249    /// ("two collections are needed to break cycle", gc.lua :502). 5.4 and 5.5
250    /// rewrote the GC and finalize the same cycle in a single pass (their
251    /// gc.lua :544 asserts collected after one `collectgarbage()`). 5.1/5.2
252    /// don't exercise this path. Set by the VM at construction.
253    pub(crate) defer_thread_cycle_finalize: bool,
254    /// Pool of freed Table allocations.
255    /// btrees-style workloads create + free ~32k tables per iter;
256    /// jemalloc's malloc/free roundtrip costs ~30ns per table = ~960µs
257    /// total per iter. Pool recycle: free_obj pushes the raw Table
258    /// pointer here instead of dropping; new_table pops + resets fields.
259    /// Cap at 4096 entries to avoid unbounded growth (worst-case: 4096
260    /// × sizeof(Table) ≈ 460 KB resident memory in idle pool).
261    table_pool: Vec<std::ptr::NonNull<crate::runtime::table::Table>>,
262    /// `gc-verify` — headers freed since the last collect
263    /// began. O(1) read-time dangling probes (`Vm::op_index`) test
264    /// membership here; cleared when the next mark starts. Only exact
265    /// under ASAN-style quarantining allocators (no immediate reuse).
266    #[cfg(feature = "gc-verify")]
267    pub(crate) recently_freed: std::collections::HashSet<usize>,
268    /// Embedding memory cap. When `Some(n)`, the VM's run loop watches
269    /// `bytes` between dispatch turns and, on overshoot, runs a full collect
270    /// and (still overshooting) raises a catchable "memory cap exceeded"
271    /// Lua error. A soft cap, not a hard alloc-time refusal: a single
272    /// allocation can briefly push `bytes` past `n`, but the embedder gets
273    /// control back at the next safe point — host policy.
274    pub(crate) mem_cap: Option<usize>,
275}
276
277/// Initial auto-GC threshold and floor (PUC GCSTEPSIZE-ish pacing).
278const GC_MIN_THRESHOLD: usize = 1 << 20;
279
280impl Heap {
281    /// Build a fresh empty heap with default GC pacing and no memory cap.
282    pub fn new() -> Heap {
283        Heap {
284            all: ptr::null_mut(),
285            strings: StringTable::new(),
286            seed: make_seed(),
287            live: 0,
288            bytes: 0,
289            next_gc: GC_MIN_THRESHOLD,
290            current_white: WHITE0,
291            gray: Vec::new(),
292            propagate: None,
293            phase: GcPhase::Pause,
294            sweep_cur: ptr::null_mut(),
295            gc_stopped: false,
296            finalize: Vec::new(),
297            tobefnz: Vec::new(),
298            no_ephemeron: false,
299            defer_thread_cycle_finalize: false,
300            mem_cap: None,
301            table_pool: Vec::new(),
302            #[cfg(feature = "gc-verify")]
303            recently_freed: std::collections::HashSet::new(),
304        }
305    }
306
307    fn link(&mut self, h: *mut GcHeader) {
308        // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
309        unsafe {
310            (*h).next = self.all;
311            // Born color depends on phase:
312            //   * Pause / Sweep — born current-white (PUC `luaC_white(g)`);
313            //     reachable from roots gets marked next cycle.
314            //   * Propagate     — born BLACK (PUC `LUAGCRYOUNG` / sasimpl
315            //     of new-during-cycle). Born-current-white during Propagate
316            //     would lose the WHITE bits at the upcoming atomic flip and
317            //     be swept this same cycle even when reachable from a
318            //     barrier-grayed root. Born BLACK skips the trace and
319            //     transitions to current-white at sweep, matching the
320            //     reachable-survivor flow.
321            let born = if self.phase == GcPhase::Propagate {
322                BLACK
323            } else {
324                self.current_white
325            };
326            (*h).flags = ((*h).flags & !COLOR_BITS) | born;
327        }
328        self.all = h;
329        self.live += 1;
330    }
331
332    /// Take ownership of a boxed object and put it under GC management.
333    /// SAFETY-by-convention: `T` must be `repr(C)` with a `GcHeader` first
334    /// field whose tag matches `T` (enforced by the typed constructors).
335    pub(crate) fn adopt<T>(&mut self, obj: Box<T>) -> Gc<T> {
336        let p = Box::into_raw(obj);
337        self.link(p as *mut GcHeader);
338        self.bytes += std::mem::size_of::<T>();
339        Gc::from_ptr(p)
340    }
341
342    /// Allocate and adopt a fresh empty [`Table`].
343    pub fn new_table(&mut self) -> Gc<Table> {
344        // table_pool fast path. When btrees-
345        // style alloc bursts have left freed Tables in the pool, pop a
346        // recycled one and reset its fields instead of mallocing fresh.
347        // Saves ~30ns per alloc (malloc roundtrip elided).
348        let p = if let Some(ptr) = self.table_pool.pop() {
349            let t = ptr.as_ptr();
350            // gc-verify: the pointer is alive again — drop it from the
351            // freed log so read-time probes don't flag the recycled table.
352            #[cfg(feature = "gc-verify")]
353            {
354                self.recently_freed.remove(&(t as usize));
355                crate::runtime::gc_verify_probe::FREED
356                    .with(|f| f.borrow_mut().remove(&(t as usize)));
357            }
358            // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
359            unsafe {
360                // Reset to fresh-Table state. Box-owned slab/nodes/
361                // metatable were already cleared in `free_obj` before
362                // pool push, so we only reset stack-resident fields here.
363                (*t).hdr = GcHeader::new(ObjTag::Table);
364                (*t).array_ptr = std::ptr::null_mut();
365                (*t).asize = 0;
366                (*t).inline_storage =
367                    std::cell::UnsafeCell::new([0; crate::runtime::table::INLINE_U64S]);
368                (*t).lastfree = 0;
369                (*t).flags = 0;
370            }
371            t
372        } else {
373            Box::into_raw(Box::new(Table::new(GcHeader::new(ObjTag::Table))))
374        };
375        // Link + bytes accounting (same as adopt path).
376        self.link(p as *mut GcHeader);
377        self.bytes += std::mem::size_of::<Table>();
378        let g = Gc::from_ptr(p);
379        // the Table is now at its final heap address; wire
380        // `array_ptr` to point at the inline storage that lives inside
381        // the boxed Table.
382        // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
383        unsafe { g.as_mut() }.init_array_ptr();
384        g
385    }
386
387    /// Adopt an empty table and pre-allocate `asize`
388    /// NIL slots in the array part. Equivalent to `new_table()`
389    /// followed by `set_int(N, Nil)` worth of `rehash`es, except
390    /// the array reaches its final size in one allocation rather
391    /// than O(log N) doubling rounds.
392    ///
393    /// `asize == 0` is identical to `new_table()`. Larger sizes
394    /// are clamped at the array part's hard ceiling
395    /// `MAX_ASIZE = 2^27`; requests beyond that fall back to the
396    /// empty table, which the interpreter would have grown
397    /// gracefully via rehash anyway.
398    pub fn new_table_sized(&mut self, asize: usize) -> Gc<Table> {
399        const MAX_ASIZE_HINT: usize = 1 << 27;
400        let g = self.new_table();
401        let clamped = asize.min(MAX_ASIZE_HINT);
402        if clamped > 0 {
403            // SAFETY: the freshly adopted table has no live borrow
404            // anywhere else; we hold the only `Gc<Table>` handle.
405            unsafe { g.as_mut() }.resize(self, clamped, 0);
406        }
407        g
408    }
409
410    /// Adopt a compiler-built prototype (its `hdr` must carry ObjTag::Proto).
411    pub fn adopt_proto(&mut self, proto: Proto) -> Gc<Proto> {
412        debug_assert!(proto.hdr.tag == ObjTag::Proto);
413        self.adopt(Box::new(proto))
414    }
415
416    /// Back-compat constructor for callers that already
417    /// built a `Box<[Gc<Upvalue>]>`. Internally re-routes through
418    /// `new_closure_inline` so small-upval cases also pick the
419    /// inline path (the input Box is freed after the copy).
420    pub fn new_closure(&mut self, proto: Gc<Proto>, upvals: Box<[Gc<Upvalue>]>) -> Gc<LuaClosure> {
421        use crate::runtime::function::INLINE_UPVALS_N;
422        let n = upvals.len();
423        if n <= INLINE_UPVALS_N {
424            let g = self.new_closure_inline(proto, &upvals);
425            drop(upvals);
426            g
427        } else {
428            // Large closure — store the input Box directly in
429            // `overflow`, no copy.
430            self.adopt_closure_with(proto, n as u32, |c| {
431                c.overflow = upvals;
432            })
433        }
434    }
435
436    /// Hot-path constructor for the `Op::Closure` handler.
437    /// Takes a slice (typically backed by a stack array) so the caller
438    /// doesn't allocate a Vec/Box just to hand it over. Upvals are
439    /// copied into `inline_storage` for small closures, or into a
440    /// freshly-allocated `Box<[..]>` for the rare overflow case.
441    pub fn new_closure_inline(
442        &mut self,
443        proto: Gc<Proto>,
444        upvals: &[Gc<Upvalue>],
445    ) -> Gc<LuaClosure> {
446        use crate::runtime::function::INLINE_UPVALS_N;
447        let n = upvals.len();
448        self.adopt_closure_with(proto, n as u32, |c| {
449            if n <= INLINE_UPVALS_N {
450                for (i, &uv) in upvals.iter().enumerate() {
451                    // SAFETY: exclusive &mut c inside the constructor;
452                    // write through the cell so no direct borrow of
453                    // the inline array is formed (see the field doc).
454                    unsafe {
455                        (*c.inline_storage.get())[i] = std::mem::MaybeUninit::new(uv);
456                    }
457                }
458            } else {
459                c.overflow = upvals.to_vec().into_boxed_slice();
460            }
461        })
462    }
463
464    fn adopt_closure_with<F: FnOnce(&mut LuaClosure)>(
465        &mut self,
466        proto: Gc<Proto>,
467        upvals_len: u32,
468        fill: F,
469    ) -> Gc<LuaClosure> {
470        let mut boxed = Box::new(LuaClosure {
471            hdr: GcHeader::new(ObjTag::Closure),
472            proto,
473            upvals_ptr: std::ptr::null_mut(),
474            upvals_len,
475            inline_storage: std::cell::UnsafeCell::new(
476                [std::mem::MaybeUninit::<Gc<Upvalue>>::uninit();
477                    crate::runtime::function::INLINE_UPVALS_N],
478            ),
479            overflow: Box::new([]),
480        });
481        // Box is heap-stable now — populate storage at the final
482        // address so `upvals_ptr` will be valid.
483        fill(&mut boxed);
484        let g = self.adopt(boxed);
485        // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
486        unsafe { g.as_mut() }.init_upvals_ptr();
487        g
488    }
489
490    /// Allocate a [`NativeClosure`] wrapping host function `f` with the
491    /// given captured upvalues.
492    pub fn new_native(
493        &mut self,
494        f: crate::runtime::value::NativeFn,
495        upvals: Box<[Value]>,
496    ) -> Gc<NativeClosure> {
497        self.adopt(Box::new(NativeClosure {
498            hdr: GcHeader::new(ObjTag::Native),
499            f,
500            upvals,
501            is_async: false,
502        }))
503    }
504
505    /// Like [`Heap::new_native`] but tags the
506    /// closure with `is_async = true`. The dispatcher's native-call
507    /// path then transmutes `f` to `AsyncNativeFn` and routes through
508    /// the cooperative-yield path. The caller is responsible for
509    /// having transmuted the `AsyncNativeFn` pointer to `NativeFn`
510    /// shape (both are `fn` pointers of the same size); see
511    /// [`crate::vm::async_drive`] for the helper that does this.
512    pub fn new_async_native(
513        &mut self,
514        f: crate::runtime::value::NativeFn,
515        upvals: Box<[Value]>,
516    ) -> Gc<NativeClosure> {
517        self.adopt(Box::new(NativeClosure {
518            hdr: GcHeader::new(ObjTag::Native),
519            f,
520            upvals,
521            is_async: true,
522        }))
523    }
524
525    /// Allocate a fresh [`Upvalue`] cell in the given `state` (open / closed).
526    pub fn new_upvalue(&mut self, state: UpvalState) -> Gc<Upvalue> {
527        self.adopt(Box::new(Upvalue {
528            hdr: GcHeader::new(ObjTag::Upvalue),
529            state,
530        }))
531    }
532
533    /// Create a fresh suspended coroutine wrapping `body`. The new thread
534    /// inherits the creator's globals table; a `setfenv(0, env)` inside it
535    /// will retune that copy without affecting the creator.
536    pub fn new_coro(
537        &mut self,
538        body: Value,
539        globals: Gc<crate::runtime::Table>,
540    ) -> Gc<crate::runtime::Coro> {
541        self.adopt(Box::new(crate::runtime::Coro {
542            hdr: GcHeader::new(ObjTag::Coro),
543            status: crate::runtime::CoroStatus::Suspended,
544            body,
545            started: false,
546            resumer: None,
547            resume_at: None,
548            error_value: None,
549            error_traceback: None,
550            error_levels: None,
551            natives: 0..0,
552            stack: Vec::new(),
553            frames: Vec::new(),
554            frame_ccmt: Vec::new(),
555            open_upvals: Vec::new(),
556            tbc: Vec::new(),
557            top: 0,
558            pcall_depth: 0,
559            hook: crate::vm::exec::HookState::default(),
560            globals,
561        }))
562    }
563
564    /// Create a userdata (an io file handle — luna's only userdata) with no
565    /// metatable yet; the io library installs the shared `FILE*` metatable.
566    pub fn new_userdata(&mut self, payload: UserdataPayload, writable: bool) -> Gc<Userdata> {
567        self.adopt(Box::new(Userdata::new(
568            GcHeader::new(ObjTag::Userdata),
569            payload,
570            writable,
571        )))
572    }
573
574    /// Create (or find) a string. Short strings (≤ 40 bytes) are interned.
575    pub fn intern(&mut self, bytes: &[u8]) -> Gc<LuaStr> {
576        if bytes.len() <= string::MAX_SHORT_LEN {
577            let (p, is_new) = self.strings.intern(bytes, self.seed);
578            if is_new {
579                self.link(p as *mut GcHeader);
580                self.bytes += string::alloc_size(bytes.len());
581            } else {
582                // PUC `luaS_new` resurrect guard (lstring.c).
583                // The bucket-chain is walked open-loop without consulting GC
584                // color; during incremental sweep an existing entry may be
585                // dead-white (in `sweep_cur`, scheduled for `free_obj`). If we
586                // hand its pointer back, the budget-paced sweep frees it out
587                // from under the mutator and the next bucket walk dereferences
588                // a libc-recycled slot — the symptom recorded in
589                // `0x800002a80000002d` deep in `StringTable::intern`).
590                //
591                // Flip the white bits to `current_white` so the upcoming sweep
592                // skips it (PUC `changewhite`). Black / not-white objects are
593                // already safe and untouched.
594                // SAFETY: `p` came from `StringTable::intern` and is a valid
595                // `LuaStr` header (its bucket chain is consistent under our
596                // single-threaded heap).
597                unsafe {
598                    let f = (*(p as *mut GcHeader)).flags;
599                    if is_white(f) && (f & self.current_white) == 0 {
600                        (*(p as *mut GcHeader)).flags = (f & !WHITE_BITS) | self.current_white;
601                    }
602                }
603            }
604            Gc::from_ptr(p)
605        } else {
606            let p = string::alloc_long(bytes, self.seed);
607            self.link(p as *mut GcHeader);
608            self.bytes += string::alloc_size(bytes.len());
609            Gc::from_ptr(p)
610        }
611    }
612
613    /// Number of GC-managed objects currently linked into the heap (live + not
614    /// yet swept). Useful for `collectgarbage("count")`-style introspection.
615    pub fn live_objects(&self) -> usize {
616        self.live
617    }
618
619    /// Approximate heap size in bytes.
620    pub fn bytes(&self) -> usize {
621        self.bytes
622    }
623
624    /// Pure-read walk over the intrusive `all`
625    /// objects list, invoking `visit(tag)` once per live (or
626    /// not-yet-swept) GC-managed object. Used by `luna-tools`'s
627    /// `luna-heap-dump` to build a per-type histogram; embedders
628    /// can reuse it for ad-hoc heap introspection.
629    ///
630    /// # Read-only contract
631    ///
632    /// The callback receives only the [`ObjTag`] discriminant and
633    /// is invoked under a `&self` borrow on the heap: no pointer
634    /// to the GC payload escapes, no `as_mut`-style aliasing is
635    /// available, and the walk performs zero allocation in the
636    /// loop. Safe to call between dispatch ticks (the only allocs
637    /// happen in the caller's bookkeeping).
638    ///
639    /// The walk visits both the live `all` list and the
640    /// `sweep_cur` detached list so a mid-cycle invocation reports
641    /// the same total as [`Heap::live_objects`].
642    pub fn walk_objects(&self, mut visit: impl FnMut(ObjTag)) {
643        for head in [self.all, self.sweep_cur] {
644            let mut cur = head;
645            while !cur.is_null() {
646                // SAFETY: pointers come from the runtime's
647                // intrusive all-objects list. `&self` borrow on
648                // the heap prevents concurrent mutation; the GC
649                // cannot run while this walk holds the borrow,
650                // so every `next` link is valid until consumed.
651                let (tag, next) = unsafe { ((*cur).tag, (*cur).next) };
652                visit(tag);
653                cur = next;
654            }
655        }
656    }
657
658    /// Whether allocation has crossed the auto-GC threshold (cheap safe-point
659    /// check for the interpreter loop).
660    #[inline(always)]
661    pub fn gc_due(&self) -> bool {
662        !self.gc_stopped && self.bytes >= self.next_gc
663    }
664
665    /// `collectgarbage("stop"/"restart")`: suspend or resume auto-GC.
666    pub(crate) fn gc_is_stopped(&self) -> bool {
667        self.gc_stopped
668    }
669
670    pub(crate) fn gc_set_stopped(&mut self, stopped: bool) {
671        self.gc_stopped = stopped;
672    }
673
674    /// Re-arm with caller-supplied `pause` (PUC param, % of live bytes). The
675    /// next cycle fires once `bytes >= live * pause / 100`. `pause=200` (PUC
676    /// default) waits for the heap to double; `pause=100` fires immediately
677    /// when alloc resumes; `pause=300` is 3× — lower pause = more aggressive.
678    pub fn rearm_gc_pause(&mut self, pause: i64) {
679        let pause = pause.max(0) as usize;
680        let target = self
681            .bytes
682            .saturating_mul(pause)
683            .saturating_div(100)
684            .max(GC_MIN_THRESHOLD);
685        self.next_gc = target;
686    }
687
688    /// Re-arm the auto-GC threshold after a collection (PUC pause-style: next
689    /// collection once the live set roughly doubles).
690    pub fn rearm_gc(&mut self) {
691        self.next_gc = self.bytes.saturating_mul(2).max(GC_MIN_THRESHOLD);
692    }
693
694    /// Apply a `before → after` box-size delta from a Table mutation
695    /// (`set`/`rehash`/`ensure_*`). Grows credit `Heap.bytes`; shrinks
696    /// debit it. `free_obj` for `ObjTag::Table` then subtracts the table's
697    /// final `internal_bytes()` so the round-trip is symmetric across the
698    /// table's whole lifetime.
699    pub(crate) fn apply_bytes_delta(&mut self, before: usize, after: usize) {
700        if after > before {
701            self.bytes += after - before;
702        } else if before > after {
703            self.bytes = self.bytes.saturating_sub(before - after);
704        }
705    }
706
707    /// Mark from `roots`, sweep everything unreachable. Returns the number of
708    /// objects freed.
709    pub fn collect(&mut self, roots: &[Value]) -> usize {
710        self.collect_ex(roots, &[])
711    }
712
713    /// Like `collect`, with additional bare-object roots (e.g. the VM's open
714    /// upvalues, which are not first-class Values).
715    pub(crate) fn collect_ex(&mut self, roots: &[Value], extra: &[*mut GcHeader]) -> usize {
716        // a full STW collection subsumes any in-flight incremental cycle:
717        // drive it to completion (Propagate → atomic → Sweep → Pause) so `all`
718        // holds the whole heap again with all marks cleared, then run a fresh
719        // STW cycle. Any tobefnz from the finished cycle stays queued and is
720        // re-marked (kept alive) by the upcoming mark_all so the VM's
721        // run_finalizers can still see them.
722        if self.phase == GcPhase::Propagate {
723            self.gc_finish_atomic();
724        }
725        if self.phase == GcPhase::Sweep {
726            self.gc_sweep_step(usize::MAX);
727        }
728        self.mark_all(roots, extra);
729        self.full_sweep()
730    }
731
732    /// Stop-the-world mark from `roots`/`extra`. Builds an ephemeral marker,
733    /// seeds from roots + extra + tobefnz + any barrier-carried gray queue,
734    /// propagates to completion, then runs the atomic tail (weak / ephemeron
735    /// / finalizer resurrection / current-white flip). After return all
736    /// reachable objects are BLACK and `current_white` has flipped, so the
737    /// caller's sweep tests `other-white` for dead. Does NOT change `phase`.
738    fn mark_all(&mut self, roots: &[Value], extra: &[*mut GcHeader]) {
739        let mut m = Marker {
740            stack: Vec::new(),
741            weak: Vec::new(),
742            ephemeron: Vec::new(),
743            no_ephemeron: self.no_ephemeron,
744            cached_protos: Vec::new(),
745        };
746        // Drain any barrier-grayed objects carried over: each was demoted from
747        // BLACK back to gray by a write barrier and is awaiting (re-)trace.
748        m.stack.append(&mut self.gray);
749        for &r in roots {
750            m.value(r);
751        }
752        for &h in extra {
753            m.header(h);
754        }
755        // objects already queued for finalization but not yet run must stay
756        // alive until the VM calls their `__gc` (they may be unreachable now).
757        for &h in &self.tobefnz {
758            m.header(h);
759        }
760        drain_marker(&mut m);
761        // ephemeron convergence: a weak-key entry's value is reachable only if
762        // the key is. Marking a value can make another key reachable, so repeat
763        // until no value is newly marked (PUC convergeephemerons).
764        if !m.ephemeron.is_empty() {
765            loop {
766                let mut changed = false;
767                let eph = m.ephemeron.clone();
768                for t in eph {
769                    // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
770                    changed |= unsafe { (*t).converge_ephemeron(&weak_key_alive, &mut m) };
771                }
772                drain_marker(&mut m);
773                if !changed {
774                    break;
775                }
776            }
777        }
778        self.atomic_tail(&mut m);
779    }
780
781    /// PUC `atomic()` tail: weak-table value-clear, finalizer resurrection,
782    /// post-resurrection ephemeron convergence, proto cache, key-clear, late
783    /// value-clear, and current-white flip. Marker is consumed; `weak` is
784    /// empty on return.
785    ///
786    /// Shared between the STW path (`mark_all`) and the incremental path
787    /// (`gc_finish_atomic`). PUC 5.5 `lgc.c::atomic` mirror:
788    ///   propagate → remarkupvals → convergeephemerons
789    ///   → clearbyvalues(weak, NULL)            ─ early value-clear
790    ///   → clearbyvalues(allweak, NULL)         ─ (same pass under luna)
791    ///   → origweak = g->weak                   ─ snapshot pre-resurrection
792    ///   → separatetobefnz(0) + markbeingfnz    ─ separate_finalizables
793    ///   → propagateall + convergeephemerons    ─ post-resurrection
794    ///   → clearbykeys(ephemeron) + clearbykeys(allweak)
795    ///   → clearbyvalues(weak, origweak)        ─ NEW (post-resurrect) only
796    ///   → clearbyvalues(allweak, origall)      ─ (same)
797    /// The `origweak` split matters because finalizer resurrection can
798    /// re-trace fresh proto/closure → new weak tables joining `m.weak`;
799    /// PUC limits the late value-clear to those new heads.
800    fn atomic_tail(&mut self, m: &mut Marker) {
801        let early_is_dead = |v: Value| -> bool {
802            let h = match v {
803                Value::Str(_) => return false,
804                Value::Table(t) => t.as_ptr() as *mut GcHeader,
805                Value::Closure(c) => c.as_ptr() as *mut GcHeader,
806                Value::Native(n) => n.as_ptr() as *mut GcHeader,
807                Value::Coro(c) => c.as_ptr() as *mut GcHeader,
808                Value::Userdata(u) => u.as_ptr() as *mut GcHeader,
809                _ => return false,
810            };
811            // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
812            unsafe { is_white((*h).flags) }
813        };
814        let mark_string = |v: Value| {
815            if let Value::Str(s) = v {
816                // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
817                unsafe {
818                    let h = s.as_ptr() as *mut GcHeader;
819                    // strings are leaves: skip gray and go straight to black
820                    (*h).flags = ((*h).flags & !COLOR_BITS) | BLACK;
821                }
822            }
823        };
824        // (1) early clearbyvalues — drop dead-value entries from every weak
825        // table on `m.weak` (PUC's combined `clearbyvalues(weak, NULL) +
826        // clearbyvalues(allweak, NULL)`). Keys are deferred to the
827        // post-resurrection sweep below.
828        for t in &m.weak {
829            // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
830            unsafe {
831                let (_wk, wv) = (**t).weak_mode();
832                if wv {
833                    (**t).clear_weak(false, true, &early_is_dead, &mark_string);
834                }
835            }
836        }
837        // (2) `origweak` snapshot — PUC takes the list head; luna's `m.weak`
838        // is a Vec, so the equivalent is its length before resurrection.
839        // Anything appended past this index is a "NEW" weak table that the
840        // resurrection pass brought into view.
841        let origweak_n = m.weak.len();
842        // (3) separate + markbeingfnz — resurrect every registered finalizable
843        // that ended up unmarked. `m.header(h)` enqueues each into the marker
844        // so the following drain_marker propagates through it.
845        self.separate_finalizables(m);
846        drain_marker(m);
847        // (4) post-resurrection ephemeron convergence — a resurrected
848        // finalizable may bring new keys into reach, which in turn marks new
849        // ephemeron values.
850        if !m.ephemeron.is_empty() {
851            loop {
852                let mut changed = false;
853                let eph = m.ephemeron.clone();
854                for t in eph {
855                    // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
856                    changed |= unsafe { (*t).converge_ephemeron(&weak_key_alive, m) };
857                }
858                drain_marker(m);
859                if !changed {
860                    break;
861                }
862            }
863        }
864        // (5) closure-cache weak refs — PUC `traverseproto` clears
865        // `Proto.cache` when the cached LClosure ended the cycle unmarked.
866        // Without this, an LClosure whose only outstanding reference is the
867        // proto's cache would survive forever and its upvalues' `__gc`
868        // finalisers would never run.
869        for &p in &m.cached_protos {
870            // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
871            unsafe {
872                if let Some(c) = (*p).cache.get() {
873                    let h = c.as_ptr() as *mut GcHeader;
874                    if is_white((*h).flags) {
875                        (*p).cache.set(None);
876                    }
877                }
878            }
879        }
880        // (6) clearbykeys — drop entries whose weak key did not survive
881        // marking, across every weak table (PUC's `clearbykeys(ephemeron)
882        // + clearbykeys(allweak)`). Pure key sweep — value-dead entries are
883        // either already nil from step (1) or wait for step (7).
884        for t in &m.weak {
885            // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
886            unsafe {
887                let (wk, _wv) = (**t).weak_mode();
888                if wk {
889                    (**t).clear_weak(true, false, &early_is_dead, &mark_string);
890                }
891            }
892        }
893        // (7) late clearbyvalues — PUC's `clearbyvalues(weak, origweak) +
894        // clearbyvalues(allweak, origall)`. Limit the sweep to NEW heads so
895        // we don't redo work already done in step (1) for the pre-resurrect
896        // tables (they were drained by then and re-marking happens through
897        // mark_string in step (6)). resurrected weak tables joining `m.weak`
898        // past `origweak_n` get their first value-clear here.
899        let weak_snapshot = std::mem::take(&mut m.weak);
900        for t in &weak_snapshot[origweak_n..] {
901            // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
902            unsafe {
903                let (_wk, wv) = (**t).weak_mode();
904                if wv {
905                    (**t).clear_weak(false, true, &early_is_dead, &mark_string);
906                }
907            }
908        }
909        // PUC 5.5 `atomic` end: flip currentwhite so survivors (presently
910        // BLACK) get transitioned into the *new* current-white during sweep,
911        // and the pre-flip current-white becomes the dead-white (the bit the
912        // sweep tests for). Born-during-sweep allocations stamp the new
913        // current-white via `Heap::link`, so they survive this cycle.
914        self.current_white ^= WHITE_BITS;
915        // `gc-verify` — tricolor invariant check at the one
916        // moment it is exact: marking is complete, nothing is freed yet.
917        // A BLACK (surviving) table holding a dead-white child means a
918        // write barrier was missed; the child will be freed by the
919        // upcoming sweep and the table left holding a dangling pointer.
920        // Nothing is dangling *yet*, so the reporter may safely print
921        // string contents to identify the key.
922        #[cfg(feature = "gc-verify")]
923        self.verify_tricolor("atomic_tail");
924    }
925
926    /// `gc-verify` — the set of every live object header
927    /// (all + sweep_cur + finalizer queues), for callers that need to
928    /// audit their own containers (e.g. the VM auditing its register
929    /// stack after a collect).
930    #[cfg(feature = "gc-verify")]
931    pub fn debug_live_set(&self) -> std::collections::HashSet<usize> {
932        let mut live = std::collections::HashSet::new();
933        // SAFETY: heap-owned intrusive lists; all elements are live
934        // allocations.
935        unsafe {
936            let mut cur = self.all;
937            while !cur.is_null() {
938                live.insert(cur as usize);
939                cur = (*cur).next;
940            }
941            let mut cur = self.sweep_cur;
942            while !cur.is_null() {
943                live.insert(cur as usize);
944                cur = (*cur).next;
945            }
946        }
947        for &h in &self.finalize {
948            live.insert(h as usize);
949        }
950        for &h in &self.tobefnz {
951            live.insert(h as usize);
952        }
953        live
954    }
955
956    /// See call site in [`Heap::atomic_tail`]. Panics on the first
957    /// BLACK table whose collectable child is dead-white (missed
958    /// barrier — that child is about to be swept while still
959    /// referenced).
960    #[cfg(feature = "gc-verify")]
961    fn verify_tricolor(&self, ctx: &str) {
962        let new_white = self.current_white;
963        // SAFETY: `all` is the heap's own intrusive list; every element
964        // is a live allocation (nothing has been freed this cycle yet).
965        unsafe {
966            let is_live = |v: Value| -> bool {
967                let h = match v {
968                    Value::Str(s) => s.as_ptr() as *mut GcHeader,
969                    Value::Table(t) => t.as_ptr() as *mut GcHeader,
970                    Value::Closure(c) => c.as_ptr() as *mut GcHeader,
971                    Value::Native(n) => n.as_ptr() as *mut GcHeader,
972                    Value::Coro(c) => c.as_ptr() as *mut GcHeader,
973                    Value::Userdata(u) => u.as_ptr() as *mut GcHeader,
974                    _ => return true,
975                };
976                let f = (*h).flags;
977                !(is_white(f) && (f & new_white) == 0)
978            };
979            let mut cur = self.all;
980            while !cur.is_null() {
981                let f = (*cur).flags;
982                if (*cur).tag == ObjTag::Table && (f & BLACK) != 0 {
983                    let t = &*(cur as *const Table);
984                    // Weak tables may legitimately reference dead objects
985                    // between clear passes; they were just cleared above,
986                    // but their sanctioned dead_key nodes are skipped by
987                    // verify_refs anyway. Only strong tables assert.
988                    let (wk, wv) = t.weak_mode();
989                    if wk || wv {
990                        cur = (*cur).next;
991                        continue;
992                    }
993                    let tp = cur as usize;
994                    t.verify_refs(&is_live, &|what, idx, v| {
995                        // Pre-sweep: dereferencing v is still safe — name
996                        // the key when it is a string.
997                        let detail = match v.as_bytes() {
998                            Some(b) => format!("str {:?}", String::from_utf8_lossy(b)),
999                            None => format!(
1000                                "non-str {:#x}",
1001                                match v {
1002                                    Value::Table(t) => t.as_ptr() as usize,
1003                                    Value::Closure(c) => c.as_ptr() as usize,
1004                                    Value::Coro(c) => c.as_ptr() as usize,
1005                                    Value::Userdata(u) => u.as_ptr() as usize,
1006                                    Value::Native(n) => n.as_ptr() as usize,
1007                                    _ => 0,
1008                                }
1009                            ),
1010                        };
1011                        panic!(
1012                            "[gc-verify] {ctx}: BLACK table {tp:#x} holds dead-white {what} \
1013                             (slot {idx}): {detail} — missed write barrier"
1014                        );
1015                    });
1016                }
1017                cur = (*cur).next;
1018            }
1019        }
1020    }
1021
1022    /// Borrow Heap's persistent propagate state as an ephemeral Marker.
1023    /// Caller MUST call `stash_marker` with the same Marker after work to
1024    /// write the (potentially mutated) state back. Used by the incremental
1025    /// Propagate path to avoid lifetime entanglement between `&mut self` and
1026    /// `&mut self.propagate`.
1027    fn loan_marker(&mut self) -> Marker {
1028        let mut prop = self
1029            .propagate
1030            .take()
1031            .expect("propagate state taken outside Propagate phase");
1032        Marker {
1033            stack: std::mem::take(&mut self.gray),
1034            weak: std::mem::take(&mut prop.weak),
1035            ephemeron: std::mem::take(&mut prop.ephemeron),
1036            no_ephemeron: prop.no_ephemeron,
1037            cached_protos: std::mem::take(&mut prop.cached_protos),
1038        }
1039    }
1040
1041    fn stash_marker(&mut self, m: Marker) {
1042        let no_ephemeron = m.no_ephemeron;
1043        self.gray = m.stack;
1044        self.propagate = Some(PropagateState {
1045            weak: m.weak,
1046            ephemeron: m.ephemeron,
1047            cached_protos: m.cached_protos,
1048            no_ephemeron,
1049        });
1050    }
1051
1052    /// Begin an incremental mark cycle: seed the persistent gray queue from
1053    /// roots + extra + tobefnz + any barrier-carried gray, install a fresh
1054    /// PropagateState, and enter `GcPhase::Propagate`. Precondition: `Pause`.
1055    pub(crate) fn gc_start_propagate(&mut self, roots: &[Value], extra: &[*mut GcHeader]) {
1056        debug_assert!(self.phase == GcPhase::Pause);
1057        self.phase = GcPhase::Propagate;
1058        self.propagate = Some(PropagateState {
1059            weak: Vec::new(),
1060            ephemeron: Vec::new(),
1061            cached_protos: Vec::new(),
1062            no_ephemeron: self.no_ephemeron,
1063        });
1064        let mut m = self.loan_marker();
1065        for &r in roots {
1066            m.value(r);
1067        }
1068        for &h in extra {
1069            m.header(h);
1070        }
1071        for &h in &self.tobefnz {
1072            m.header(h);
1073        }
1074        self.stash_marker(m);
1075    }
1076
1077    /// Drain up to `budget` gray objects (blacken + trace). Returns true if
1078    /// the gray queue is now empty (caller should follow up with
1079    /// `gc_finish_atomic`). PUC `propagatemark` budgeted loop.
1080    pub(crate) fn gc_step_propagate(&mut self, budget: usize) -> bool {
1081        debug_assert!(self.phase == GcPhase::Propagate);
1082        let mut m = self.loan_marker();
1083        let mut n = 0;
1084        while n < budget {
1085            let Some(h) = m.stack.pop() else {
1086                break;
1087            };
1088            // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1089            unsafe {
1090                (*h).flags = ((*h).flags & !WHITE_BITS) | BLACK;
1091                match (*h).tag {
1092                    ObjTag::Str => {}
1093                    ObjTag::Table => (*(h as *mut Table)).trace(&mut m),
1094                    ObjTag::Proto => (*(h as *mut Proto)).trace(&mut m),
1095                    ObjTag::Closure => (*(h as *mut LuaClosure)).trace(&mut m),
1096                    ObjTag::Upvalue => (*(h as *mut Upvalue)).trace(&mut m),
1097                    ObjTag::Native => (*(h as *mut NativeClosure)).trace(&mut m),
1098                    ObjTag::Coro => (*(h as *mut crate::runtime::Coro)).trace(&mut m),
1099                    ObjTag::Userdata => (*(h as *mut Userdata)).trace(&mut m),
1100                }
1101            }
1102            n += 1;
1103        }
1104        let exhausted = m.stack.is_empty();
1105        self.stash_marker(m);
1106        exhausted
1107    }
1108
1109    /// Conclude a Propagate cycle: drain any residual gray, run the atomic
1110    /// tail (weak / ephemeron / finalizer / proto-cache / flip), detach `all`
1111    /// into `sweep_cur`, and enter `GcPhase::Sweep`. Releases `propagate`.
1112    /// PUC `atomic` + `entersweep` transition.
1113    pub(crate) fn gc_finish_atomic(&mut self) {
1114        debug_assert!(self.phase == GcPhase::Propagate);
1115        let mut m = self.loan_marker();
1116        // any residual gray (caller may not have drained to empty)
1117        drain_marker(&mut m);
1118        // pre-atomic ephemeron convergence
1119        if !m.ephemeron.is_empty() {
1120            loop {
1121                let mut changed = false;
1122                let eph = m.ephemeron.clone();
1123                for t in eph {
1124                    // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1125                    changed |= unsafe { (*t).converge_ephemeron(&weak_key_alive, &mut m) };
1126                }
1127                drain_marker(&mut m);
1128                if !changed {
1129                    break;
1130                }
1131            }
1132        }
1133        self.atomic_tail(&mut m);
1134        // PropagateState consumed; transition to Sweep phase by detaching
1135        // the whole heap into sweep_cur (mirrors gc_mark_atomic). Anything
1136        // allocated past this point links onto fresh `all` and survives.
1137        self.propagate = None;
1138        debug_assert!(self.gray.is_empty(), "gray queue not drained at atomic");
1139        self.sweep_cur = std::mem::replace(&mut self.all, ptr::null_mut());
1140        self.phase = GcPhase::Sweep;
1141    }
1142
1143    /// Phase peek (for the VM-side step driver).
1144    pub(crate) fn gc_phase_is_pause(&self) -> bool {
1145        self.phase == GcPhase::Pause
1146    }
1147    pub(crate) fn gc_phase_is_propagate(&self) -> bool {
1148        self.phase == GcPhase::Propagate
1149    }
1150    #[allow(dead_code)] // public phase-peek API trio; sweep variant unused internally
1151    pub(crate) fn gc_phase_is_sweep(&self) -> bool {
1152        self.phase == GcPhase::Sweep
1153    }
1154
1155    /// Forward write barrier: when a BLACK `parent` acquires a fresh reference
1156    /// to a WHITE `child`, gray the child (strings go straight to BLACK as
1157    /// leaves) and push onto the persistent gray queue so the next propagate
1158    /// step traces it. Mirrors PUC `luaC_barrier_`. No-op outside Propagate
1159    /// (parent is gray or white — the mutator never sees a BLACK object live
1160    /// outside an incremental cycle).
1161    #[allow(clippy::not_unsafe_ptr_arg_deref)] // Internal GC barrier; caller (Gc<T>::write_*) guarantees ptr validity per SAFETY below.
1162    pub fn barrier_forward(&mut self, parent: *mut GcHeader, child: Value) {
1163        // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1164        unsafe {
1165            if !is_black((*parent).flags) {
1166                return;
1167            }
1168            let ch = match child {
1169                Value::Str(s) => s.as_ptr() as *mut GcHeader,
1170                Value::Table(t) => t.as_ptr() as *mut GcHeader,
1171                Value::Closure(c) => c.as_ptr() as *mut GcHeader,
1172                Value::Native(n) => n.as_ptr() as *mut GcHeader,
1173                Value::Coro(c) => c.as_ptr() as *mut GcHeader,
1174                Value::Userdata(u) => u.as_ptr() as *mut GcHeader,
1175                _ => return,
1176            };
1177            let cf = (*ch).flags;
1178            if !is_white(cf) {
1179                return;
1180            }
1181            if (*ch).tag == ObjTag::Str {
1182                (*ch).flags = (cf & !COLOR_BITS) | BLACK;
1183            } else {
1184                (*ch).flags = cf & !WHITE_BITS;
1185                self.gray.push(ch);
1186            }
1187        }
1188    }
1189
1190    /// Backward write barrier for objects with many fields (tables, threads):
1191    /// demote the parent itself back to gray so propagate re-traces it.
1192    /// Mirrors PUC `luaC_barrierback_`. One call covers any number of
1193    /// subsequent stores until the next propagate finishes — much cheaper for
1194    /// tables than per-child forward barriers. No-op outside Propagate.
1195    #[allow(clippy::not_unsafe_ptr_arg_deref)] // Internal GC barrier; caller (Gc<T>::write_*) guarantees ptr validity per SAFETY below.
1196    pub fn barrier_back(&mut self, parent: *mut GcHeader) {
1197        // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1198        unsafe {
1199            let f = (*parent).flags;
1200            if !is_black(f) {
1201                return;
1202            }
1203            (*parent).flags = f & !COLOR_BITS;
1204            self.gray.push(parent);
1205        }
1206    }
1207
1208    /// Move every registered finalizable that is now unreachable to `tobefnz`
1209    /// and resurrect it (mark it via `m`) so it — and the data its `__gc` needs
1210    /// — survives this cycle. Survivors stay registered in `finalize`. PUC's
1211    /// `separatetobefnz` walks `g->finobj` head-first, but `g->finobj` is a
1212    /// linked list that registration *prepends* to — so dead objects end up
1213    /// in `tobefnz` in reverse registration order, and `__gc` ultimately
1214    /// runs LIFO. luna's `finalize` is a Vec that grows forward, so iterate
1215    /// it in reverse here to match the LIFO contract (gc.lua's userdata
1216    /// section asserts the finalizers fire from value 10 back to 0).
1217    fn separate_finalizables(&mut self, m: &mut Marker) {
1218        let mut i = self.finalize.len();
1219        while i > 0 {
1220            i -= 1;
1221            let h = self.finalize[i];
1222            // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1223            if unsafe { is_white((*h).flags) } {
1224                // Two-pass cycle-finalize (PUC 5.3 gc.lua :502): when a
1225                // finalizable table holds onto an unreachable coroutine, the
1226                // cycle (table → coroutine.stack → closure → table) keeps the
1227                // mark phase from reaching the table even though it is still
1228                // logically alive for one more GC pass. PUC's mark-sweep wakes
1229                // it via `markbeingfnz` *after* sweeping, so the actual `__gc`
1230                // call lands one cycle later. luna mirrors this by resurrecting
1231                // the table on the first sighting and only enqueuing it for
1232                // `__gc` on the second.
1233                let in_thread_cycle = self.defer_thread_cycle_finalize
1234                    // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1235                    && unsafe { (*h).tag } == ObjTag::Table
1236                    && {
1237                        let t = h as *mut Table;
1238                        // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1239                        unsafe { (*t).refs_contain_unmarked_coro() }
1240                    };
1241                // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1242                let already_deferred = unsafe { (*h).flags & DEFERRED != 0 };
1243                if in_thread_cycle && !already_deferred {
1244                    // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1245                    unsafe { (*h).flags |= DEFERRED };
1246                    m.header(h);
1247                    continue;
1248                }
1249                // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1250                unsafe { (*h).flags = ((*h).flags & !(FIN | DEFERRED)) | FINALIZED };
1251                self.tobefnz.push(h);
1252                m.header(h);
1253                self.finalize.swap_remove(i);
1254            } else {
1255                // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1256                unsafe { (*h).flags &= !DEFERRED };
1257            }
1258        }
1259    }
1260
1261    /// Register a table for finalization (a live `__gc` metamethod was just set
1262    /// via setmetatable). No-op if it is already pending a finalize (FIN bit).
1263    /// PUC 5.5 reference manual §2.5.3: "An object can be marked again for
1264    /// finalization by calling setmetatable with a different metatable, or
1265    /// with the same metatable but with a different __gc field" — so a
1266    /// previously finalized object (FINALIZED bit set then reset by
1267    /// `take_tobefnz`) can re-register. PUC's `luaC_checkfinalizer` is gated
1268    /// on `tofinalize(o)` only, which mirrors checking the FIN bit.
1269    pub(crate) fn register_finalizable(&mut self, t: Gc<Table>) {
1270        let h = t.as_ptr() as *mut GcHeader;
1271        // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1272        unsafe {
1273            if (*h).flags & FIN == 0 {
1274                (*h).flags |= FIN;
1275                self.finalize.push(h);
1276            }
1277        }
1278    }
1279
1280    /// Register a userdata for finalization. PUC 5.1 `newproxy(true)` plus a
1281    /// metatable carrying `__gc` lets a Lua script attach a finalizer to a
1282    /// proxy object — gc.lua's "testing userdata" section binds this
1283    /// behaviour together with weak tables.
1284    pub(crate) fn register_finalizable_userdata(&mut self, u: Gc<crate::runtime::Userdata>) {
1285        let h = u.as_ptr() as *mut GcHeader;
1286        // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1287        unsafe {
1288            if (*h).flags & FIN == 0 {
1289                (*h).flags |= FIN;
1290                self.finalize.push(h);
1291            }
1292        }
1293    }
1294
1295    /// Every userdata still awaiting finalization, registered or already
1296    /// queued. The io library uses it to reach all open files the way C's
1297    /// `fflush(NULL)` and `exit` reach every `FILE*`.
1298    pub(crate) fn finalizable_userdata(&self) -> Vec<Gc<crate::runtime::Userdata>> {
1299        self.finalize
1300            .iter()
1301            .chain(self.tobefnz.iter())
1302            // SAFETY: both lists hold GcHeader pointers of live objects registered for finalization (heap.rs:5-7); a finalizable object is not freed before its finalizer runs.
1303            .filter(|&&h| unsafe { (*h).tag } == ObjTag::Userdata)
1304            .map(|&h| Gc::from_ptr(h as *mut crate::runtime::Userdata))
1305            .collect()
1306    }
1307
1308    /// Take the objects awaiting their `__gc` call (the VM runs the
1309    /// finalizers). Each entry is dispatched on its `ObjTag` so the caller
1310    /// can look up `__gc` for either a table or a proxy userdata.
1311    ///
1312    /// Mirrors PUC 5.5 `udata2finalize`: the FINALIZED bit is reset on the
1313    /// way out so a `setmetatable(obj, mt_with___gc)` inside (or after) the
1314    /// finalizer can re-register the object for a future round, per the Lua
1315    /// 5.5 reference manual §2.5.3 re-finalize semantics.
1316    pub(crate) fn take_tobefnz(&mut self) -> Vec<crate::runtime::Value> {
1317        use crate::runtime::Value;
1318        std::mem::take(&mut self.tobefnz)
1319            .into_iter()
1320            // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1321            .map(|h| unsafe {
1322                (*h).flags &= !FINALIZED;
1323                match (*h).tag {
1324                    ObjTag::Table => Value::Table(Gc::from_ptr(h as *mut Table)),
1325                    ObjTag::Userdata => {
1326                        Value::Userdata(Gc::from_ptr(h as *mut crate::runtime::Userdata))
1327                    }
1328                    _ => unreachable!("non-finalizable object queued for finalization"),
1329                }
1330            })
1331            .collect()
1332    }
1333
1334    /// Move ALL still-registered finalizables to the pending queue regardless of
1335    /// reachability (PUC separatetobefnz(g, 1) at state close), so the VM can run
1336    /// every `__gc` before the heap is torn down.
1337    pub(crate) fn queue_all_finalizers(&mut self) {
1338        for h in std::mem::take(&mut self.finalize) {
1339            // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1340            unsafe { (*h).flags = ((*h).flags & !FIN) | FINALIZED };
1341            self.tobefnz.push(h);
1342        }
1343    }
1344
1345    /// Sweep the whole `all` list in one pass: free dead-white objects,
1346    /// transition survivors (BLACK or current-white) to the new current-white
1347    /// so the next cycle can re-mark them. Returns the number of objects freed.
1348    fn full_sweep(&mut self) -> usize {
1349        // detach the list first so freeing (which needs &mut self for the
1350        // string table) never aliases a pointer into self
1351        let mut freed = 0;
1352        let new_white = self.current_white;
1353        // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1354        unsafe {
1355            let mut cur = std::mem::replace(&mut self.all, ptr::null_mut());
1356            let mut kept_head: *mut GcHeader = ptr::null_mut();
1357            let mut kept_tail: *mut GcHeader = ptr::null_mut();
1358            while !cur.is_null() {
1359                let next = (*cur).next;
1360                let f = (*cur).flags;
1361                // dead = other-white (i.e. white but not current-white).
1362                // Survivors are BLACK (just-marked) or current-white (born
1363                // during the sweep itself).
1364                let dead = is_white(f) && (f & new_white) == 0;
1365                if !dead {
1366                    (*cur).flags = (f & !COLOR_BITS) | new_white;
1367                    (*cur).next = ptr::null_mut();
1368                    if kept_tail.is_null() {
1369                        kept_head = cur;
1370                    } else {
1371                        (*kept_tail).next = cur;
1372                    }
1373                    kept_tail = cur;
1374                } else {
1375                    self.free_obj(cur);
1376                    freed += 1;
1377                }
1378                cur = next;
1379            }
1380            self.all = kept_head;
1381        }
1382        self.live -= freed;
1383        #[cfg(feature = "gc-verify")]
1384        self.verify_no_dangling("full_sweep");
1385        freed
1386    }
1387
1388    /// Sweep up to `budget` objects from the detached `sweep_cur` list: free
1389    /// unmarked ones, splice marked survivors back onto `all` (clearing their
1390    /// MARK bit). Returns true once the list is exhausted (cycle complete →
1391    /// back to `Pause`). Safe with no write barrier: marking was atomic, so any
1392    /// object still unmarked here was unreachable at mark time and the mutator
1393    /// holds no reference that could have resurrected it.
1394    pub(crate) fn gc_sweep_step(&mut self, budget: usize) -> bool {
1395        let mut n = 0;
1396        let new_white = self.current_white;
1397        // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1398        unsafe {
1399            while n < budget && !self.sweep_cur.is_null() {
1400                let cur = self.sweep_cur;
1401                let next = (*cur).next;
1402                let f = (*cur).flags;
1403                let dead = is_white(f) && (f & new_white) == 0;
1404                if !dead {
1405                    (*cur).flags = (f & !COLOR_BITS) | new_white;
1406                    (*cur).next = self.all;
1407                    self.all = cur;
1408                } else {
1409                    self.free_obj(cur);
1410                    self.live -= 1;
1411                }
1412                self.sweep_cur = next;
1413                n += 1;
1414            }
1415        }
1416        // Verify after EVERY step, not just cycle completion: the
1417        // mutator runs between steps, so a reference dangling mid-cycle
1418        // is already a live bug.
1419        #[cfg(feature = "gc-verify")]
1420        self.verify_no_dangling("sweep_step");
1421        if self.sweep_cur.is_null() {
1422            self.phase = GcPhase::Pause;
1423            true
1424        } else {
1425            false
1426        }
1427    }
1428
1429    /// `gc-verify` — post-sweep dangling-reference check
1430    /// (PUC `lua_checkmemory` analogue). Builds the live set from the
1431    /// `all` list + finalizer queues, then walks every live table's
1432    /// collectable refs via [`Table::verify_refs`]. Panics with table
1433    /// pointer + slot detail on the first reference whose target is no
1434    /// longer live — i.e. at the collect that CREATED the dangling
1435    /// pointer, not at the later allocator-dependent dereference.
1436    #[cfg(feature = "gc-verify")]
1437    pub fn verify_no_dangling(&self, ctx: &str) {
1438        use std::collections::HashSet;
1439        fn value_header(v: Value) -> Option<*mut GcHeader> {
1440            match v {
1441                Value::Str(s) => Some(s.as_ptr() as *mut GcHeader),
1442                Value::Table(t) => Some(t.as_ptr() as *mut GcHeader),
1443                Value::Closure(c) => Some(c.as_ptr() as *mut GcHeader),
1444                Value::Native(n) => Some(n.as_ptr() as *mut GcHeader),
1445                Value::Coro(c) => Some(c.as_ptr() as *mut GcHeader),
1446                Value::Userdata(u) => Some(u.as_ptr() as *mut GcHeader),
1447                _ => None,
1448            }
1449        }
1450        let mut live: HashSet<usize> = HashSet::new();
1451        // SAFETY: `all` / `sweep_cur` / finalizer-queue pointers are the
1452        // heap's own intrusive lists; every element is a live allocation
1453        // until free_obj unlinks it.
1454        unsafe {
1455            let mut cur = self.all;
1456            while !cur.is_null() {
1457                live.insert(cur as usize);
1458                cur = (*cur).next;
1459            }
1460            let mut cur = self.sweep_cur;
1461            while !cur.is_null() {
1462                live.insert(cur as usize);
1463                cur = (*cur).next;
1464            }
1465            for &h in &self.finalize {
1466                live.insert(h as usize);
1467            }
1468            for &h in &self.tobefnz {
1469                live.insert(h as usize);
1470            }
1471            let is_live = |v: Value| -> bool {
1472                match value_header(v) {
1473                    None => true,
1474                    Some(h) => live.contains(&(h as usize)),
1475                }
1476            };
1477            // Walk BOTH lists: `all` (already-swept survivors) and
1478            // `sweep_cur` (not-yet-swept remainder of an incremental
1479            // cycle). A dangling reference can live in either while the
1480            // mutator runs between sweep steps.
1481            let new_white = self.current_white;
1482            for head in [self.all, self.sweep_cur] {
1483                let mut cur = head;
1484                while !cur.is_null() {
1485                    // Skip dead-but-not-yet-swept objects on `sweep_cur`:
1486                    // they are unreachable, so a dangling ref inside them
1487                    // is benign (their peers may already be freed).
1488                    let f = (*cur).flags;
1489                    if is_white(f) && (f & new_white) == 0 {
1490                        cur = (*cur).next;
1491                        continue;
1492                    }
1493                    if (*cur).tag == ObjTag::Table {
1494                        let t = &*(cur as *const Table);
1495                        let tp = cur as usize;
1496                        t.verify_refs(&is_live, &|what, idx, v| {
1497                            // Do NOT Debug-format `v` — for Value::Str that
1498                            // would dereference the (dangling) payload.
1499                            let p = value_header(v).map(|h| h as usize).unwrap_or(0);
1500                            panic!(
1501                                "[gc-verify] {ctx}: table {tp:#x} holds dangling {what} \
1502                                 (slot {idx}, target {p:#x})"
1503                            );
1504                        });
1505                    }
1506                    cur = (*cur).next;
1507                }
1508            }
1509        }
1510    }
1511
1512    unsafe fn free_obj(&mut self, h: *mut GcHeader) {
1513        #[cfg(feature = "gc-verify")]
1514        {
1515            self.recently_freed.insert(h as usize);
1516            crate::runtime::gc_verify_probe::FREED.with(|f| f.borrow_mut().insert(h as usize));
1517        }
1518        // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1519        unsafe {
1520            match (*h).tag {
1521                ObjTag::Table => {
1522                    let t = h as *mut Table;
1523                    let internal = (*t).internal_bytes();
1524                    self.bytes = self
1525                        .bytes
1526                        .saturating_sub(std::mem::size_of::<Table>() + internal);
1527                    // pool recycle. Drop the
1528                    // Box-owned interior (slab, nodes, metatable) so the
1529                    // Table struct itself can be re-handed-out by a
1530                    // future `new_table` without re-mallocing. Cap pool
1531                    // at 4096 entries to bound idle memory.
1532                    const TABLE_POOL_CAP: usize = 4096;
1533                    if self.table_pool.len() < TABLE_POOL_CAP {
1534                        // Free interior heap allocations now (slab + nodes).
1535                        // Each Box::new([]) is a dangling no-alloc empty
1536                        // slice, so reassigning is just a pointer move.
1537                        (*t).slab = Box::new([]);
1538                        (*t).nodes = Box::new([]);
1539                        // drop the SoA Robin Hood parallel arrays too
1540                        // (usually Box::new([]) dangling stubs).
1541                        (*t).keys = Box::new([]);
1542                        (*t).vals = Box::new([]);
1543                        (*t).meta = Box::new([]);
1544                        (*t).tombstones = 0;
1545                        (*t).iter_depth = 0;
1546                        (*t).metatable = None;
1547                        // Stash the raw pointer for future reuse.
1548                        // SAFETY: t is non-null (came from a live Gc<Table>);
1549                        // pool owns it until reuse or Heap::Drop.
1550                        self.table_pool.push(std::ptr::NonNull::new_unchecked(t));
1551                    } else {
1552                        drop(Box::from_raw(t));
1553                    }
1554                }
1555                ObjTag::Proto => {
1556                    self.bytes = self.bytes.saturating_sub(std::mem::size_of::<Proto>());
1557                    drop(Box::from_raw(h as *mut Proto));
1558                }
1559                ObjTag::Closure => {
1560                    self.bytes = self.bytes.saturating_sub(std::mem::size_of::<LuaClosure>());
1561                    drop(Box::from_raw(h as *mut LuaClosure));
1562                }
1563                ObjTag::Upvalue => {
1564                    self.bytes = self.bytes.saturating_sub(std::mem::size_of::<Upvalue>());
1565                    drop(Box::from_raw(h as *mut Upvalue));
1566                }
1567                ObjTag::Native => {
1568                    self.bytes = self
1569                        .bytes
1570                        .saturating_sub(std::mem::size_of::<NativeClosure>());
1571                    drop(Box::from_raw(h as *mut NativeClosure));
1572                }
1573                ObjTag::Coro => {
1574                    self.bytes = self
1575                        .bytes
1576                        .saturating_sub(std::mem::size_of::<crate::runtime::Coro>());
1577                    drop(Box::from_raw(h as *mut crate::runtime::Coro));
1578                }
1579                ObjTag::Userdata => {
1580                    self.bytes = self.bytes.saturating_sub(std::mem::size_of::<Userdata>());
1581                    drop(Box::from_raw(h as *mut Userdata));
1582                }
1583                ObjTag::Str => {
1584                    let s = h as *mut LuaStr;
1585                    self.bytes = self.bytes.saturating_sub(string::alloc_size((*s).len()));
1586                    if (*s).is_short() {
1587                        self.strings.remove(s);
1588                    }
1589                    string::free(s);
1590                }
1591            }
1592        }
1593    }
1594}
1595
1596impl Drop for Heap {
1597    fn drop(&mut self) {
1598        // free everything regardless of reachability, including any list still
1599        // detached for an in-flight incremental sweep
1600        // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1601        unsafe {
1602            for mut cur in [self.all, self.sweep_cur] {
1603                while !cur.is_null() {
1604                    let next = (*cur).next;
1605                    self.free_obj(cur);
1606                    cur = next;
1607                }
1608            }
1609            // release the table_pool's
1610            // dangling Box<Table> ptrs. Each was Box::into_raw'd into
1611            // the pool (via free_obj recycle path); without this, the
1612            // Tables would leak. The pool's Tables had their interior
1613            // Box-owned fields (slab/nodes/metatable) already cleared
1614            // when they were recycled, so dropping the Table now only
1615            // releases the Table struct itself.
1616            for ptr in self.table_pool.drain(..) {
1617                drop(Box::from_raw(ptr.as_ptr()));
1618            }
1619        }
1620    }
1621}
1622
1623impl Default for Heap {
1624    fn default() -> Heap {
1625        Heap::new()
1626    }
1627}
1628
1629/// Mark accumulator: gray stack plus entry points for Values and bare
1630/// object headers (Protos/Upvalues are not first-class Values).
1631pub(crate) struct Marker {
1632    stack: Vec<*mut GcHeader>,
1633    /// live tables with a weak `__mode`, collected during marking and processed
1634    /// (dead weak entries cleared) before the sweep
1635    pub(crate) weak: Vec<*mut Table>,
1636    /// ephemeron tables (weak keys, strong values): their hash values are not
1637    /// marked during trace but in a fixpoint pass keyed on key-reachability
1638    pub(crate) ephemeron: Vec<*mut Table>,
1639    /// PUC 5.1 mode: skip ephemeron handling — `__mode='k'` tables mark their
1640    /// values strongly during the normal trace pass (see [`Heap::no_ephemeron`]).
1641    pub(crate) no_ephemeron: bool,
1642    /// Protos with a non-null closure cache (PUC `Proto.cache`). After
1643    /// marking is done, any cached LClosure that ended the cycle unmarked is
1644    /// cleared so the sweep can collect it — the cache is a *weak* reference
1645    /// (PUC `traverseproto` checks `iswhite(cache)`). Seen via [`Proto::trace`].
1646    pub(crate) cached_protos: Vec<*mut crate::runtime::Proto>,
1647}
1648
1649/// Drain the gray stack: pop each marked object and trace its children until
1650/// the worklist is empty (iterative, so deep graphs don't overflow the Rust
1651/// stack). Shared by the root mark and the post-resurrection remark.
1652fn drain_marker(m: &mut Marker) {
1653    while let Some(h) = m.stack.pop() {
1654        // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1655        unsafe {
1656            // PUC `propagatemark`: gray → black before scanning children, so a
1657            // child that points back at us (cycle) re-traces us as already
1658            // black and does not loop. White bits were cleared on push.
1659            (*h).flags = ((*h).flags & !WHITE_BITS) | BLACK;
1660            match (*h).tag {
1661                ObjTag::Str => {}
1662                ObjTag::Table => (*(h as *mut Table)).trace(m),
1663                ObjTag::Proto => (*(h as *mut Proto)).trace(m),
1664                ObjTag::Closure => (*(h as *mut LuaClosure)).trace(m),
1665                ObjTag::Upvalue => (*(h as *mut Upvalue)).trace(m),
1666                ObjTag::Native => (*(h as *mut NativeClosure)).trace(m),
1667                ObjTag::Coro => (*(h as *mut crate::runtime::Coro)).trace(m),
1668                ObjTag::Userdata => (*(h as *mut Userdata)).trace(m),
1669            }
1670        }
1671    }
1672}
1673
1674impl Marker {
1675    /// Mark a value, returning true if it was newly marked (was white).
1676    pub(crate) fn value(&mut self, v: Value) -> bool {
1677        let h = match v {
1678            Value::Str(s) => s.as_ptr() as *mut GcHeader,
1679            Value::Table(t) => t.as_ptr() as *mut GcHeader,
1680            Value::Closure(c) => c.as_ptr() as *mut GcHeader,
1681            Value::Native(n) => n.as_ptr() as *mut GcHeader,
1682            Value::Coro(c) => c.as_ptr() as *mut GcHeader,
1683            Value::Userdata(u) => u.as_ptr() as *mut GcHeader,
1684            _ => return false,
1685        };
1686        self.header(h)
1687    }
1688
1689    /// Mark a bare header, returning true if it was newly marked (was white).
1690    /// Transitions white → gray (in PUC `reallymarkobject` terms): clears the
1691    /// current-white bit and pushes onto the gray stack. `drain_marker` later
1692    /// pops it, traces children, and stamps it BLACK.
1693    pub(crate) fn header(&mut self, h: *mut GcHeader) -> bool {
1694        // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1695        unsafe {
1696            let f = (*h).flags;
1697            if is_white(f) {
1698                (*h).flags = f & !WHITE_BITS;
1699                self.stack.push(h);
1700                true
1701            } else {
1702                false
1703            }
1704        }
1705    }
1706}
1707
1708/// Whether a value is "alive" for ephemeron key purposes: non-collectable
1709/// values and strings are always alive (strings are never weakly cleared);
1710/// a collectable object is alive only once marked (gray or black).
1711fn weak_key_alive(v: Value) -> bool {
1712    let h = match v {
1713        Value::Table(t) => t.as_ptr() as *mut GcHeader,
1714        Value::Closure(c) => c.as_ptr() as *mut GcHeader,
1715        Value::Native(n) => n.as_ptr() as *mut GcHeader,
1716        Value::Coro(c) => c.as_ptr() as *mut GcHeader,
1717        Value::Userdata(u) => u.as_ptr() as *mut GcHeader,
1718        _ => return true, // strings, numbers, booleans: never weak-collected
1719    };
1720    // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1721    unsafe { !is_white((*h).flags) }
1722}
1723
1724/// Hash seed from address entropy (ASLR) and clock, luaL_makeseed style.
1725fn make_seed() -> u32 {
1726    let stack_var = 0u8;
1727    let mut h = &stack_var as *const u8 as u64;
1728    h ^= (make_seed as *const () as u64) << 16;
1729    if let Ok(d) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
1730        h ^= (d.subsec_nanos() as u64) << 32 ^ d.as_secs();
1731    }
1732    h ^= h >> 33;
1733    h = h.wrapping_mul(0xff51_afd7_ed55_8ccd);
1734    h ^= h >> 33;
1735    h as u32
1736}
1737
1738#[cfg(test)]
1739mod tests {
1740    use super::*;
1741    use crate::vm::isa::{Inst, Op};
1742
1743    #[test]
1744    fn collect_traces_function_objects() {
1745        let mut heap = Heap::new();
1746        let source = heap.intern(b"@test");
1747        let kstr = heap.intern(b"a-constant-string");
1748        let inner = Proto {
1749            hdr: GcHeader::new(ObjTag::Proto),
1750            code: Box::new([Inst::iabc(Op::Return0, 0, 0, 0, false)]),
1751            consts: Box::new([]),
1752            protos: Box::new([]),
1753            upvals: Box::new([]),
1754            num_params: 0,
1755            is_vararg: false,
1756            has_vararg_table_pseudo: false,
1757            has_compat_vararg_arg: false,
1758            max_stack: 2,
1759            lines: Box::new([1]),
1760            source,
1761            line_defined: 1,
1762            last_line_defined: 1,
1763            locvars: Box::new([]),
1764            cache: std::cell::Cell::new(None),
1765            jit: std::cell::Cell::new(crate::runtime::function::JitProtoState::Untried),
1766            env_upval_idx: u8::MAX,
1767            trace_hot_count: std::cell::Cell::new(0),
1768            call_hot_count: std::cell::Cell::new(0),
1769            trace_discard_count: std::cell::Cell::new(0),
1770            trace_gave_up: std::cell::Cell::new(false),
1771            trace_compile_failures: crate::jit::send_compat::TRefLock::new(Vec::new()),
1772            traces: crate::jit::send_compat::TRefLock::new(Vec::new()),
1773        };
1774        let inner = heap.adopt_proto(inner);
1775        let outer = Proto {
1776            hdr: GcHeader::new(ObjTag::Proto),
1777            code: Box::new([Inst::iabc(Op::Return0, 0, 0, 0, false)]),
1778            consts: Box::new([Value::Str(kstr)]),
1779            protos: Box::new([inner]),
1780            upvals: Box::new([]),
1781            num_params: 0,
1782            is_vararg: true,
1783            has_vararg_table_pseudo: false,
1784            has_compat_vararg_arg: false,
1785            max_stack: 2,
1786            lines: Box::new([1]),
1787            source,
1788            line_defined: 0,
1789            last_line_defined: 0,
1790            locvars: Box::new([]),
1791            cache: std::cell::Cell::new(None),
1792            jit: std::cell::Cell::new(crate::runtime::function::JitProtoState::Untried),
1793            env_upval_idx: u8::MAX,
1794            trace_hot_count: std::cell::Cell::new(0),
1795            call_hot_count: std::cell::Cell::new(0),
1796            trace_discard_count: std::cell::Cell::new(0),
1797            trace_gave_up: std::cell::Cell::new(false),
1798            trace_compile_failures: crate::jit::send_compat::TRefLock::new(Vec::new()),
1799            traces: crate::jit::send_compat::TRefLock::new(Vec::new()),
1800        };
1801        let outer = heap.adopt_proto(outer);
1802        let captured = heap.intern(b"captured-value-string-xxxxxxxxxxxxxxxxxxxxxxxxx");
1803        let uv = heap.new_upvalue(UpvalState::Closed(Value::Str(captured)));
1804        let cl = heap.new_closure(outer, Box::new([uv]));
1805        // objects: source, kstr, inner, outer, captured, uv, cl
1806        assert_eq!(heap.live_objects(), 7);
1807        // rooting the closure keeps the whole graph alive
1808        assert_eq!(heap.collect(&[Value::Closure(cl)]), 0);
1809        assert_eq!(heap.live_objects(), 7);
1810        assert_eq!(heap.collect(&[]), 7);
1811        assert_eq!(heap.live_objects(), 0);
1812    }
1813
1814    #[test]
1815    fn collect_unreachable() {
1816        let mut heap = Heap::new();
1817        let s = heap.intern(b"hello");
1818        let t = heap.new_table();
1819        assert_eq!(heap.live_objects(), 2);
1820        // both rooted: nothing freed
1821        assert_eq!(heap.collect(&[Value::Str(s), Value::Table(t)]), 0);
1822        // only table rooted: string freed
1823        assert_eq!(heap.collect(&[Value::Table(t)]), 1);
1824        assert_eq!(heap.live_objects(), 1);
1825        // nothing rooted
1826        assert_eq!(heap.collect(&[]), 1);
1827        assert_eq!(heap.live_objects(), 0);
1828    }
1829
1830    #[test]
1831    fn collect_traces_table_contents() {
1832        let mut heap = Heap::new();
1833        let t = heap.new_table();
1834        let k = heap.intern(b"key-string-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); // long
1835        let v = heap.intern(b"val");
1836        unsafe { t.as_mut() }
1837            .set(&mut heap, Value::Str(k), Value::Str(v))
1838            .unwrap();
1839        let inner = heap.new_table();
1840        unsafe { t.as_mut() }
1841            .set(&mut heap, Value::Int(1), Value::Table(inner))
1842            .unwrap();
1843        unsafe { inner.as_mut() }.set_metatable(Some(t));
1844        assert_eq!(heap.live_objects(), 4);
1845        // root only the outer table: everything reachable through it survives
1846        assert_eq!(heap.collect(&[Value::Table(t)]), 0);
1847        assert_eq!(heap.live_objects(), 4);
1848        assert_eq!(heap.collect(&[]), 4);
1849    }
1850
1851    #[test]
1852    fn interned_string_reclaimed_and_reinternable() {
1853        let mut heap = Heap::new();
1854        heap.intern(b"transient");
1855        assert_eq!(heap.collect(&[]), 1);
1856        let s2 = heap.intern(b"transient");
1857        assert_eq!(s2.as_bytes(), b"transient");
1858        assert_eq!(heap.live_objects(), 1);
1859    }
1860
1861    #[test]
1862    fn bytes_and_live_round_trip_to_zero() {
1863        // Memory-invariant audit: after a churn of table allocation, rehash-
1864        // driven growth, and full collection of an empty root set, both
1865        // `heap.bytes` and `heap.live_objects` must return to 0. Catches any
1866        // alloc / free asymmetry in the Table internal-Box delta tracking
1867        // or the live counter (link/sweep symmetry).
1868        let mut heap = Heap::new();
1869        assert_eq!(heap.bytes(), 0);
1870        assert_eq!(heap.live_objects(), 0);
1871        // Build a churn: 50 tables, each filled with 200 int keys (forces
1872        // multiple rehashes); plus interned strings spliced through the
1873        // hash part. Bytes should grow well past the empty baseline.
1874        let mut roots: Vec<Value> = Vec::new();
1875        for ti in 0..50 {
1876            let t = heap.new_table();
1877            for k in 1..=200 {
1878                let _ =
1879                    unsafe { t.as_mut() }.set(&mut heap, Value::Int(k), Value::Int(ti * 1000 + k));
1880            }
1881            for sk in 0..32 {
1882                let key = Value::Str(heap.intern(format!("k{ti}-{sk}").as_bytes()));
1883                let _ = unsafe { t.as_mut() }.set(&mut heap, key, Value::Int(sk));
1884            }
1885            roots.push(Value::Table(t));
1886        }
1887        let live_peak = heap.live_objects();
1888        let bytes_peak = heap.bytes();
1889        assert!(live_peak > 0, "live should be >0 after churn");
1890        assert!(bytes_peak > 0, "bytes should be >0 after churn");
1891        // Root only half — the other half should be collected.
1892        let half = roots.len() / 2;
1893        let freed = heap.collect(&roots[..half]);
1894        assert!(freed > 0, "some objects should have been freed");
1895        assert!(
1896            heap.bytes() < bytes_peak,
1897            "bytes must drop after partial collect"
1898        );
1899        assert!(
1900            heap.live_objects() < live_peak,
1901            "live must drop after partial collect"
1902        );
1903        // Drop everything: counters must return to 0 exactly.
1904        drop(roots);
1905        let _ = heap.collect(&[]);
1906        assert_eq!(heap.live_objects(), 0, "live not zero after full collect");
1907        assert_eq!(
1908            heap.bytes(),
1909            0,
1910            "bytes not zero after full collect — asymmetric alloc/free"
1911        );
1912    }
1913
1914    /// Regression test for a string-table use-after-free:
1915    /// `StringTable::intern` must NOT return a dead-white (about-to-be-swept)
1916    /// short-string pointer. Mirrors PUC `luaS_new`'s resurrect-on-hit guard
1917    /// (lstring.c — `if (isdead(g, ts)) changewhite(ts);`).
1918    ///
1919    /// Without the guard, the bucket-chain still references the unswept
1920    /// short string after the atomic flip; a re-`intern` of the same bytes
1921    /// hands back that pointer; the budget-paced sweep then frees it and
1922    /// the next bucket walk dereferences libc-recycled garbage (the
1923    /// `0x800002a80000002d` misaligned pointer, for example).
1924    #[test]
1925    fn intern_resurrects_dead_white_short_string() {
1926        let mut heap = Heap::new();
1927        let alive = heap.intern(b"keep-me-alive-1");
1928        let dying = heap.intern(b"transient-x");
1929        let dying_ptr = dying.as_ptr();
1930        let dying_bytes = dying.as_bytes().to_vec();
1931        // Drive an incremental cycle by hand to reproduce the race:
1932        //   1. mark-propagate with `alive` only as a root → `dying` stays white
1933        //   2. atomic flip → `dying` becomes dead-white, bucket still points at it
1934        //   3. RE-INTERN the same bytes BEFORE sweep clears the bucket
1935        //   4. fix must either (a) skip the dead entry & alloc fresh, or
1936        //      (b) resurrect dying back to current-white
1937        let alive_root = [Value::Str(alive)];
1938        heap.gc_start_propagate(&alive_root, &[]);
1939        while !heap.gc_step_propagate(usize::MAX) {}
1940        heap.gc_finish_atomic();
1941        // At this point sweep_cur holds the detached old-heap list and the
1942        // dying string is dead-white. The bucket chain in `self.strings`
1943        // still references it.
1944        let resurrected = heap.intern(&dying_bytes);
1945        // Two valid outcomes:
1946        //   * resurrect: same pointer, but flagged current-white so sweep
1947        //     will keep it alive (PUC luaS_new shape)
1948        //   * skip-and-alloc-fresh: different pointer, dying gets swept
1949        //     normally as it should
1950        // Either way, after completing the sweep the heap must NOT crash
1951        // when we try to intern more short strings (which walks bucket chains).
1952        while !heap.gc_sweep_step(usize::MAX) {}
1953        // Smoke: bucket-chain walk for a fresh string must not deref a
1954        // freed pointer.
1955        let _fresh = heap.intern(b"after-sweep-canary");
1956        // If the fix is "resurrect", same pointer + bytes preserved:
1957        if resurrected.as_ptr() == dying_ptr {
1958            assert_eq!(resurrected.as_bytes(), dying_bytes.as_slice());
1959        }
1960        // Final cleanup: full collect must complete without UAF.
1961        drop(heap);
1962    }
1963
1964    #[test]
1965    fn deep_table_chain_marks_iteratively() {
1966        // deep chain: explicit mark stack must not overflow (smaller under
1967        // miri — the interpreter makes 100k tables take ~30 minutes)
1968        let n = if cfg!(miri) { 2_000 } else { 100_000 };
1969        let mut heap = Heap::new();
1970        let head = heap.new_table();
1971        let mut cur = head;
1972        for _ in 0..n {
1973            let next = heap.new_table();
1974            unsafe { cur.as_mut() }
1975                .set(&mut heap, Value::Int(1), Value::Table(next))
1976                .unwrap();
1977            cur = next;
1978        }
1979        assert_eq!(heap.collect(&[Value::Table(head)]), 0);
1980        assert_eq!(heap.collect(&[]), n + 1);
1981    }
1982}