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)]` (Track A4 — pub-surface 0 unsafe): embedders should
143    /// not see this in rustdoc. The safe path for mutating freshly-allocated
144    /// tables is the `TableBuilder` / `vm.table_of(...)` API (Track B3).
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    /// P17-D v2 layer-15 attack: 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    /// v2.13 WUC `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    /// P09 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        // P17-D v2 layer-15 attack — 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        // P11-S5d.I — 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    /// P11-S5c.B — 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    /// P11-S5d.M — 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    /// P11-S5d.M — 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    /// v1.1 B10 Stage 2 — 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            stack: Vec::new(),
551            frames: Vec::new(),
552            open_upvals: Vec::new(),
553            tbc: Vec::new(),
554            top: 0,
555            pcall_depth: 0,
556            hook: crate::vm::exec::HookState::default(),
557            globals,
558        }))
559    }
560
561    /// Create a userdata (an io file handle — luna's only userdata) with no
562    /// metatable yet; the io library installs the shared `FILE*` metatable.
563    pub fn new_userdata(&mut self, payload: UserdataPayload, writable: bool) -> Gc<Userdata> {
564        self.adopt(Box::new(Userdata::new(
565            GcHeader::new(ObjTag::Userdata),
566            payload,
567            writable,
568        )))
569    }
570
571    /// Create (or find) a string. Short strings (≤ 40 bytes) are interned.
572    pub fn intern(&mut self, bytes: &[u8]) -> Gc<LuaStr> {
573        if bytes.len() <= string::MAX_SHORT_LEN {
574            let (p, is_new) = self.strings.intern(bytes, self.seed);
575            if is_new {
576                self.link(p as *mut GcHeader);
577                self.bytes += string::alloc_size(bytes.len());
578            } else {
579                // PUC `luaS_new` resurrect guard (lstring.c).
580                // The bucket-chain is walked open-loop without consulting GC
581                // color; during incremental sweep an existing entry may be
582                // dead-white (in `sweep_cur`, scheduled for `free_obj`). If we
583                // hand its pointer back, the budget-paced sweep frees it out
584                // from under the mutator and the next bucket walk dereferences
585                // a libc-recycled slot — the symptom recorded in
586                // `.dev/known-bugs/stringtable-intern-uaf.md` (misaligned ptr
587                // `0x800002a80000002d` deep in `StringTable::intern`).
588                //
589                // Flip the white bits to `current_white` so the upcoming sweep
590                // skips it (PUC `changewhite`). Black / not-white objects are
591                // already safe and untouched.
592                // SAFETY: `p` came from `StringTable::intern` and is a valid
593                // `LuaStr` header (its bucket chain is consistent under our
594                // single-threaded heap).
595                unsafe {
596                    let f = (*(p as *mut GcHeader)).flags;
597                    if is_white(f) && (f & self.current_white) == 0 {
598                        (*(p as *mut GcHeader)).flags = (f & !WHITE_BITS) | self.current_white;
599                    }
600                }
601            }
602            Gc::from_ptr(p)
603        } else {
604            let p = string::alloc_long(bytes, self.seed);
605            self.link(p as *mut GcHeader);
606            self.bytes += string::alloc_size(bytes.len());
607            Gc::from_ptr(p)
608        }
609    }
610
611    /// Number of GC-managed objects currently linked into the heap (live + not
612    /// yet swept). Useful for `collectgarbage("count")`-style introspection.
613    pub fn live_objects(&self) -> usize {
614        self.live
615    }
616
617    /// Approximate heap size in bytes.
618    pub fn bytes(&self) -> usize {
619        self.bytes
620    }
621
622    /// v2.0 Track TL — pure-read walk over the intrusive `all`
623    /// objects list, invoking `visit(tag)` once per live (or
624    /// not-yet-swept) GC-managed object. Used by `luna-tools`'s
625    /// `luna-heap-dump` to build a per-type histogram; embedders
626    /// can reuse it for ad-hoc heap introspection.
627    ///
628    /// # Read-only contract
629    ///
630    /// The callback receives only the [`ObjTag`] discriminant and
631    /// is invoked under a `&self` borrow on the heap: no pointer
632    /// to the GC payload escapes, no `as_mut`-style aliasing is
633    /// available, and the walk performs zero allocation in the
634    /// loop. Safe to call between dispatch ticks (the only allocs
635    /// happen in the caller's bookkeeping).
636    ///
637    /// The walk visits both the live `all` list and the
638    /// `sweep_cur` detached list so a mid-cycle invocation reports
639    /// the same total as [`Heap::live_objects`].
640    pub fn walk_objects(&self, mut visit: impl FnMut(ObjTag)) {
641        for head in [self.all, self.sweep_cur] {
642            let mut cur = head;
643            while !cur.is_null() {
644                // SAFETY: pointers come from the runtime's
645                // intrusive all-objects list. `&self` borrow on
646                // the heap prevents concurrent mutation; the GC
647                // cannot run while this walk holds the borrow,
648                // so every `next` link is valid until consumed.
649                let (tag, next) = unsafe { ((*cur).tag, (*cur).next) };
650                visit(tag);
651                cur = next;
652            }
653        }
654    }
655
656    /// Whether allocation has crossed the auto-GC threshold (cheap safe-point
657    /// check for the interpreter loop).
658    #[inline(always)]
659    pub fn gc_due(&self) -> bool {
660        !self.gc_stopped && self.bytes >= self.next_gc
661    }
662
663    /// `collectgarbage("stop"/"restart")`: suspend or resume auto-GC.
664    pub(crate) fn gc_is_stopped(&self) -> bool {
665        self.gc_stopped
666    }
667
668    pub(crate) fn gc_set_stopped(&mut self, stopped: bool) {
669        self.gc_stopped = stopped;
670    }
671
672    /// Re-arm with caller-supplied `pause` (PUC param, % of live bytes). The
673    /// next cycle fires once `bytes >= live * pause / 100`. `pause=200` (PUC
674    /// default) waits for the heap to double; `pause=100` fires immediately
675    /// when alloc resumes; `pause=300` is 3× — lower pause = more aggressive.
676    pub fn rearm_gc_pause(&mut self, pause: i64) {
677        let pause = pause.max(0) as usize;
678        let target = self
679            .bytes
680            .saturating_mul(pause)
681            .saturating_div(100)
682            .max(GC_MIN_THRESHOLD);
683        self.next_gc = target;
684    }
685
686    /// Re-arm the auto-GC threshold after a collection (PUC pause-style: next
687    /// collection once the live set roughly doubles).
688    pub fn rearm_gc(&mut self) {
689        self.next_gc = self.bytes.saturating_mul(2).max(GC_MIN_THRESHOLD);
690    }
691
692    /// Apply a `before → after` box-size delta from a Table mutation
693    /// (`set`/`rehash`/`ensure_*`). Grows credit `Heap.bytes`; shrinks
694    /// debit it. `free_obj` for `ObjTag::Table` then subtracts the table's
695    /// final `internal_bytes()` so the round-trip is symmetric across the
696    /// table's whole lifetime.
697    pub(crate) fn apply_bytes_delta(&mut self, before: usize, after: usize) {
698        if after > before {
699            self.bytes += after - before;
700        } else if before > after {
701            self.bytes = self.bytes.saturating_sub(before - after);
702        }
703    }
704
705    /// Mark from `roots`, sweep everything unreachable. Returns the number of
706    /// objects freed.
707    pub fn collect(&mut self, roots: &[Value]) -> usize {
708        self.collect_ex(roots, &[])
709    }
710
711    /// Like `collect`, with additional bare-object roots (e.g. the VM's open
712    /// upvalues, which are not first-class Values).
713    pub(crate) fn collect_ex(&mut self, roots: &[Value], extra: &[*mut GcHeader]) -> usize {
714        // a full STW collection subsumes any in-flight incremental cycle:
715        // drive it to completion (Propagate → atomic → Sweep → Pause) so `all`
716        // holds the whole heap again with all marks cleared, then run a fresh
717        // STW cycle. Any tobefnz from the finished cycle stays queued and is
718        // re-marked (kept alive) by the upcoming mark_all so the VM's
719        // run_finalizers can still see them.
720        if self.phase == GcPhase::Propagate {
721            self.gc_finish_atomic();
722        }
723        if self.phase == GcPhase::Sweep {
724            self.gc_sweep_step(usize::MAX);
725        }
726        self.mark_all(roots, extra);
727        self.full_sweep()
728    }
729
730    /// Stop-the-world mark from `roots`/`extra`. Builds an ephemeral marker,
731    /// seeds from roots + extra + tobefnz + any barrier-carried gray queue,
732    /// propagates to completion, then runs the atomic tail (weak / ephemeron
733    /// / finalizer resurrection / current-white flip). After return all
734    /// reachable objects are BLACK and `current_white` has flipped, so the
735    /// caller's sweep tests `other-white` for dead. Does NOT change `phase`.
736    fn mark_all(&mut self, roots: &[Value], extra: &[*mut GcHeader]) {
737        let mut m = Marker {
738            stack: Vec::new(),
739            weak: Vec::new(),
740            ephemeron: Vec::new(),
741            no_ephemeron: self.no_ephemeron,
742            cached_protos: Vec::new(),
743        };
744        // Drain any barrier-grayed objects carried over: each was demoted from
745        // BLACK back to gray by a write barrier and is awaiting (re-)trace.
746        m.stack.append(&mut self.gray);
747        for &r in roots {
748            m.value(r);
749        }
750        for &h in extra {
751            m.header(h);
752        }
753        // objects already queued for finalization but not yet run must stay
754        // alive until the VM calls their `__gc` (they may be unreachable now).
755        for &h in &self.tobefnz {
756            m.header(h);
757        }
758        drain_marker(&mut m);
759        // ephemeron convergence: a weak-key entry's value is reachable only if
760        // the key is. Marking a value can make another key reachable, so repeat
761        // until no value is newly marked (PUC convergeephemerons).
762        if !m.ephemeron.is_empty() {
763            loop {
764                let mut changed = false;
765                let eph = m.ephemeron.clone();
766                for t in eph {
767                    // 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).
768                    changed |= unsafe { (*t).converge_ephemeron(&weak_key_alive, &mut m) };
769                }
770                drain_marker(&mut m);
771                if !changed {
772                    break;
773                }
774            }
775        }
776        self.atomic_tail(&mut m);
777    }
778
779    /// PUC `atomic()` tail: weak-table value-clear, finalizer resurrection,
780    /// post-resurrection ephemeron convergence, proto cache, key-clear, late
781    /// value-clear, and current-white flip. Marker is consumed; `weak` is
782    /// empty on return.
783    ///
784    /// Shared between the STW path (`mark_all`) and the incremental path
785    /// (`gc_finish_atomic`). PUC 5.5 `lgc.c::atomic` mirror:
786    ///   propagate → remarkupvals → convergeephemerons
787    ///   → clearbyvalues(weak, NULL)            ─ early value-clear
788    ///   → clearbyvalues(allweak, NULL)         ─ (same pass under luna)
789    ///   → origweak = g->weak                   ─ snapshot pre-resurrection
790    ///   → separatetobefnz(0) + markbeingfnz    ─ separate_finalizables
791    ///   → propagateall + convergeephemerons    ─ post-resurrection
792    ///   → clearbykeys(ephemeron) + clearbykeys(allweak)
793    ///   → clearbyvalues(weak, origweak)        ─ NEW (post-resurrect) only
794    ///   → clearbyvalues(allweak, origall)      ─ (same)
795    /// The `origweak` split matters because finalizer resurrection can
796    /// re-trace fresh proto/closure → new weak tables joining `m.weak`;
797    /// PUC limits the late value-clear to those new heads.
798    fn atomic_tail(&mut self, m: &mut Marker) {
799        let early_is_dead = |v: Value| -> bool {
800            let h = match v {
801                Value::Str(_) => return false,
802                Value::Table(t) => t.as_ptr() as *mut GcHeader,
803                Value::Closure(c) => c.as_ptr() as *mut GcHeader,
804                Value::Native(n) => n.as_ptr() as *mut GcHeader,
805                Value::Coro(c) => c.as_ptr() as *mut GcHeader,
806                Value::Userdata(u) => u.as_ptr() as *mut GcHeader,
807                _ => return false,
808            };
809            // 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).
810            unsafe { is_white((*h).flags) }
811        };
812        let mark_string = |v: Value| {
813            if let Value::Str(s) = v {
814                // 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).
815                unsafe {
816                    let h = s.as_ptr() as *mut GcHeader;
817                    // strings are leaves: skip gray and go straight to black
818                    (*h).flags = ((*h).flags & !COLOR_BITS) | BLACK;
819                }
820            }
821        };
822        // (1) early clearbyvalues — drop dead-value entries from every weak
823        // table on `m.weak` (PUC's combined `clearbyvalues(weak, NULL) +
824        // clearbyvalues(allweak, NULL)`). Keys are deferred to the
825        // post-resurrection sweep below.
826        for t in &m.weak {
827            // 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).
828            unsafe {
829                let (_wk, wv) = (**t).weak_mode();
830                if wv {
831                    (**t).clear_weak(false, true, &early_is_dead, &mark_string);
832                }
833            }
834        }
835        // (2) `origweak` snapshot — PUC takes the list head; luna's `m.weak`
836        // is a Vec, so the equivalent is its length before resurrection.
837        // Anything appended past this index is a "NEW" weak table that the
838        // resurrection pass brought into view.
839        let origweak_n = m.weak.len();
840        // (3) separate + markbeingfnz — resurrect every registered finalizable
841        // that ended up unmarked. `m.header(h)` enqueues each into the marker
842        // so the following drain_marker propagates through it.
843        self.separate_finalizables(m);
844        drain_marker(m);
845        // (4) post-resurrection ephemeron convergence — a resurrected
846        // finalizable may bring new keys into reach, which in turn marks new
847        // ephemeron values.
848        if !m.ephemeron.is_empty() {
849            loop {
850                let mut changed = false;
851                let eph = m.ephemeron.clone();
852                for t in eph {
853                    // 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).
854                    changed |= unsafe { (*t).converge_ephemeron(&weak_key_alive, m) };
855                }
856                drain_marker(m);
857                if !changed {
858                    break;
859                }
860            }
861        }
862        // (5) closure-cache weak refs — PUC `traverseproto` clears
863        // `Proto.cache` when the cached LClosure ended the cycle unmarked.
864        // Without this, an LClosure whose only outstanding reference is the
865        // proto's cache would survive forever and its upvalues' `__gc`
866        // finalisers would never run.
867        for &p in &m.cached_protos {
868            // 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).
869            unsafe {
870                if let Some(c) = (*p).cache.get() {
871                    let h = c.as_ptr() as *mut GcHeader;
872                    if is_white((*h).flags) {
873                        (*p).cache.set(None);
874                    }
875                }
876            }
877        }
878        // (6) clearbykeys — drop entries whose weak key did not survive
879        // marking, across every weak table (PUC's `clearbykeys(ephemeron)
880        // + clearbykeys(allweak)`). Pure key sweep — value-dead entries are
881        // either already nil from step (1) or wait for step (7).
882        for t in &m.weak {
883            // 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).
884            unsafe {
885                let (wk, _wv) = (**t).weak_mode();
886                if wk {
887                    (**t).clear_weak(true, false, &early_is_dead, &mark_string);
888                }
889            }
890        }
891        // (7) late clearbyvalues — PUC's `clearbyvalues(weak, origweak) +
892        // clearbyvalues(allweak, origall)`. Limit the sweep to NEW heads so
893        // we don't redo work already done in step (1) for the pre-resurrect
894        // tables (they were drained by then and re-marking happens through
895        // mark_string in step (6)). resurrected weak tables joining `m.weak`
896        // past `origweak_n` get their first value-clear here.
897        let weak_snapshot = std::mem::take(&mut m.weak);
898        for t in &weak_snapshot[origweak_n..] {
899            // 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).
900            unsafe {
901                let (_wk, wv) = (**t).weak_mode();
902                if wv {
903                    (**t).clear_weak(false, true, &early_is_dead, &mark_string);
904                }
905            }
906        }
907        // PUC 5.5 `atomic` end: flip currentwhite so survivors (presently
908        // BLACK) get transitioned into the *new* current-white during sweep,
909        // and the pre-flip current-white becomes the dead-white (the bit the
910        // sweep tests for). Born-during-sweep allocations stamp the new
911        // current-white via `Heap::link`, so they survive this cycle.
912        self.current_white ^= WHITE_BITS;
913        // v2.13 WUC `gc-verify` — tricolor invariant check at the one
914        // moment it is exact: marking is complete, nothing is freed yet.
915        // A BLACK (surviving) table holding a dead-white child means a
916        // write barrier was missed; the child will be freed by the
917        // upcoming sweep and the table left holding a dangling pointer.
918        // Nothing is dangling *yet*, so the reporter may safely print
919        // string contents to identify the key.
920        #[cfg(feature = "gc-verify")]
921        self.verify_tricolor("atomic_tail");
922    }
923
924    /// v2.13 WUC `gc-verify` — the set of every live object header
925    /// (all + sweep_cur + finalizer queues), for callers that need to
926    /// audit their own containers (e.g. the VM auditing its register
927    /// stack after a collect).
928    #[cfg(feature = "gc-verify")]
929    pub fn debug_live_set(&self) -> std::collections::HashSet<usize> {
930        let mut live = std::collections::HashSet::new();
931        // SAFETY: heap-owned intrusive lists; all elements are live
932        // allocations.
933        unsafe {
934            let mut cur = self.all;
935            while !cur.is_null() {
936                live.insert(cur as usize);
937                cur = (*cur).next;
938            }
939            let mut cur = self.sweep_cur;
940            while !cur.is_null() {
941                live.insert(cur as usize);
942                cur = (*cur).next;
943            }
944        }
945        for &h in &self.finalize {
946            live.insert(h as usize);
947        }
948        for &h in &self.tobefnz {
949            live.insert(h as usize);
950        }
951        live
952    }
953
954    /// See call site in [`Heap::atomic_tail`]. Panics on the first
955    /// BLACK table whose collectable child is dead-white (missed
956    /// barrier — that child is about to be swept while still
957    /// referenced).
958    #[cfg(feature = "gc-verify")]
959    fn verify_tricolor(&self, ctx: &str) {
960        let new_white = self.current_white;
961        // SAFETY: `all` is the heap's own intrusive list; every element
962        // is a live allocation (nothing has been freed this cycle yet).
963        unsafe {
964            let is_live = |v: Value| -> bool {
965                let h = match v {
966                    Value::Str(s) => s.as_ptr() as *mut GcHeader,
967                    Value::Table(t) => t.as_ptr() as *mut GcHeader,
968                    Value::Closure(c) => c.as_ptr() as *mut GcHeader,
969                    Value::Native(n) => n.as_ptr() as *mut GcHeader,
970                    Value::Coro(c) => c.as_ptr() as *mut GcHeader,
971                    Value::Userdata(u) => u.as_ptr() as *mut GcHeader,
972                    _ => return true,
973                };
974                let f = (*h).flags;
975                !(is_white(f) && (f & new_white) == 0)
976            };
977            let mut cur = self.all;
978            while !cur.is_null() {
979                let f = (*cur).flags;
980                if (*cur).tag == ObjTag::Table && (f & BLACK) != 0 {
981                    let t = &*(cur as *const Table);
982                    // Weak tables may legitimately reference dead objects
983                    // between clear passes; they were just cleared above,
984                    // but their sanctioned dead_key nodes are skipped by
985                    // verify_refs anyway. Only strong tables assert.
986                    let (wk, wv) = t.weak_mode();
987                    if wk || wv {
988                        cur = (*cur).next;
989                        continue;
990                    }
991                    let tp = cur as usize;
992                    t.verify_refs(&is_live, &|what, idx, v| {
993                        // Pre-sweep: dereferencing v is still safe — name
994                        // the key when it is a string.
995                        let detail = match v.as_bytes() {
996                            Some(b) => format!("str {:?}", String::from_utf8_lossy(b)),
997                            None => format!(
998                                "non-str {:#x}",
999                                match v {
1000                                    Value::Table(t) => t.as_ptr() as usize,
1001                                    Value::Closure(c) => c.as_ptr() as usize,
1002                                    Value::Coro(c) => c.as_ptr() as usize,
1003                                    Value::Userdata(u) => u.as_ptr() as usize,
1004                                    Value::Native(n) => n.as_ptr() as usize,
1005                                    _ => 0,
1006                                }
1007                            ),
1008                        };
1009                        panic!(
1010                            "[gc-verify] {ctx}: BLACK table {tp:#x} holds dead-white {what} \
1011                             (slot {idx}): {detail} — missed write barrier"
1012                        );
1013                    });
1014                }
1015                cur = (*cur).next;
1016            }
1017        }
1018    }
1019
1020    /// Borrow Heap's persistent propagate state as an ephemeral Marker.
1021    /// Caller MUST call `stash_marker` with the same Marker after work to
1022    /// write the (potentially mutated) state back. Used by the incremental
1023    /// Propagate path to avoid lifetime entanglement between `&mut self` and
1024    /// `&mut self.propagate`.
1025    fn loan_marker(&mut self) -> Marker {
1026        let mut prop = self
1027            .propagate
1028            .take()
1029            .expect("propagate state taken outside Propagate phase");
1030        Marker {
1031            stack: std::mem::take(&mut self.gray),
1032            weak: std::mem::take(&mut prop.weak),
1033            ephemeron: std::mem::take(&mut prop.ephemeron),
1034            no_ephemeron: prop.no_ephemeron,
1035            cached_protos: std::mem::take(&mut prop.cached_protos),
1036        }
1037    }
1038
1039    fn stash_marker(&mut self, m: Marker) {
1040        let no_ephemeron = m.no_ephemeron;
1041        self.gray = m.stack;
1042        self.propagate = Some(PropagateState {
1043            weak: m.weak,
1044            ephemeron: m.ephemeron,
1045            cached_protos: m.cached_protos,
1046            no_ephemeron,
1047        });
1048    }
1049
1050    /// Begin an incremental mark cycle: seed the persistent gray queue from
1051    /// roots + extra + tobefnz + any barrier-carried gray, install a fresh
1052    /// PropagateState, and enter `GcPhase::Propagate`. Precondition: `Pause`.
1053    pub(crate) fn gc_start_propagate(&mut self, roots: &[Value], extra: &[*mut GcHeader]) {
1054        debug_assert!(self.phase == GcPhase::Pause);
1055        self.phase = GcPhase::Propagate;
1056        self.propagate = Some(PropagateState {
1057            weak: Vec::new(),
1058            ephemeron: Vec::new(),
1059            cached_protos: Vec::new(),
1060            no_ephemeron: self.no_ephemeron,
1061        });
1062        let mut m = self.loan_marker();
1063        for &r in roots {
1064            m.value(r);
1065        }
1066        for &h in extra {
1067            m.header(h);
1068        }
1069        for &h in &self.tobefnz {
1070            m.header(h);
1071        }
1072        self.stash_marker(m);
1073    }
1074
1075    /// Drain up to `budget` gray objects (blacken + trace). Returns true if
1076    /// the gray queue is now empty (caller should follow up with
1077    /// `gc_finish_atomic`). PUC `propagatemark` budgeted loop.
1078    pub(crate) fn gc_step_propagate(&mut self, budget: usize) -> bool {
1079        debug_assert!(self.phase == GcPhase::Propagate);
1080        let mut m = self.loan_marker();
1081        let mut n = 0;
1082        while n < budget {
1083            let Some(h) = m.stack.pop() else {
1084                break;
1085            };
1086            // 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).
1087            unsafe {
1088                (*h).flags = ((*h).flags & !WHITE_BITS) | BLACK;
1089                match (*h).tag {
1090                    ObjTag::Str => {}
1091                    ObjTag::Table => (*(h as *mut Table)).trace(&mut m),
1092                    ObjTag::Proto => (*(h as *mut Proto)).trace(&mut m),
1093                    ObjTag::Closure => (*(h as *mut LuaClosure)).trace(&mut m),
1094                    ObjTag::Upvalue => (*(h as *mut Upvalue)).trace(&mut m),
1095                    ObjTag::Native => (*(h as *mut NativeClosure)).trace(&mut m),
1096                    ObjTag::Coro => (*(h as *mut crate::runtime::Coro)).trace(&mut m),
1097                    ObjTag::Userdata => (*(h as *mut Userdata)).trace(&mut m),
1098                }
1099            }
1100            n += 1;
1101        }
1102        let exhausted = m.stack.is_empty();
1103        self.stash_marker(m);
1104        exhausted
1105    }
1106
1107    /// Conclude a Propagate cycle: drain any residual gray, run the atomic
1108    /// tail (weak / ephemeron / finalizer / proto-cache / flip), detach `all`
1109    /// into `sweep_cur`, and enter `GcPhase::Sweep`. Releases `propagate`.
1110    /// PUC `atomic` + `entersweep` transition.
1111    pub(crate) fn gc_finish_atomic(&mut self) {
1112        debug_assert!(self.phase == GcPhase::Propagate);
1113        let mut m = self.loan_marker();
1114        // any residual gray (caller may not have drained to empty)
1115        drain_marker(&mut m);
1116        // pre-atomic ephemeron convergence
1117        if !m.ephemeron.is_empty() {
1118            loop {
1119                let mut changed = false;
1120                let eph = m.ephemeron.clone();
1121                for t in eph {
1122                    // 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).
1123                    changed |= unsafe { (*t).converge_ephemeron(&weak_key_alive, &mut m) };
1124                }
1125                drain_marker(&mut m);
1126                if !changed {
1127                    break;
1128                }
1129            }
1130        }
1131        self.atomic_tail(&mut m);
1132        // PropagateState consumed; transition to Sweep phase by detaching
1133        // the whole heap into sweep_cur (mirrors gc_mark_atomic). Anything
1134        // allocated past this point links onto fresh `all` and survives.
1135        self.propagate = None;
1136        debug_assert!(self.gray.is_empty(), "gray queue not drained at atomic");
1137        self.sweep_cur = std::mem::replace(&mut self.all, ptr::null_mut());
1138        self.phase = GcPhase::Sweep;
1139    }
1140
1141    /// Phase peek (for the VM-side step driver).
1142    pub(crate) fn gc_phase_is_pause(&self) -> bool {
1143        self.phase == GcPhase::Pause
1144    }
1145    pub(crate) fn gc_phase_is_propagate(&self) -> bool {
1146        self.phase == GcPhase::Propagate
1147    }
1148    #[allow(dead_code)] // public phase-peek API trio; sweep variant unused internally
1149    pub(crate) fn gc_phase_is_sweep(&self) -> bool {
1150        self.phase == GcPhase::Sweep
1151    }
1152
1153    /// Forward write barrier: when a BLACK `parent` acquires a fresh reference
1154    /// to a WHITE `child`, gray the child (strings go straight to BLACK as
1155    /// leaves) and push onto the persistent gray queue so the next propagate
1156    /// step traces it. Mirrors PUC `luaC_barrier_`. No-op outside Propagate
1157    /// (parent is gray or white — the mutator never sees a BLACK object live
1158    /// outside an incremental cycle).
1159    #[allow(clippy::not_unsafe_ptr_arg_deref)] // Internal GC barrier; caller (Gc<T>::write_*) guarantees ptr validity per SAFETY below.
1160    pub fn barrier_forward(&mut self, parent: *mut GcHeader, child: Value) {
1161        // 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).
1162        unsafe {
1163            if !is_black((*parent).flags) {
1164                return;
1165            }
1166            let ch = match child {
1167                Value::Str(s) => s.as_ptr() as *mut GcHeader,
1168                Value::Table(t) => t.as_ptr() as *mut GcHeader,
1169                Value::Closure(c) => c.as_ptr() as *mut GcHeader,
1170                Value::Native(n) => n.as_ptr() as *mut GcHeader,
1171                Value::Coro(c) => c.as_ptr() as *mut GcHeader,
1172                Value::Userdata(u) => u.as_ptr() as *mut GcHeader,
1173                _ => return,
1174            };
1175            let cf = (*ch).flags;
1176            if !is_white(cf) {
1177                return;
1178            }
1179            if (*ch).tag == ObjTag::Str {
1180                (*ch).flags = (cf & !COLOR_BITS) | BLACK;
1181            } else {
1182                (*ch).flags = cf & !WHITE_BITS;
1183                self.gray.push(ch);
1184            }
1185        }
1186    }
1187
1188    /// Backward write barrier for objects with many fields (tables, threads):
1189    /// demote the parent itself back to gray so propagate re-traces it.
1190    /// Mirrors PUC `luaC_barrierback_`. One call covers any number of
1191    /// subsequent stores until the next propagate finishes — much cheaper for
1192    /// tables than per-child forward barriers. No-op outside Propagate.
1193    #[allow(clippy::not_unsafe_ptr_arg_deref)] // Internal GC barrier; caller (Gc<T>::write_*) guarantees ptr validity per SAFETY below.
1194    pub fn barrier_back(&mut self, parent: *mut GcHeader) {
1195        // 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).
1196        unsafe {
1197            let f = (*parent).flags;
1198            if !is_black(f) {
1199                return;
1200            }
1201            (*parent).flags = f & !COLOR_BITS;
1202            self.gray.push(parent);
1203        }
1204    }
1205
1206    /// Move every registered finalizable that is now unreachable to `tobefnz`
1207    /// and resurrect it (mark it via `m`) so it — and the data its `__gc` needs
1208    /// — survives this cycle. Survivors stay registered in `finalize`. PUC's
1209    /// `separatetobefnz` walks `g->finobj` head-first, but `g->finobj` is a
1210    /// linked list that registration *prepends* to — so dead objects end up
1211    /// in `tobefnz` in reverse registration order, and `__gc` ultimately
1212    /// runs LIFO. luna's `finalize` is a Vec that grows forward, so iterate
1213    /// it in reverse here to match the LIFO contract (gc.lua's userdata
1214    /// section asserts the finalizers fire from value 10 back to 0).
1215    fn separate_finalizables(&mut self, m: &mut Marker) {
1216        let mut i = self.finalize.len();
1217        while i > 0 {
1218            i -= 1;
1219            let h = self.finalize[i];
1220            // 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).
1221            if unsafe { is_white((*h).flags) } {
1222                // Two-pass cycle-finalize (PUC 5.3 gc.lua :502): when a
1223                // finalizable table holds onto an unreachable coroutine, the
1224                // cycle (table → coroutine.stack → closure → table) keeps the
1225                // mark phase from reaching the table even though it is still
1226                // logically alive for one more GC pass. PUC's mark-sweep wakes
1227                // it via `markbeingfnz` *after* sweeping, so the actual `__gc`
1228                // call lands one cycle later. luna mirrors this by resurrecting
1229                // the table on the first sighting and only enqueuing it for
1230                // `__gc` on the second.
1231                let in_thread_cycle = self.defer_thread_cycle_finalize
1232                    // 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).
1233                    && unsafe { (*h).tag } == ObjTag::Table
1234                    && {
1235                        let t = h as *mut Table;
1236                        // 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).
1237                        unsafe { (*t).refs_contain_unmarked_coro() }
1238                    };
1239                // 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).
1240                let already_deferred = unsafe { (*h).flags & DEFERRED != 0 };
1241                if in_thread_cycle && !already_deferred {
1242                    // 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).
1243                    unsafe { (*h).flags |= DEFERRED };
1244                    m.header(h);
1245                    continue;
1246                }
1247                // 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).
1248                unsafe { (*h).flags = ((*h).flags & !(FIN | DEFERRED)) | FINALIZED };
1249                self.tobefnz.push(h);
1250                m.header(h);
1251                self.finalize.swap_remove(i);
1252            } else {
1253                // 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).
1254                unsafe { (*h).flags &= !DEFERRED };
1255            }
1256        }
1257    }
1258
1259    /// Register a table for finalization (a live `__gc` metamethod was just set
1260    /// via setmetatable). No-op if it is already pending a finalize (FIN bit).
1261    /// PUC 5.5 reference manual §2.5.3: "An object can be marked again for
1262    /// finalization by calling setmetatable with a different metatable, or
1263    /// with the same metatable but with a different __gc field" — so a
1264    /// previously finalized object (FINALIZED bit set then reset by
1265    /// `take_tobefnz`) can re-register. PUC's `luaC_checkfinalizer` is gated
1266    /// on `tofinalize(o)` only, which mirrors checking the FIN bit.
1267    pub(crate) fn register_finalizable(&mut self, t: Gc<Table>) {
1268        let h = t.as_ptr() as *mut GcHeader;
1269        // 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).
1270        unsafe {
1271            if (*h).flags & FIN == 0 {
1272                (*h).flags |= FIN;
1273                self.finalize.push(h);
1274            }
1275        }
1276    }
1277
1278    /// Register a userdata for finalization. PUC 5.1 `newproxy(true)` plus a
1279    /// metatable carrying `__gc` lets a Lua script attach a finalizer to a
1280    /// proxy object — gc.lua's "testing userdata" section binds this
1281    /// behaviour together with weak tables.
1282    pub(crate) fn register_finalizable_userdata(&mut self, u: Gc<crate::runtime::Userdata>) {
1283        let h = u.as_ptr() as *mut GcHeader;
1284        // 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).
1285        unsafe {
1286            if (*h).flags & FIN == 0 {
1287                (*h).flags |= FIN;
1288                self.finalize.push(h);
1289            }
1290        }
1291    }
1292
1293    /// Take the objects awaiting their `__gc` call (the VM runs the
1294    /// finalizers). Each entry is dispatched on its `ObjTag` so the caller
1295    /// can look up `__gc` for either a table or a proxy userdata.
1296    ///
1297    /// Mirrors PUC 5.5 `udata2finalize`: the FINALIZED bit is reset on the
1298    /// way out so a `setmetatable(obj, mt_with___gc)` inside (or after) the
1299    /// finalizer can re-register the object for a future round, per the Lua
1300    /// 5.5 reference manual §2.5.3 re-finalize semantics.
1301    pub(crate) fn take_tobefnz(&mut self) -> Vec<crate::runtime::Value> {
1302        use crate::runtime::Value;
1303        std::mem::take(&mut self.tobefnz)
1304            .into_iter()
1305            // 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).
1306            .map(|h| unsafe {
1307                (*h).flags &= !FINALIZED;
1308                match (*h).tag {
1309                    ObjTag::Table => Value::Table(Gc::from_ptr(h as *mut Table)),
1310                    ObjTag::Userdata => {
1311                        Value::Userdata(Gc::from_ptr(h as *mut crate::runtime::Userdata))
1312                    }
1313                    _ => unreachable!("non-finalizable object queued for finalization"),
1314                }
1315            })
1316            .collect()
1317    }
1318
1319    /// Move ALL still-registered finalizables to the pending queue regardless of
1320    /// reachability (PUC separatetobefnz(g, 1) at state close), so the VM can run
1321    /// every `__gc` before the heap is torn down.
1322    pub(crate) fn queue_all_finalizers(&mut self) {
1323        for h in std::mem::take(&mut self.finalize) {
1324            // 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).
1325            unsafe { (*h).flags = ((*h).flags & !FIN) | FINALIZED };
1326            self.tobefnz.push(h);
1327        }
1328    }
1329
1330    /// Sweep the whole `all` list in one pass: free dead-white objects,
1331    /// transition survivors (BLACK or current-white) to the new current-white
1332    /// so the next cycle can re-mark them. Returns the number of objects freed.
1333    fn full_sweep(&mut self) -> usize {
1334        // detach the list first so freeing (which needs &mut self for the
1335        // string table) never aliases a pointer into self
1336        let mut freed = 0;
1337        let new_white = self.current_white;
1338        // 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).
1339        unsafe {
1340            let mut cur = std::mem::replace(&mut self.all, ptr::null_mut());
1341            let mut kept_head: *mut GcHeader = ptr::null_mut();
1342            let mut kept_tail: *mut GcHeader = ptr::null_mut();
1343            while !cur.is_null() {
1344                let next = (*cur).next;
1345                let f = (*cur).flags;
1346                // dead = other-white (i.e. white but not current-white).
1347                // Survivors are BLACK (just-marked) or current-white (born
1348                // during the sweep itself).
1349                let dead = is_white(f) && (f & new_white) == 0;
1350                if !dead {
1351                    (*cur).flags = (f & !COLOR_BITS) | new_white;
1352                    (*cur).next = ptr::null_mut();
1353                    if kept_tail.is_null() {
1354                        kept_head = cur;
1355                    } else {
1356                        (*kept_tail).next = cur;
1357                    }
1358                    kept_tail = cur;
1359                } else {
1360                    self.free_obj(cur);
1361                    freed += 1;
1362                }
1363                cur = next;
1364            }
1365            self.all = kept_head;
1366        }
1367        self.live -= freed;
1368        #[cfg(feature = "gc-verify")]
1369        self.verify_no_dangling("full_sweep");
1370        freed
1371    }
1372
1373    /// Sweep up to `budget` objects from the detached `sweep_cur` list: free
1374    /// unmarked ones, splice marked survivors back onto `all` (clearing their
1375    /// MARK bit). Returns true once the list is exhausted (cycle complete →
1376    /// back to `Pause`). Safe with no write barrier: marking was atomic, so any
1377    /// object still unmarked here was unreachable at mark time and the mutator
1378    /// holds no reference that could have resurrected it.
1379    pub(crate) fn gc_sweep_step(&mut self, budget: usize) -> bool {
1380        let mut n = 0;
1381        let new_white = self.current_white;
1382        // 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).
1383        unsafe {
1384            while n < budget && !self.sweep_cur.is_null() {
1385                let cur = self.sweep_cur;
1386                let next = (*cur).next;
1387                let f = (*cur).flags;
1388                let dead = is_white(f) && (f & new_white) == 0;
1389                if !dead {
1390                    (*cur).flags = (f & !COLOR_BITS) | new_white;
1391                    (*cur).next = self.all;
1392                    self.all = cur;
1393                } else {
1394                    self.free_obj(cur);
1395                    self.live -= 1;
1396                }
1397                self.sweep_cur = next;
1398                n += 1;
1399            }
1400        }
1401        // Verify after EVERY step, not just cycle completion: the
1402        // mutator runs between steps, so a reference dangling mid-cycle
1403        // is already a live bug (this is exactly UAF-C's window).
1404        #[cfg(feature = "gc-verify")]
1405        self.verify_no_dangling("sweep_step");
1406        if self.sweep_cur.is_null() {
1407            self.phase = GcPhase::Pause;
1408            true
1409        } else {
1410            false
1411        }
1412    }
1413
1414    /// v2.13 WUC `gc-verify` — post-sweep dangling-reference check
1415    /// (PUC `lua_checkmemory` analogue). Builds the live set from the
1416    /// `all` list + finalizer queues, then walks every live table's
1417    /// collectable refs via [`Table::verify_refs`]. Panics with table
1418    /// pointer + slot detail on the first reference whose target is no
1419    /// longer live — i.e. at the collect that CREATED the dangling
1420    /// pointer, not at the later allocator-dependent dereference.
1421    #[cfg(feature = "gc-verify")]
1422    pub fn verify_no_dangling(&self, ctx: &str) {
1423        use std::collections::HashSet;
1424        fn value_header(v: Value) -> Option<*mut GcHeader> {
1425            match v {
1426                Value::Str(s) => Some(s.as_ptr() as *mut GcHeader),
1427                Value::Table(t) => Some(t.as_ptr() as *mut GcHeader),
1428                Value::Closure(c) => Some(c.as_ptr() as *mut GcHeader),
1429                Value::Native(n) => Some(n.as_ptr() as *mut GcHeader),
1430                Value::Coro(c) => Some(c.as_ptr() as *mut GcHeader),
1431                Value::Userdata(u) => Some(u.as_ptr() as *mut GcHeader),
1432                _ => None,
1433            }
1434        }
1435        let mut live: HashSet<usize> = HashSet::new();
1436        // SAFETY: `all` / `sweep_cur` / finalizer-queue pointers are the
1437        // heap's own intrusive lists; every element is a live allocation
1438        // until free_obj unlinks it.
1439        unsafe {
1440            let mut cur = self.all;
1441            while !cur.is_null() {
1442                live.insert(cur as usize);
1443                cur = (*cur).next;
1444            }
1445            let mut cur = self.sweep_cur;
1446            while !cur.is_null() {
1447                live.insert(cur as usize);
1448                cur = (*cur).next;
1449            }
1450            for &h in &self.finalize {
1451                live.insert(h as usize);
1452            }
1453            for &h in &self.tobefnz {
1454                live.insert(h as usize);
1455            }
1456            let is_live = |v: Value| -> bool {
1457                match value_header(v) {
1458                    None => true,
1459                    Some(h) => live.contains(&(h as usize)),
1460                }
1461            };
1462            // Walk BOTH lists: `all` (already-swept survivors) and
1463            // `sweep_cur` (not-yet-swept remainder of an incremental
1464            // cycle). A dangling reference can live in either while the
1465            // mutator runs between sweep steps.
1466            let new_white = self.current_white;
1467            for head in [self.all, self.sweep_cur] {
1468                let mut cur = head;
1469                while !cur.is_null() {
1470                    // Skip dead-but-not-yet-swept objects on `sweep_cur`:
1471                    // they are unreachable, so a dangling ref inside them
1472                    // is benign (their peers may already be freed).
1473                    let f = (*cur).flags;
1474                    if is_white(f) && (f & new_white) == 0 {
1475                        cur = (*cur).next;
1476                        continue;
1477                    }
1478                    if (*cur).tag == ObjTag::Table {
1479                        let t = &*(cur as *const Table);
1480                        let tp = cur as usize;
1481                        t.verify_refs(&is_live, &|what, idx, v| {
1482                            // Do NOT Debug-format `v` — for Value::Str that
1483                            // would dereference the (dangling) payload.
1484                            let p = value_header(v).map(|h| h as usize).unwrap_or(0);
1485                            panic!(
1486                                "[gc-verify] {ctx}: table {tp:#x} holds dangling {what} \
1487                                 (slot {idx}, target {p:#x})"
1488                            );
1489                        });
1490                    }
1491                    cur = (*cur).next;
1492                }
1493            }
1494        }
1495    }
1496
1497    unsafe fn free_obj(&mut self, h: *mut GcHeader) {
1498        #[cfg(feature = "gc-verify")]
1499        {
1500            self.recently_freed.insert(h as usize);
1501            crate::runtime::gc_verify_probe::FREED.with(|f| f.borrow_mut().insert(h as usize));
1502        }
1503        // 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).
1504        unsafe {
1505            match (*h).tag {
1506                ObjTag::Table => {
1507                    let t = h as *mut Table;
1508                    let internal = (*t).internal_bytes();
1509                    self.bytes = self
1510                        .bytes
1511                        .saturating_sub(std::mem::size_of::<Table>() + internal);
1512                    // P17-D v2 layer-15 attack — pool recycle. Drop the
1513                    // Box-owned interior (slab, nodes, metatable) so the
1514                    // Table struct itself can be re-handed-out by a
1515                    // future `new_table` without re-mallocing. Cap pool
1516                    // at 4096 entries to bound idle memory.
1517                    const TABLE_POOL_CAP: usize = 4096;
1518                    if self.table_pool.len() < TABLE_POOL_CAP {
1519                        // Free interior heap allocations now (slab + nodes).
1520                        // Each Box::new([]) is a dangling no-alloc empty
1521                        // slice, so reassigning is just a pointer move.
1522                        (*t).slab = Box::new([]);
1523                        (*t).nodes = Box::new([]);
1524                        // C3 — drop SoA Robin Hood parallel arrays too.
1525                        // These are Box::new([]) dangling stubs until
1526                        // Phase D cuts over (Phase B initial state).
1527                        (*t).keys = Box::new([]);
1528                        (*t).vals = Box::new([]);
1529                        (*t).meta = Box::new([]);
1530                        (*t).tombstones = 0;
1531                        (*t).iter_depth = 0;
1532                        (*t).metatable = None;
1533                        // Stash the raw pointer for future reuse.
1534                        // SAFETY: t is non-null (came from a live Gc<Table>);
1535                        // pool owns it until reuse or Heap::Drop.
1536                        self.table_pool.push(std::ptr::NonNull::new_unchecked(t));
1537                    } else {
1538                        drop(Box::from_raw(t));
1539                    }
1540                }
1541                ObjTag::Proto => {
1542                    self.bytes = self.bytes.saturating_sub(std::mem::size_of::<Proto>());
1543                    drop(Box::from_raw(h as *mut Proto));
1544                }
1545                ObjTag::Closure => {
1546                    self.bytes = self.bytes.saturating_sub(std::mem::size_of::<LuaClosure>());
1547                    drop(Box::from_raw(h as *mut LuaClosure));
1548                }
1549                ObjTag::Upvalue => {
1550                    self.bytes = self.bytes.saturating_sub(std::mem::size_of::<Upvalue>());
1551                    drop(Box::from_raw(h as *mut Upvalue));
1552                }
1553                ObjTag::Native => {
1554                    self.bytes = self
1555                        .bytes
1556                        .saturating_sub(std::mem::size_of::<NativeClosure>());
1557                    drop(Box::from_raw(h as *mut NativeClosure));
1558                }
1559                ObjTag::Coro => {
1560                    self.bytes = self
1561                        .bytes
1562                        .saturating_sub(std::mem::size_of::<crate::runtime::Coro>());
1563                    drop(Box::from_raw(h as *mut crate::runtime::Coro));
1564                }
1565                ObjTag::Userdata => {
1566                    self.bytes = self.bytes.saturating_sub(std::mem::size_of::<Userdata>());
1567                    drop(Box::from_raw(h as *mut Userdata));
1568                }
1569                ObjTag::Str => {
1570                    let s = h as *mut LuaStr;
1571                    self.bytes = self.bytes.saturating_sub(string::alloc_size((*s).len()));
1572                    if (*s).is_short() {
1573                        self.strings.remove(s);
1574                    }
1575                    string::free(s);
1576                }
1577            }
1578        }
1579    }
1580}
1581
1582impl Drop for Heap {
1583    fn drop(&mut self) {
1584        // free everything regardless of reachability, including any list still
1585        // detached for an in-flight incremental sweep
1586        // 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).
1587        unsafe {
1588            for mut cur in [self.all, self.sweep_cur] {
1589                while !cur.is_null() {
1590                    let next = (*cur).next;
1591                    self.free_obj(cur);
1592                    cur = next;
1593                }
1594            }
1595            // P17-D v2 layer-15 attack — release the table_pool's
1596            // dangling Box<Table> ptrs. Each was Box::into_raw'd into
1597            // the pool (via free_obj recycle path); without this, the
1598            // Tables would leak. The pool's Tables had their interior
1599            // Box-owned fields (slab/nodes/metatable) already cleared
1600            // when they were recycled, so dropping the Table now only
1601            // releases the Table struct itself.
1602            for ptr in self.table_pool.drain(..) {
1603                drop(Box::from_raw(ptr.as_ptr()));
1604            }
1605        }
1606    }
1607}
1608
1609impl Default for Heap {
1610    fn default() -> Heap {
1611        Heap::new()
1612    }
1613}
1614
1615/// Mark accumulator: gray stack plus entry points for Values and bare
1616/// object headers (Protos/Upvalues are not first-class Values).
1617pub(crate) struct Marker {
1618    stack: Vec<*mut GcHeader>,
1619    /// live tables with a weak `__mode`, collected during marking and processed
1620    /// (dead weak entries cleared) before the sweep
1621    pub(crate) weak: Vec<*mut Table>,
1622    /// ephemeron tables (weak keys, strong values): their hash values are not
1623    /// marked during trace but in a fixpoint pass keyed on key-reachability
1624    pub(crate) ephemeron: Vec<*mut Table>,
1625    /// PUC 5.1 mode: skip ephemeron handling — `__mode='k'` tables mark their
1626    /// values strongly during the normal trace pass (see [`Heap::no_ephemeron`]).
1627    pub(crate) no_ephemeron: bool,
1628    /// Protos with a non-null closure cache (PUC `Proto.cache`). After
1629    /// marking is done, any cached LClosure that ended the cycle unmarked is
1630    /// cleared so the sweep can collect it — the cache is a *weak* reference
1631    /// (PUC `traverseproto` checks `iswhite(cache)`). Seen via [`Proto::trace`].
1632    pub(crate) cached_protos: Vec<*mut crate::runtime::Proto>,
1633}
1634
1635/// Drain the gray stack: pop each marked object and trace its children until
1636/// the worklist is empty (iterative, so deep graphs don't overflow the Rust
1637/// stack). Shared by the root mark and the post-resurrection remark.
1638fn drain_marker(m: &mut Marker) {
1639    while let Some(h) = m.stack.pop() {
1640        // 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).
1641        unsafe {
1642            // PUC `propagatemark`: gray → black before scanning children, so a
1643            // child that points back at us (cycle) re-traces us as already
1644            // black and does not loop. White bits were cleared on push.
1645            (*h).flags = ((*h).flags & !WHITE_BITS) | BLACK;
1646            match (*h).tag {
1647                ObjTag::Str => {}
1648                ObjTag::Table => (*(h as *mut Table)).trace(m),
1649                ObjTag::Proto => (*(h as *mut Proto)).trace(m),
1650                ObjTag::Closure => (*(h as *mut LuaClosure)).trace(m),
1651                ObjTag::Upvalue => (*(h as *mut Upvalue)).trace(m),
1652                ObjTag::Native => (*(h as *mut NativeClosure)).trace(m),
1653                ObjTag::Coro => (*(h as *mut crate::runtime::Coro)).trace(m),
1654                ObjTag::Userdata => (*(h as *mut Userdata)).trace(m),
1655            }
1656        }
1657    }
1658}
1659
1660impl Marker {
1661    /// Mark a value, returning true if it was newly marked (was white).
1662    pub(crate) fn value(&mut self, v: Value) -> bool {
1663        let h = match v {
1664            Value::Str(s) => s.as_ptr() as *mut GcHeader,
1665            Value::Table(t) => t.as_ptr() as *mut GcHeader,
1666            Value::Closure(c) => c.as_ptr() as *mut GcHeader,
1667            Value::Native(n) => n.as_ptr() as *mut GcHeader,
1668            Value::Coro(c) => c.as_ptr() as *mut GcHeader,
1669            Value::Userdata(u) => u.as_ptr() as *mut GcHeader,
1670            _ => return false,
1671        };
1672        self.header(h)
1673    }
1674
1675    /// Mark a bare header, returning true if it was newly marked (was white).
1676    /// Transitions white → gray (in PUC `reallymarkobject` terms): clears the
1677    /// current-white bit and pushes onto the gray stack. `drain_marker` later
1678    /// pops it, traces children, and stamps it BLACK.
1679    pub(crate) fn header(&mut self, h: *mut GcHeader) -> bool {
1680        // 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).
1681        unsafe {
1682            let f = (*h).flags;
1683            if is_white(f) {
1684                (*h).flags = f & !WHITE_BITS;
1685                self.stack.push(h);
1686                true
1687            } else {
1688                false
1689            }
1690        }
1691    }
1692}
1693
1694/// Whether a value is "alive" for ephemeron key purposes: non-collectable
1695/// values and strings are always alive (strings are never weakly cleared);
1696/// a collectable object is alive only once marked (gray or black).
1697fn weak_key_alive(v: Value) -> bool {
1698    let h = match v {
1699        Value::Table(t) => t.as_ptr() as *mut GcHeader,
1700        Value::Closure(c) => c.as_ptr() as *mut GcHeader,
1701        Value::Native(n) => n.as_ptr() as *mut GcHeader,
1702        Value::Coro(c) => c.as_ptr() as *mut GcHeader,
1703        Value::Userdata(u) => u.as_ptr() as *mut GcHeader,
1704        _ => return true, // strings, numbers, booleans: never weak-collected
1705    };
1706    // 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).
1707    unsafe { !is_white((*h).flags) }
1708}
1709
1710/// Hash seed from address entropy (ASLR) and clock, luaL_makeseed style.
1711fn make_seed() -> u32 {
1712    let stack_var = 0u8;
1713    let mut h = &stack_var as *const u8 as u64;
1714    h ^= (make_seed as *const () as u64) << 16;
1715    if let Ok(d) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
1716        h ^= (d.subsec_nanos() as u64) << 32 ^ d.as_secs();
1717    }
1718    h ^= h >> 33;
1719    h = h.wrapping_mul(0xff51_afd7_ed55_8ccd);
1720    h ^= h >> 33;
1721    h as u32
1722}
1723
1724#[cfg(test)]
1725mod tests {
1726    use super::*;
1727    use crate::vm::isa::{Inst, Op};
1728
1729    #[test]
1730    fn collect_traces_function_objects() {
1731        let mut heap = Heap::new();
1732        let source = heap.intern(b"@test");
1733        let kstr = heap.intern(b"a-constant-string");
1734        let inner = Proto {
1735            hdr: GcHeader::new(ObjTag::Proto),
1736            code: Box::new([Inst::iabc(Op::Return0, 0, 0, 0, false)]),
1737            consts: Box::new([]),
1738            protos: Box::new([]),
1739            upvals: Box::new([]),
1740            num_params: 0,
1741            is_vararg: false,
1742            has_vararg_table_pseudo: false,
1743            has_compat_vararg_arg: false,
1744            max_stack: 2,
1745            lines: Box::new([1]),
1746            source,
1747            line_defined: 1,
1748            last_line_defined: 1,
1749            locvars: Box::new([]),
1750            cache: std::cell::Cell::new(None),
1751            jit: std::cell::Cell::new(crate::runtime::function::JitProtoState::Untried),
1752            env_upval_idx: u8::MAX,
1753            trace_hot_count: std::cell::Cell::new(0),
1754            call_hot_count: std::cell::Cell::new(0),
1755            trace_discard_count: std::cell::Cell::new(0),
1756            trace_gave_up: std::cell::Cell::new(false),
1757            traces: crate::jit::send_compat::TRefLock::new(Vec::new()),
1758        };
1759        let inner = heap.adopt_proto(inner);
1760        let outer = Proto {
1761            hdr: GcHeader::new(ObjTag::Proto),
1762            code: Box::new([Inst::iabc(Op::Return0, 0, 0, 0, false)]),
1763            consts: Box::new([Value::Str(kstr)]),
1764            protos: Box::new([inner]),
1765            upvals: Box::new([]),
1766            num_params: 0,
1767            is_vararg: true,
1768            has_vararg_table_pseudo: false,
1769            has_compat_vararg_arg: false,
1770            max_stack: 2,
1771            lines: Box::new([1]),
1772            source,
1773            line_defined: 0,
1774            last_line_defined: 0,
1775            locvars: Box::new([]),
1776            cache: std::cell::Cell::new(None),
1777            jit: std::cell::Cell::new(crate::runtime::function::JitProtoState::Untried),
1778            env_upval_idx: u8::MAX,
1779            trace_hot_count: std::cell::Cell::new(0),
1780            call_hot_count: std::cell::Cell::new(0),
1781            trace_discard_count: std::cell::Cell::new(0),
1782            trace_gave_up: std::cell::Cell::new(false),
1783            traces: crate::jit::send_compat::TRefLock::new(Vec::new()),
1784        };
1785        let outer = heap.adopt_proto(outer);
1786        let captured = heap.intern(b"captured-value-string-xxxxxxxxxxxxxxxxxxxxxxxxx");
1787        let uv = heap.new_upvalue(UpvalState::Closed(Value::Str(captured)));
1788        let cl = heap.new_closure(outer, Box::new([uv]));
1789        // objects: source, kstr, inner, outer, captured, uv, cl
1790        assert_eq!(heap.live_objects(), 7);
1791        // rooting the closure keeps the whole graph alive
1792        assert_eq!(heap.collect(&[Value::Closure(cl)]), 0);
1793        assert_eq!(heap.live_objects(), 7);
1794        assert_eq!(heap.collect(&[]), 7);
1795        assert_eq!(heap.live_objects(), 0);
1796    }
1797
1798    #[test]
1799    fn collect_unreachable() {
1800        let mut heap = Heap::new();
1801        let s = heap.intern(b"hello");
1802        let t = heap.new_table();
1803        assert_eq!(heap.live_objects(), 2);
1804        // both rooted: nothing freed
1805        assert_eq!(heap.collect(&[Value::Str(s), Value::Table(t)]), 0);
1806        // only table rooted: string freed
1807        assert_eq!(heap.collect(&[Value::Table(t)]), 1);
1808        assert_eq!(heap.live_objects(), 1);
1809        // nothing rooted
1810        assert_eq!(heap.collect(&[]), 1);
1811        assert_eq!(heap.live_objects(), 0);
1812    }
1813
1814    #[test]
1815    fn collect_traces_table_contents() {
1816        let mut heap = Heap::new();
1817        let t = heap.new_table();
1818        let k = heap.intern(b"key-string-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); // long
1819        let v = heap.intern(b"val");
1820        unsafe { t.as_mut() }
1821            .set(&mut heap, Value::Str(k), Value::Str(v))
1822            .unwrap();
1823        let inner = heap.new_table();
1824        unsafe { t.as_mut() }
1825            .set(&mut heap, Value::Int(1), Value::Table(inner))
1826            .unwrap();
1827        unsafe { inner.as_mut() }.set_metatable(Some(t));
1828        assert_eq!(heap.live_objects(), 4);
1829        // root only the outer table: everything reachable through it survives
1830        assert_eq!(heap.collect(&[Value::Table(t)]), 0);
1831        assert_eq!(heap.live_objects(), 4);
1832        assert_eq!(heap.collect(&[]), 4);
1833    }
1834
1835    #[test]
1836    fn interned_string_reclaimed_and_reinternable() {
1837        let mut heap = Heap::new();
1838        heap.intern(b"transient");
1839        assert_eq!(heap.collect(&[]), 1);
1840        let s2 = heap.intern(b"transient");
1841        assert_eq!(s2.as_bytes(), b"transient");
1842        assert_eq!(heap.live_objects(), 1);
1843    }
1844
1845    #[test]
1846    fn bytes_and_live_round_trip_to_zero() {
1847        // Memory-invariant audit: after a churn of table allocation, rehash-
1848        // driven growth, and full collection of an empty root set, both
1849        // `heap.bytes` and `heap.live_objects` must return to 0. Catches any
1850        // alloc / free asymmetry in the Table internal-Box delta tracking
1851        // (4bab3c5) or the live counter (link/sweep symmetry).
1852        let mut heap = Heap::new();
1853        assert_eq!(heap.bytes(), 0);
1854        assert_eq!(heap.live_objects(), 0);
1855        // Build a churn: 50 tables, each filled with 200 int keys (forces
1856        // multiple rehashes); plus interned strings spliced through the
1857        // hash part. Bytes should grow well past the empty baseline.
1858        let mut roots: Vec<Value> = Vec::new();
1859        for ti in 0..50 {
1860            let t = heap.new_table();
1861            for k in 1..=200 {
1862                let _ =
1863                    unsafe { t.as_mut() }.set(&mut heap, Value::Int(k), Value::Int(ti * 1000 + k));
1864            }
1865            for sk in 0..32 {
1866                let key = Value::Str(heap.intern(format!("k{ti}-{sk}").as_bytes()));
1867                let _ = unsafe { t.as_mut() }.set(&mut heap, key, Value::Int(sk));
1868            }
1869            roots.push(Value::Table(t));
1870        }
1871        let live_peak = heap.live_objects();
1872        let bytes_peak = heap.bytes();
1873        assert!(live_peak > 0, "live should be >0 after churn");
1874        assert!(bytes_peak > 0, "bytes should be >0 after churn");
1875        // Root only half — the other half should be collected.
1876        let half = roots.len() / 2;
1877        let freed = heap.collect(&roots[..half]);
1878        assert!(freed > 0, "some objects should have been freed");
1879        assert!(
1880            heap.bytes() < bytes_peak,
1881            "bytes must drop after partial collect"
1882        );
1883        assert!(
1884            heap.live_objects() < live_peak,
1885            "live must drop after partial collect"
1886        );
1887        // Drop everything: counters must return to 0 exactly.
1888        drop(roots);
1889        let _ = heap.collect(&[]);
1890        assert_eq!(heap.live_objects(), 0, "live not zero after full collect");
1891        assert_eq!(
1892            heap.bytes(),
1893            0,
1894            "bytes not zero after full collect — asymmetric alloc/free"
1895        );
1896    }
1897
1898    /// Regression for `.dev/known-bugs/stringtable-intern-uaf.md`:
1899    /// `StringTable::intern` must NOT return a dead-white (about-to-be-swept)
1900    /// short-string pointer. Mirrors PUC `luaS_new`'s resurrect-on-hit guard
1901    /// (lstring.c — `if (isdead(g, ts)) changewhite(ts);`).
1902    ///
1903    /// Without the guard, the bucket-chain still references the unswept
1904    /// short string after the atomic flip; a re-`intern` of the same bytes
1905    /// hands back that pointer; the budget-paced sweep then frees it and
1906    /// the next bucket walk dereferences libc-recycled garbage (the
1907    /// `0x800002a80000002d` misaligned pointer seen in the audit).
1908    #[test]
1909    fn intern_resurrects_dead_white_short_string() {
1910        let mut heap = Heap::new();
1911        let alive = heap.intern(b"keep-me-alive-1");
1912        let dying = heap.intern(b"transient-x");
1913        let dying_ptr = dying.as_ptr();
1914        let dying_bytes = dying.as_bytes().to_vec();
1915        // Drive an incremental cycle by hand to reproduce the race:
1916        //   1. mark-propagate with `alive` only as a root → `dying` stays white
1917        //   2. atomic flip → `dying` becomes dead-white, bucket still points at it
1918        //   3. RE-INTERN the same bytes BEFORE sweep clears the bucket
1919        //   4. fix must either (a) skip the dead entry & alloc fresh, or
1920        //      (b) resurrect dying back to current-white
1921        let alive_root = [Value::Str(alive)];
1922        heap.gc_start_propagate(&alive_root, &[]);
1923        while !heap.gc_step_propagate(usize::MAX) {}
1924        heap.gc_finish_atomic();
1925        // At this point sweep_cur holds the detached old-heap list and the
1926        // dying string is dead-white. The bucket chain in `self.strings`
1927        // still references it.
1928        let resurrected = heap.intern(&dying_bytes);
1929        // Two valid outcomes:
1930        //   * resurrect: same pointer, but flagged current-white so sweep
1931        //     will keep it alive (PUC luaS_new shape)
1932        //   * skip-and-alloc-fresh: different pointer, dying gets swept
1933        //     normally as it should
1934        // Either way, after completing the sweep the heap must NOT crash
1935        // when we try to intern more short strings (which walks bucket chains).
1936        while !heap.gc_sweep_step(usize::MAX) {}
1937        // Smoke: bucket-chain walk for a fresh string must not deref a
1938        // freed pointer.
1939        let _fresh = heap.intern(b"after-sweep-canary");
1940        // If the fix is "resurrect", same pointer + bytes preserved:
1941        if resurrected.as_ptr() == dying_ptr {
1942            assert_eq!(resurrected.as_bytes(), dying_bytes.as_slice());
1943        }
1944        // Final cleanup: full collect must complete without UAF.
1945        drop(heap);
1946    }
1947
1948    #[test]
1949    fn deep_table_chain_marks_iteratively() {
1950        // deep chain: explicit mark stack must not overflow (smaller under
1951        // miri — the interpreter makes 100k tables take ~30 minutes)
1952        let n = if cfg!(miri) { 2_000 } else { 100_000 };
1953        let mut heap = Heap::new();
1954        let head = heap.new_table();
1955        let mut cur = head;
1956        for _ in 0..n {
1957            let next = heap.new_table();
1958            unsafe { cur.as_mut() }
1959                .set(&mut heap, Value::Int(1), Value::Table(next))
1960                .unwrap();
1961            cur = next;
1962        }
1963        assert_eq!(heap.collect(&[Value::Table(head)]), 0);
1964        assert_eq!(heap.collect(&[]), n + 1);
1965    }
1966}