Skip to main content

lua_gc/
heap.rs

1//! Phase D mark-and-sweep garbage collector.
2//!
3//! This module is the production GC for the Lua runtime, replacing the
4//! `Rc<T>`-backed `GcRef<T>` placeholder used through Phase B/C. It is a
5//! single-threaded, stop-the-world, precise tracing collector with a
6//! forward write barrier. Incremental marking is a future enhancement;
7//! the current implementation does a full collect each time `step` decides
8//! it's time.
9//!
10//! # Vocabulary
11//!
12//! - **Gc<T>**: a pointer-sized handle. `Copy + Clone`. Replaces `GcRef<T>`.
13//! - **GcBox<T>**: the heap allocation; contains a header and the value.
14//! - **GcHeader**: per-object metadata (color, age, finalized flag, intrusive
15//!   `next` pointer for the allgc list).
16//! - **Trace**: trait every GC-rooted type implements. The `trace` method
17//!   walks all `Gc<_>` fields and calls `Marker::mark` on each.
18//! - **Marker**: passed to `trace`; carries the gray queue.
19//! - **Heap**: owns the allgc list head, byte counters, GC state machine.
20//!
21//! # Safety model
22//!
23//! All `unsafe` is confined to this crate (per `harness/unsafe-budgets.toml`).
24//! The invariants are:
25//!
26//! 1. Every `Gc<T>` points to a valid, allocated, not-yet-swept `GcBox<T>`.
27//! 2. The allgc intrusive list is consistent: traversing `header.next` from
28//!    `Heap.head` reaches every live `GcBox` exactly once.
29//! 3. After `Heap::full_collect(roots)`, every `Gc<T>` reachable from `roots`
30//!    is still valid; unreachable boxes are dropped and deallocated.
31//!
32//! # Migration shape
33//!
34//! Existing code holds `GcRef<T>` (which after Phase D is a type alias for
35//! `Gc<T>`). Legacy call sites like `GcRef::new(value)` route through
36//! `Gc::new_uncollected` which allocates a `GcBox` but does NOT register it
37//! in any heap. Phase D-1b agent work converts these to
38//! `state.heap().allocate(value)` so the new box joins the allgc chain.
39
40use std::cell::{Cell, RefCell};
41use std::collections::HashMap;
42use std::marker::PhantomData;
43use std::ptr::NonNull;
44
45// ──────────────────────────────────────────────────────────────────────────
46// Phase D-1c — scoped thread-local HeapGuard
47//
48// Lua's C API supports multiple `lua_State`s on one OS thread (sandbox-per-
49// state is a real embedding pattern). We honor that by stacking the
50// currently-active heap rather than holding a single slot. `HeapGuard::push`
51// activates a heap; drop pops it.
52//
53// `with_current_heap(...)` exposes the top of the stack only for the dynamic
54// extent of a closure — used by `GcRef::new` call sites that don't have
55// `&mut LuaState` in scope.
56// ──────────────────────────────────────────────────────────────────────────
57
58thread_local! {
59    static CURRENT_HEAP_STACK: RefCell<Vec<NonNull<Heap>>> = const { RefCell::new(Vec::new()) };
60}
61
62/// A scoped guard for the currently-active heap. Pushed at entry to
63/// `state.run()` / `state.protected_call()` / `state.load()`; popped on
64/// drop. Supports nesting (multiple LuaStates on one thread).
65pub struct HeapGuard {
66    // Anchor a NonNull so the user can't accidentally drop the guard while
67    // an inner Lua state is still active. We rely on RAII.
68    _private: (),
69}
70
71impl HeapGuard {
72    /// Push `heap` onto the active stack. Returns a guard; dropping it pops.
73    ///
74    /// # Safety
75    ///
76    /// The pointer must remain valid for the lifetime of the guard. Callers
77    /// typically pass `&state.global.heap`, which lives as long as the
78    /// `GlobalState` (an `Rc<RefCell<_>>`); the guard must drop before the
79    /// state is dropped.
80    pub fn push(heap: &Heap) -> Self {
81        let ptr = NonNull::from(heap);
82        CURRENT_HEAP_STACK.with(|stack| stack.borrow_mut().push(ptr));
83        HeapGuard { _private: () }
84    }
85}
86
87impl Drop for HeapGuard {
88    fn drop(&mut self) {
89        CURRENT_HEAP_STACK.with(|stack| {
90            let popped = stack.borrow_mut().pop();
91            debug_assert!(popped.is_some(), "HeapGuard::drop with empty stack");
92        });
93    }
94}
95
96/// Runs `f` with a reference to the currently-active heap, or `None` if no
97/// `HeapGuard` is in scope.
98///
99/// The heap reference is deliberately scoped to the closure. This avoids the
100/// previous `current_heap() -> Option<&'static Heap>` lifetime lie while still
101/// supporting legacy `GcRef::new` call sites that do not receive `&mut LuaState`.
102pub fn with_current_heap<R>(f: impl for<'a> FnOnce(Option<&'a Heap>) -> R) -> R {
103    CURRENT_HEAP_STACK.with(|stack| {
104        let ptr = stack.borrow().last().copied();
105        // SAFETY: the top NonNull was produced from a live `&Heap` whose
106        // lifetime is bounded by the corresponding `HeapGuard`. The reference
107        // is only handed to `f`, and cannot escape through the return type.
108        let heap = ptr.map(|ptr| unsafe { &*ptr.as_ptr() });
109        f(heap)
110    })
111}
112
113#[derive(Copy, Clone, Debug)]
114pub struct HeapRef {
115    ptr: NonNull<Heap>,
116}
117
118impl HeapRef {
119    pub fn from_heap(heap: &Heap) -> Self {
120        HeapRef {
121            ptr: NonNull::from(heap),
122        }
123    }
124
125    pub fn contains_allocation(self, identity: usize, token: usize) -> bool {
126        // SAFETY: `HeapRef` is created only from a live `&Heap`. Runtime-owned
127        // weak handles store it inside `GlobalState`, whose heap field outlives
128        // those handles. The method only traverses heap metadata and never
129        // dereferences the weak target pointer.
130        unsafe { self.ptr.as_ref() }.contains_allocation(identity, token)
131    }
132}
133
134/// A traced color in the tri-color invariant.
135#[derive(Copy, Clone, PartialEq, Eq, Debug)]
136pub enum Color {
137    /// Not yet visited in the current cycle. The collector alternates between
138    /// two white bits so allocations made during sweep are not collected by
139    /// the cycle already in progress.
140    White0,
141    /// Alternate white bit.
142    White1,
143    /// Visited; outgoing references not yet traced.
144    Gray,
145    /// Fully traced; no outgoing pointers to white objects.
146    Black,
147}
148
149impl Color {
150    pub fn is_white(self) -> bool {
151        matches!(self, Color::White0 | Color::White1)
152    }
153
154    fn other_white(self) -> Self {
155        match self {
156            Color::White0 => Color::White1,
157            Color::White1 => Color::White0,
158            Color::Gray | Color::Black => self,
159        }
160    }
161}
162
163/// Object age used by Lua's generational collector.
164///
165/// Mirrors `G_NEW` through `G_TOUCHED2` in `lgc.h`.
166#[derive(Copy, Clone, PartialEq, Eq, Debug)]
167pub enum GcAge {
168    New,
169    Survival,
170    Old0,
171    Old1,
172    Old,
173    Touched1,
174    Touched2,
175}
176
177impl GcAge {
178    pub fn is_old(self) -> bool {
179        !matches!(self, GcAge::New | GcAge::Survival)
180    }
181
182    fn next_after_minor(self) -> Self {
183        match self {
184            GcAge::New => GcAge::Survival,
185            GcAge::Survival | GcAge::Old0 => GcAge::Old1,
186            GcAge::Old1 | GcAge::Old | GcAge::Touched2 => GcAge::Old,
187            GcAge::Touched1 => GcAge::Touched2,
188        }
189    }
190}
191
192/// Per-object GC metadata. Lives at the start of every `GcBox`.
193#[repr(C)]
194pub struct GcHeader {
195    color: Cell<Color>,
196    age: Cell<GcAge>,
197    /// Set true after this object's finalizer (`__gc` metamethod) has run.
198    /// Phase D defers finalizers; this is reserved.
199    finalized: Cell<bool>,
200    /// True iff this box is linked into a heap's allgc chain, so it will be
201    /// swept and its `size` refunded. `new_uncollected` boxes leave this
202    /// false: they never join a chain, are never swept, and so must never
203    /// have buffer bytes charged against the pacer (the charge would never be
204    /// refunded). [`Gc::account_buffer`] is a no-op when this is false.
205    ///
206    /// Kept separate from `size`: `collected` controls whether buffer charges
207    /// are refundable; `size` remains the exact byte count refunded by sweep.
208    collected: Cell<bool>,
209    /// Intrusive link into the heap's allgc chain.
210    next: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
211    /// Rough byte size charged to the pacer for this object. Starts at the
212    /// `GcBox<T>` size and is adjusted in place by [`Gc::account_buffer`] when
213    /// the value's owned heap buffers (table array/node Vecs) grow or shrink.
214    /// Invariant: this is always exactly the amount sweep will refund to the
215    /// heap's byte counter when this object is freed.
216    size: Cell<usize>,
217    /// Concrete Rust type name captured at allocation for testC/diagnostic
218    /// telemetry. Collector behavior must not branch on this field.
219    type_name: &'static str,
220}
221
222impl GcHeader {
223    fn new_white(size: usize, color: Color, type_name: &'static str) -> Self {
224        Self {
225            color: Cell::new(color),
226            age: Cell::new(GcAge::New),
227            finalized: Cell::new(false),
228            collected: Cell::new(false),
229            next: Cell::new(None),
230            size: Cell::new(size),
231            type_name,
232        }
233    }
234}
235
236/// A heap-allocated, GC-tracked value plus its header.
237#[repr(C)]
238pub struct GcBox<T: ?Sized> {
239    header: GcHeader,
240    value: T,
241}
242
243impl<T: ?Sized> GcBox<T> {
244    pub fn header(&self) -> &GcHeader {
245        &self.header
246    }
247    pub fn value(&self) -> &T {
248        &self.value
249    }
250}
251
252/// A GC-managed pointer. Copy + Clone (one-machine-word). Replaces `GcRef<T>`.
253pub struct Gc<T: ?Sized> {
254    ptr: NonNull<GcBox<T>>,
255    /// Marker so `Gc<T>` is treated as if it owns `T` for variance.
256    _marker: PhantomData<T>,
257}
258
259// SAFETY: Gc is just a pointer. The Cell-based interior mutability of the
260// header is single-threaded (the entire Lua runtime is single-threaded), so
261// no Send/Sync impls are needed and we don't provide them.
262impl<T: ?Sized> Copy for Gc<T> {}
263impl<T: ?Sized> Clone for Gc<T> {
264    fn clone(&self) -> Self {
265        *self
266    }
267}
268
269impl<T: ?Sized> PartialEq for Gc<T> {
270    fn eq(&self, other: &Self) -> bool {
271        std::ptr::addr_eq(self.ptr.as_ptr(), other.ptr.as_ptr())
272    }
273}
274impl<T: ?Sized> Eq for Gc<T> {}
275
276impl<T: ?Sized> std::hash::Hash for Gc<T> {
277    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
278        self.ptr.as_ptr().hash(state)
279    }
280}
281
282impl<T: ?Sized> std::fmt::Debug for Gc<T> {
283    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284        write!(f, "Gc({:p})", self.ptr.as_ptr())
285    }
286}
287
288impl<T: Trace + 'static> Gc<T> {
289    /// Allocate a `GcBox<T>` outside any heap registry. Used by legacy
290    /// `GcRef::new` call sites until Phase D-1b migrates them. The returned
291    /// `Gc<T>` is reachable only through the caller's own retention path;
292    /// without joining a heap's allgc chain, it will never be swept (so
293    /// effectively leaks until process exit — same as Rc behavior).
294    pub fn new_uncollected(value: T) -> Self {
295        let size = std::mem::size_of::<T>();
296        let boxed = Box::new(GcBox {
297            header: GcHeader::new_white(size, Color::White0, std::any::type_name::<T>()),
298            value,
299        });
300        Gc {
301            ptr: NonNull::new(Box::into_raw(boxed)).expect("Box::into_raw is non-null"),
302            _marker: PhantomData,
303        }
304    }
305}
306
307impl<T: ?Sized> Gc<T> {
308    /// Two `Gc<T>`s are identity-equal iff they point at the same box.
309    pub fn ptr_eq(a: Self, b: Self) -> bool {
310        std::ptr::addr_eq(a.ptr.as_ptr(), b.ptr.as_ptr())
311    }
312
313    /// Identity as a usize — usable as a hash table key for "is the *same
314    /// object*" lookups.
315    pub fn identity(self) -> usize {
316        self.ptr.as_ptr() as *const () as usize
317    }
318
319    /// Access the underlying value. Returns `&T` so callers can read fields
320    /// without taking the `Gc` apart. Interior mutability lives inside T's
321    /// own fields (Cell, RefCell, etc.).
322    fn as_box(&self) -> &GcBox<T> {
323        // SAFETY: A Gc<T> is constructed only by allocate() or
324        // new_uncollected(), both of which produce a valid GcBox. The box
325        // outlives the Gc until sweep, which only frees boxes not reachable
326        // from any root — so as long as this Gc is on the stack or in a
327        // traced field, the box is live.
328        unsafe { self.ptr.as_ref() }
329    }
330
331    fn header(&self) -> &GcHeader {
332        &self.as_box().header
333    }
334
335    pub fn color(self) -> Color {
336        self.header().color.get()
337    }
338
339    pub fn set_color(self, color: Color) {
340        self.header().color.set(color);
341    }
342
343    pub fn age(self) -> GcAge {
344        self.header().age.get()
345    }
346
347    pub fn set_age(self, age: GcAge) {
348        self.header().age.set(age);
349    }
350
351    /// Charge (`delta > 0`) or refund (`delta < 0`) `delta` bytes of this
352    /// object's owned heap buffers against the pacer, keeping `header.size`
353    /// as the single source of truth for what sweep will refund.
354    ///
355    /// No-op when `delta == 0` or when this box is not on a heap allgc chain
356    /// (`collected == false`): an uncollected box is never swept, so charging
357    /// it would permanently inflate the byte counter. On the collected path,
358    /// `header.size` and the heap's byte counter move together, so after sweep
359    /// frees this box it refunds exactly the bytes that were charged here.
360    pub fn account_buffer(&self, heap: &Heap, delta: isize) {
361        if delta == 0 {
362            return;
363        }
364        let header = self.header();
365        if !header.collected.get() {
366            return;
367        }
368        if delta >= 0 {
369            header.size.set(header.size.get().saturating_add(delta as usize));
370        } else {
371            header
372                .size
373                .set(header.size.get().saturating_sub((-delta) as usize));
374        }
375        heap.adjust_bytes(delta);
376    }
377}
378
379impl<T: ?Sized> std::ops::Deref for Gc<T> {
380    type Target = T;
381    fn deref(&self) -> &T {
382        &self.as_box().value
383    }
384}
385
386impl<T: ?Sized> AsRef<T> for Gc<T> {
387    fn as_ref(&self) -> &T {
388        &self.as_box().value
389    }
390}
391
392/// Every GC-rooted type implements `Trace` to expose its `Gc<_>` fields.
393///
394/// The `trace` method visits every reachable `Gc<_>` and calls
395/// `Marker::mark` on it. For container fields (`Vec<LuaValue>`, etc.) call
396/// `field.trace(m)` to delegate.
397///
398/// # Mechanical pattern
399///
400/// ```ignore
401/// impl Trace for LuaTable {
402///     fn trace(&self, m: &mut Marker) {
403///         for v in self.array.iter() { v.trace(m); }
404///         if let Some(mt) = self.metatable { m.mark(mt); }
405///     }
406/// }
407/// ```
408pub trait Trace {
409    fn trace(&self, m: &mut Marker);
410}
411
412// Common blanket impls so most container types Just Work.
413impl<T: Trace> Trace for Vec<T> {
414    fn trace(&self, m: &mut Marker) {
415        for item in self.iter() {
416            item.trace(m);
417        }
418    }
419}
420
421impl<T: Trace> Trace for Option<T> {
422    fn trace(&self, m: &mut Marker) {
423        if let Some(v) = self {
424            v.trace(m);
425        }
426    }
427}
428
429impl<T: Trace + ?Sized> Trace for Box<T> {
430    fn trace(&self, m: &mut Marker) {
431        (**self).trace(m);
432    }
433}
434
435impl<T: Trace + ?Sized> Trace for std::rc::Rc<T> {
436    fn trace(&self, m: &mut Marker) {
437        (**self).trace(m);
438    }
439}
440
441impl<T: Trace> Trace for RefCell<T> {
442    fn trace(&self, m: &mut Marker) {
443        self.borrow().trace(m);
444    }
445}
446
447/// `Gc<T>` is itself traceable: marking it forwards to the contained `T`.
448impl<T: Trace + 'static> Trace for Gc<T> {
449    fn trace(&self, m: &mut Marker) {
450        m.mark(*self);
451    }
452}
453
454// Trivially-traceable primitive types: visiting does nothing.
455macro_rules! trace_noop {
456    ($($t:ty),*) => {
457        $(impl Trace for $t {
458            fn trace(&self, _m: &mut Marker) {}
459        })*
460    };
461}
462trace_noop!(
463    bool, u8, u16, u32, u64, u128, usize,
464    i8, i16, i32, i64, i128, isize,
465    f32, f64, char, String, str
466);
467
468impl<T> Trace for std::marker::PhantomData<T> {
469    fn trace(&self, _m: &mut Marker) {}
470}
471
472/// Holds the gray queue during a mark phase. Passed to `Trace::trace`.
473pub struct Marker {
474    gray_queue: Vec<NonNull<GcBox<dyn Trace>>>,
475    visited: std::collections::HashSet<usize>,
476}
477
478impl Marker {
479    fn new() -> Self {
480        Self {
481            gray_queue: Vec::with_capacity(256),
482            visited: std::collections::HashSet::new(),
483        }
484    }
485
486    /// Mark a `Gc<T>` as gray (reachable, but its outgoing edges not yet
487    /// traced). Called by `Trace::trace` implementations.
488    ///
489    /// Per-cycle dedup uses `visited` (a HashSet of box identities) rather
490    /// than the color flag. Color-based dedup would silently skip
491    /// `new_uncollected` boxes left Black by the previous cycle — those
492    /// allocations are NOT on the heap's allgc chain, so the start-of-mark
493    /// "reset all allgc to White" loop does not reach them, and a Black
494    /// uncollected box would be skipped without re-tracing its children
495    /// (causing reachable allgc descendants to be swept). The visited set
496    /// is rebuilt every `full_collect` (Marker::new), so this dedup is
497    /// always per-cycle.
498    pub fn mark<T: Trace + 'static>(&mut self, gc: Gc<T>) {
499        let id = gc.identity();
500        if self.visited.insert(id) {
501            let header = gc.header();
502            header.color.set(Color::Gray);
503            let ptr: NonNull<GcBox<dyn Trace>> = gc.ptr;
504            self.gray_queue.push(ptr);
505        }
506    }
507
508    /// Record that an Rc-backed object (`GcRef<T>` in Phase A-D-0) has been
509    /// visited and return whether this is the first visit. Used by recursive
510    /// `Trace` impls to break cycles while the real `Gc<T>` gray-queue path is
511    /// not yet wired (e.g. `_G._G == _G` would otherwise infinitely recurse).
512    pub fn try_visit(&mut self, addr: usize) -> bool {
513        self.visited.insert(addr)
514    }
515
516    /// True iff `id` was reached during the mark phase. Used by the
517    /// post-mark hook (`Heap::full_collect_with_post_mark`) to decide whether
518    /// a weak-table entry's target is still strongly reachable. Read-only —
519    /// callers cannot add entries.
520    pub fn is_visited(&self, id: usize) -> bool {
521        self.visited.contains(&id)
522    }
523
524    /// Number of objects marked so far. Used by the post-mark hook's
525    /// ephemeron-convergence fixed-point loop to detect when an iteration
526    /// added no new reachable objects and the loop can terminate.
527    pub fn visited_count(&self) -> usize {
528        self.visited.len()
529    }
530
531    /// Drain the gray queue, transitively marking children. Each gray box
532    /// becomes black; its `Trace::trace` is called so the children it points
533    /// at get pushed onto the queue. Repeats until the queue is empty.
534    ///
535    /// Exposed for the post-mark hook so it can run ephemeron convergence:
536    /// after marking new values via [`Marker::mark`] (or `value.trace(self)`),
537    /// the hook calls `drain_gray_queue` to propagate the new reachability,
538    /// then re-checks for fixed-point.
539    pub fn drain_gray_queue(&mut self) {
540        while let Some(gray_ptr) = self.gray_queue.pop() {
541            unsafe {
542                let bx = gray_ptr.as_ref();
543                bx.header.color.set(Color::Black);
544                bx.value.trace(self);
545            }
546        }
547    }
548}
549
550/// Phases of the incremental collection cycle.
551///
552/// The state machine matches a simplified subset of C-Lua's `lgc.c` FSM and
553/// is driven by [`Heap::incremental_step_with_post_mark`].
554///
555/// Transitions:
556/// - `Pause` → `Propagate` (on first step: reset colors, trace roots).
557/// - `Propagate` → `Atomic` (when the gray queue empties).
558/// - `Atomic` → `Sweep` (post-mark hook has run; sweep cursor is initialized).
559/// - `Sweep` → `Finalize` (sweep cursor reached the end of allgc).
560/// - `Finalize` → `Pause` (any pending finalize work has been drained).
561///
562/// `Collecting` is kept as a compatibility alias for the old API (used by
563/// `barrier`) — it means "anything but Pause."
564#[derive(Copy, Clone, PartialEq, Eq, Debug)]
565pub enum GcState {
566    Pause,
567    Propagate,
568    Atomic,
569    Sweep,
570    Finalize,
571}
572
573impl GcState {
574    pub fn is_pause(self) -> bool {
575        matches!(self, GcState::Pause)
576    }
577    pub fn is_propagate(self) -> bool {
578        matches!(self, GcState::Propagate)
579    }
580    pub fn is_sweep(self) -> bool {
581        matches!(self, GcState::Sweep)
582    }
583}
584
585/// Outcome of one call to [`Heap::incremental_step_with_post_mark`].
586#[derive(Copy, Clone, PartialEq, Eq, Debug)]
587pub enum StepOutcome {
588    /// The step finished a cycle. The heap is back at `GcState::Pause`.
589    Paused,
590    /// The step performed work but the cycle is not finished. Caller may
591    /// step again.
592    InProgress,
593    /// The heap is paused (via [`Heap::pause`]) or the caller asked for zero
594    /// budget while no cycle was in progress and no work was needed.
595    SkippedStopped,
596}
597
598/// Work budget for one incremental step.
599///
600/// `remaining_work` counts down by one for each unit of work performed (one
601/// gray object traced, one swept node visited, one finalizer dispatched).
602/// `max_credit` caps how negative `remaining_work` may be allowed to go — a
603/// step that overshoots its budget rolls the overrun into the caller's debt
604/// rather than letting unbounded work happen in one call.
605#[derive(Copy, Clone, Debug)]
606pub struct StepBudget {
607    pub remaining_work: isize,
608    pub max_credit: isize,
609}
610
611impl StepBudget {
612    /// Build a budget from a number of allowed work units.
613    pub fn from_work(work: isize) -> Self {
614        Self { remaining_work: work.max(1), max_credit: work.max(1) }
615    }
616}
617
618/// The heap. One per `GlobalState`. Owns the intrusive allgc list of every
619/// allocated `GcBox`, tracks total bytes, and runs collections.
620/// Floor for the post-collection threshold. Without it, a tight
621/// allocation loop drives the live set near zero, so `bytes * pause/100`
622/// collapses toward zero and a full stop-the-world collection fires every
623/// few allocations, re-tracing all roots each time (issue #38, GC path).
624/// The floor bounds the wasted work: the collector waits until at least
625/// this many bytes of garbage accumulate before collecting a small heap.
626///
627/// Raised from 256 KB to 1 MB once table array/node buffer bytes became
628/// honestly accounted (see [`Gc::account_buffer`]): with real buffer bytes
629/// flowing into the pacer, a 256 KB floor fires too eagerly on table-heavy
630/// workloads, re-tracing all roots each time. 1 MB keeps the small-heap
631/// over-collection guard while letting honest pressure drive the threshold.
632const GC_MIN_THRESHOLD: usize = 1024 * 1024;
633
634pub struct Heap {
635    /// Head of the singly-linked allgc list (every live `GcBox`).
636    head: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
637    /// Total bytes allocated (sum of header sizes; rough).
638    bytes: Cell<usize>,
639    /// White bit used for new allocations and for survivors after a sweep.
640    current_white: Cell<Color>,
641    /// Heap-owned allocation tokens keyed by box address. Weak handles store
642    /// these tokens so address reuse after sweep cannot resurrect a stale weak
643    /// target.
644    allocation_tokens: RefCell<HashMap<usize, usize>>,
645    /// Next non-zero token for a collected allocation.
646    next_allocation_token: Cell<usize>,
647    /// Threshold above which `step` triggers a collection.
648    threshold: Cell<usize>,
649    /// Multiplier on bytes_used to set next threshold after collection.
650    pause_multiplier: Cell<usize>,
651    /// State machine for the incremental collector.
652    state: Cell<GcState>,
653    /// If true, `step` and `barrier` are no-ops (for bootstrap before the
654    /// world is consistent).
655    paused: Cell<bool>,
656    /// Counter of full collections performed (for diagnostics).
657    collections: Cell<usize>,
658    /// In-progress marker state for incremental cycles. `Some` between
659    /// `Propagate` start and `Sweep` start; `None` otherwise.
660    marker: RefCell<Option<Marker>>,
661    /// Sweep cursor. Points at the `Cell` whose `Option<NonNull>` is the
662    /// "current" link being inspected during the sweep phase. Encoded as a
663    /// raw pointer because the cell lives inside a `GcHeader` (Cell, not Cell<Cell>).
664    /// `None` means: sweep starts from `self.head`.
665    sweep_prev_next: Cell<Option<NonNull<Cell<Option<NonNull<GcBox<dyn Trace>>>>>>>,
666}
667
668impl Default for Heap {
669    fn default() -> Self {
670        Self::new()
671    }
672}
673
674impl Heap {
675    pub fn new() -> Self {
676        Self {
677            head: Cell::new(None),
678            bytes: Cell::new(0),
679            current_white: Cell::new(Color::White0),
680            allocation_tokens: RefCell::new(HashMap::new()),
681            next_allocation_token: Cell::new(1),
682            threshold: Cell::new(64 * 1024), // initial threshold: 64 KB
683            pause_multiplier: Cell::new(200), // 200% = collect when bytes 2x threshold
684            state: Cell::new(GcState::Pause),
685            paused: Cell::new(true), // start paused; caller enables when world is consistent
686            collections: Cell::new(0),
687            marker: RefCell::new(None),
688            sweep_prev_next: Cell::new(None),
689        }
690    }
691
692    /// Enable collection. Until this is called, `step` is a no-op (so the
693    /// runtime can bootstrap without prematurely freeing objects).
694    pub fn unpause(&self) {
695        self.paused.set(false);
696    }
697
698    pub fn is_paused(&self) -> bool {
699        self.paused.get()
700    }
701
702    /// Allocate a new `GcBox<T>` and prepend it to the allgc chain.
703    pub fn allocate<T: Trace + 'static>(&self, value: T) -> Gc<T> {
704        let size = std::mem::size_of::<GcBox<T>>();
705        let boxed = Box::new(GcBox {
706            header: GcHeader::new_white(
707                size,
708                self.current_white.get(),
709                std::any::type_name::<T>(),
710            ),
711            value,
712        });
713        boxed.header.next.set(self.head.get());
714        boxed.header.collected.set(true);
715        let raw: *mut GcBox<T> = Box::into_raw(boxed);
716        let ptr: NonNull<GcBox<T>> =
717            NonNull::new(raw).expect("Box::into_raw is non-null");
718        let dyn_ptr: NonNull<GcBox<dyn Trace>> = ptr;
719        let identity = ptr.as_ptr() as *const () as usize;
720        let token = self.next_token();
721        self.allocation_tokens
722            .borrow_mut()
723            .insert(identity, token);
724        self.head.set(Some(dyn_ptr));
725        self.bytes.set(self.bytes.get() + size);
726        Gc {
727            ptr,
728            _marker: PhantomData,
729        }
730    }
731
732    /// Bytes currently retained by GC-tracked objects (rough estimate).
733    pub fn bytes_used(&self) -> usize {
734        self.bytes.get()
735    }
736
737    /// Adjust the heap's pacer byte counter by a signed delta, saturating at
738    /// zero. Used by [`Gc::account_buffer`] to charge or refund the bytes of
739    /// an object's owned heap buffers (table array/node Vecs) so collections
740    /// fire at honest memory pressure rather than only on header sizes.
741    pub fn adjust_bytes(&self, delta: isize) {
742        if delta >= 0 {
743            self.bytes.set(self.bytes.get().saturating_add(delta as usize));
744        } else {
745            self.bytes
746                .set(self.bytes.get().saturating_sub((-delta) as usize));
747        }
748    }
749
750    /// Current collection threshold in bytes. When `bytes_used() >= threshold_bytes()`,
751    /// the next `step()` will run a full collection (unless paused). Used by
752    /// callers that want to short-circuit expensive prep work (e.g. snapshotting
753    /// weak tables / pending finalizers) when no collection will actually fire.
754    pub fn threshold_bytes(&self) -> usize {
755        self.threshold.get()
756    }
757
758    /// Override the next automatic collection threshold.
759    ///
760    /// The VM uses this when Lua-level GC pacing (`GCdebt`, minor-debt, and
761    /// pause-debt calculations) has already computed a byte threshold from the
762    /// collector-owned live-byte counter.
763    pub fn set_threshold_bytes(&self, threshold: usize) {
764        self.threshold.set(threshold.max(1));
765    }
766
767    /// Cheap predicate: would a `step()` actually do work? Equivalent to
768    /// `!paused && bytes_used() >= threshold_bytes()`. Callers that build
769    /// snapshot state before invoking the heap should gate on this.
770    pub fn would_collect(&self) -> bool {
771        !self.paused.get() && self.bytes.get() >= self.threshold.get()
772    }
773
774    pub fn collections(&self) -> usize {
775        self.collections.get()
776    }
777
778    fn next_token(&self) -> usize {
779        let token = self.next_allocation_token.get().max(1);
780        let next = token.checked_add(1).unwrap_or(1).max(1);
781        self.next_allocation_token.set(next);
782        token
783    }
784
785    fn current_white(&self) -> Color {
786        self.current_white.get()
787    }
788
789    fn other_white(&self) -> Color {
790        self.current_white.get().other_white()
791    }
792
793    fn flip_current_white(&self) {
794        self.current_white.set(self.other_white());
795    }
796
797    fn for_each_header(&self, mut f: impl FnMut(&GcHeader)) {
798        let mut cursor = self.head.get();
799        while let Some(ptr) = cursor {
800            let header = self.header_from_ptr(ptr);
801            cursor = header.next.get();
802            f(header);
803        }
804    }
805
806    fn header_from_ptr<'a>(&'a self, ptr: NonNull<GcBox<dyn Trace>>) -> &'a GcHeader {
807        unsafe { &(*ptr.as_ptr()).header }
808    }
809
810    /// Return the current heap token for a live allocation identity.
811    ///
812    /// Weak handles use this before sweep to capture their target allocation
813    /// without dereferencing stale pointers later.
814    pub fn allocation_token(&self, identity: usize) -> Option<usize> {
815        self.allocation_tokens.borrow().get(&identity).copied()
816    }
817
818    /// Return true when `identity` still names the same heap allocation.
819    ///
820    /// The token check prevents allocator address reuse from making a stale
821    /// weak handle look live again.
822    pub fn contains_allocation(&self, identity: usize, token: usize) -> bool {
823        self.allocation_token(identity) == Some(token)
824    }
825
826    /// Forward write barrier: invoked when `parent` (already-traced black
827    /// object) gains a new reference to `child`. To preserve the tri-color
828    /// invariant ("no black points to white"), we mark the child gray
829    /// immediately. Cheap: one branch + maybe one queue push.
830    ///
831    /// During incremental mode this prevents the marking phase from missing
832    /// the new edge. In current stop-the-world mode it's still correct (a
833    /// no-op when the collection is idle), so call sites can be wired now
834    /// and the incremental upgrade is mechanical later.
835    pub fn barrier<P, C>(&self, parent: Gc<P>, child: Gc<C>)
836    where
837        P: Trace + 'static,
838        C: Trace + 'static,
839    {
840        if self.paused.get() || self.state.get().is_pause() {
841            return;
842        }
843        if parent.header().color.get() != Color::Black {
844            return;
845        }
846        if !child.header().color.get().is_white() {
847            return;
848        }
849        child.header().color.set(Color::Gray);
850        if let Ok(mut m_opt) = self.marker.try_borrow_mut() {
851            if let Some(m) = m_opt.as_mut() {
852                let ptr: NonNull<GcBox<dyn Trace>> = child.ptr;
853                m.gray_queue.push(ptr);
854                m.visited.insert(child.identity());
855            }
856        }
857    }
858
859    /// Generational forward barrier: if an old object receives a reference to a
860    /// young object, the child cannot jump directly to OLD because it may still
861    /// point at younger objects. Lua marks it OLD0 so later young collections
862    /// advance it through OLD1 to OLD.
863    pub fn generational_forward_barrier<P, C>(&self, parent: Gc<P>, child: Gc<C>)
864    where
865        P: Trace + 'static,
866        C: Trace + 'static,
867    {
868        if parent.age().is_old() && !child.age().is_old() {
869            child.set_age(GcAge::Old0);
870        }
871        self.barrier(parent, child);
872    }
873
874    /// Generational backward barrier: an old object that now points to a young
875    /// object is revisited by the next young collection. This mirrors
876    /// `luaC_barrierback_`'s age transition to TOUCHED1.
877    pub fn generational_backward_barrier<P>(&self, parent: Gc<P>)
878    where
879        P: Trace + 'static,
880    {
881        if parent.age().is_old() {
882            parent.set_color(Color::Gray);
883            parent.set_age(GcAge::Touched1);
884        }
885    }
886
887    /// Possibly run a collection. Trigger: bytes_used > threshold.
888    /// Caller passes the root set (the runtime — typically `GlobalState`
889    /// implementing `Trace`).
890    pub fn step(&self, roots: &dyn Trace) {
891        self.step_with_post_mark(roots, |_: &mut Marker| {});
892    }
893
894    /// Like [`step`] but invokes a `post_mark` hook when a collection
895    /// actually fires (threshold reached). Hook is a no-op on the
896    /// short-circuit path. The runtime uses this to bridge weak-table
897    /// pruning into implicit GC steps fired from inside the VM loop.
898    pub fn step_with_post_mark<F: FnMut(&mut Marker)>(
899        &self,
900        roots: &dyn Trace,
901        post_mark: F,
902    ) {
903        if self.paused.get() {
904            return;
905        }
906        if self.bytes.get() < self.threshold.get() {
907            return;
908        }
909        self.full_collect_with_post_mark(roots, post_mark);
910    }
911
912    /// Stop-the-world full collect. Marks every reachable object from
913    /// `roots`, then sweeps white (unreachable) boxes from the allgc chain.
914    pub fn full_collect(&self, roots: &dyn Trace) {
915        self.full_collect_with_post_mark(roots, |_: &mut Marker| {});
916    }
917
918    /// Run only the mark/atomic hook portion of a collection, without sweeping.
919    ///
920    /// This is used by runtimes that need an atomic reachability snapshot for
921    /// weak-table cleanup while they are deliberately avoiding object freeing.
922    pub fn mark_only_with_post_mark<F: FnMut(&mut Marker)>(
923        &self,
924        roots: &dyn Trace,
925        mut post_mark: F,
926    ) {
927        if self.paused.get() {
928            return;
929        }
930        let mut marker = Marker::new();
931        roots.trace(&mut marker);
932        marker.drain_gray_queue();
933        post_mark(&mut marker);
934        marker.drain_gray_queue();
935    }
936
937    /// Metadata transition used when entering generational mode after a full
938    /// mark: all currently live objects become old.
939    pub fn promote_all_to_old(&self) {
940        self.for_each_header(|header| {
941            header.age.set(GcAge::Old);
942            header.color.set(Color::Black);
943        });
944    }
945
946    /// Metadata transition used when returning to incremental mode: Lua clears
947    /// age information and treats all objects as new again.
948    pub fn reset_all_ages(&self) {
949        let current_white = self.current_white();
950        self.for_each_header(|header| {
951            header.age.set(GcAge::New);
952            header.color.set(current_white);
953        });
954    }
955
956    /// Run a complete young-generation collection.
957    ///
958    /// This is the first generational path: it uses the normal root tracer for
959    /// correctness, then limits sweep/freeing to young objects. Later work can
960    /// replace the full root traversal with cohort-list traversal without
961    /// changing the age/sweep contract introduced here.
962    pub fn minor_collect_with_post_mark<F: FnMut(&mut Marker)>(
963        &self,
964        roots: &dyn Trace,
965        mut post_mark: F,
966    ) {
967        if self.paused.get() {
968            return;
969        }
970        if !self.state.get().is_pause() {
971            self.abort_cycle();
972        }
973
974        self.state.set(GcState::Propagate);
975        let mut marker = Marker::new();
976        roots.trace(&mut marker);
977        marker.drain_gray_queue();
978
979        self.state.set(GcState::Atomic);
980        post_mark(&mut marker);
981        marker.drain_gray_queue();
982
983        self.state.set(GcState::Sweep);
984        self.sweep_young();
985        *self.marker.borrow_mut() = None;
986        self.sweep_prev_next.set(None);
987        self.state.set(GcState::Pause);
988        self.collections.set(self.collections.get() + 1);
989    }
990
991    /// Stop-the-world full collect with a post-mark hook.
992    ///
993    /// Internally drives the incremental state machine to completion with
994    /// an unbounded budget — equivalent to repeatedly calling
995    /// [`incremental_step_with_post_mark`] until it returns `Paused`. The
996    /// post-mark hook is invoked exactly once, during the atomic transition.
997    pub fn full_collect_with_post_mark<F: FnMut(&mut Marker)>(
998        &self,
999        roots: &dyn Trace,
1000        mut post_mark: F,
1001    ) {
1002        if self.paused.get() {
1003            return;
1004        }
1005        if !self.state.get().is_pause() {
1006            self.abort_cycle();
1007        }
1008        let unlimited = StepBudget { remaining_work: isize::MAX, max_credit: isize::MAX };
1009        loop {
1010            let outcome = self.incremental_step_with_post_mark(roots, unlimited, &mut post_mark);
1011            if matches!(outcome, StepOutcome::Paused | StepOutcome::SkippedStopped) {
1012                break;
1013            }
1014        }
1015    }
1016
1017    /// Run one budgeted step of the incremental collector.
1018    ///
1019    /// The state machine advances `Pause → Propagate → Atomic → Sweep →
1020    /// Finalize → Pause`. Each phase consumes budget; the call returns when
1021    /// the budget runs out or the cycle reaches `Pause`. The `post_mark`
1022    /// hook is invoked exactly once per cycle, during the `Atomic`
1023    /// transition (after the initial gray-queue drain, before sweep starts).
1024    ///
1025    /// Returns:
1026    /// - [`StepOutcome::Paused`] — the cycle completed.
1027    /// - [`StepOutcome::InProgress`] — budget exhausted before the cycle
1028    ///   finished; caller may step again.
1029    /// - [`StepOutcome::SkippedStopped`] — heap is paused; nothing happened.
1030    pub fn incremental_step_with_post_mark<F: FnMut(&mut Marker)>(
1031        &self,
1032        roots: &dyn Trace,
1033        mut budget: StepBudget,
1034        mut post_mark: F,
1035    ) -> StepOutcome {
1036        if self.paused.get() {
1037            return StepOutcome::SkippedStopped;
1038        }
1039        self.run_budgeted(roots, &mut budget, &mut post_mark);
1040        if self.state.get().is_pause() {
1041            StepOutcome::Paused
1042        } else {
1043            StepOutcome::InProgress
1044        }
1045    }
1046
1047    fn run_budgeted(
1048        &self,
1049        roots: &dyn Trace,
1050        budget: &mut StepBudget,
1051        post_mark: &mut dyn FnMut(&mut Marker),
1052    ) -> bool {
1053        self.run_budgeted_until(roots, budget, post_mark, None)
1054    }
1055
1056    fn run_budgeted_until(
1057        &self,
1058        roots: &dyn Trace,
1059        budget: &mut StepBudget,
1060        post_mark: &mut dyn FnMut(&mut Marker),
1061        stop_at: Option<GcState>,
1062    ) -> bool {
1063        let mut did_work = false;
1064        loop {
1065            if stop_at == Some(self.state.get()) {
1066                return did_work;
1067            }
1068            if budget.remaining_work <= -budget.max_credit {
1069                return did_work;
1070            }
1071            match self.state.get() {
1072                GcState::Pause => {
1073                    self.start_cycle(roots);
1074                    self.state.set(GcState::Propagate);
1075                    budget.remaining_work -= 1;
1076                    did_work = true;
1077                    if stop_at == Some(GcState::Propagate) {
1078                        return did_work;
1079                    }
1080                }
1081                GcState::Propagate => {
1082                    let work = self.drain_gray_budgeted(budget.remaining_work.max(1));
1083                    budget.remaining_work -= work as isize;
1084                    did_work = did_work || work > 0;
1085                    let empty = {
1086                        let m = self.marker.borrow();
1087                        m.as_ref().map(|m| m.gray_queue.is_empty()).unwrap_or(true)
1088                    };
1089                    if empty {
1090                        self.state.set(GcState::Atomic);
1091                        if stop_at == Some(GcState::Atomic) {
1092                            return did_work;
1093                        }
1094                    } else if budget.remaining_work <= 0 {
1095                        return did_work;
1096                    }
1097                }
1098                GcState::Atomic => {
1099                    self.run_atomic(post_mark);
1100                    self.state.set(GcState::Sweep);
1101                    budget.remaining_work -= 1;
1102                    did_work = true;
1103                    if stop_at == Some(GcState::Sweep) {
1104                        return did_work;
1105                    }
1106                }
1107                GcState::Sweep => {
1108                    let work = self.sweep_budgeted(budget.remaining_work.max(1));
1109                    budget.remaining_work -= work as isize;
1110                    did_work = did_work || work > 0;
1111                    if self.sweep_prev_next.get().is_none() {
1112                        self.state.set(GcState::Finalize);
1113                        if stop_at == Some(GcState::Finalize) {
1114                            return did_work;
1115                        }
1116                    } else if budget.remaining_work <= 0 {
1117                        return did_work;
1118                    }
1119                }
1120                GcState::Finalize => {
1121                    self.finish_cycle();
1122                    self.state.set(GcState::Pause);
1123                    if stop_at == Some(GcState::Pause) {
1124                        return did_work;
1125                    }
1126                    return did_work;
1127                }
1128            }
1129        }
1130    }
1131
1132    /// Drive an incremental cycle until `target` is entered, stopping before any
1133    /// subsequent phase work. Intended for testC-style inspection of mid-cycle
1134    /// color/barrier invariants; normal collector pacing uses
1135    /// [`Self::incremental_step_with_post_mark`].
1136    pub fn incremental_run_until_state_with_post_mark<F: FnMut(&mut Marker)>(
1137        &self,
1138        roots: &dyn Trace,
1139        target: GcState,
1140        max_work: isize,
1141        mut post_mark: F,
1142    ) -> StepOutcome {
1143        if self.paused.get() {
1144            return StepOutcome::SkippedStopped;
1145        }
1146        let work = max_work.max(1);
1147        let mut budget = StepBudget {
1148            remaining_work: work,
1149            max_credit: work,
1150        };
1151        self.run_budgeted_until(roots, &mut budget, &mut post_mark, Some(target));
1152        if self.state.get().is_pause() {
1153            StepOutcome::Paused
1154        } else {
1155            StepOutcome::InProgress
1156        }
1157    }
1158
1159    fn start_cycle(&self, roots: &dyn Trace) {
1160        self.flip_current_white();
1161        let mut marker = Marker::new();
1162        roots.trace(&mut marker);
1163        *self.marker.borrow_mut() = Some(marker);
1164        self.sweep_prev_next.set(None);
1165    }
1166
1167    fn drain_gray_budgeted(&self, max_units: isize) -> usize {
1168        let mut m_opt = self.marker.borrow_mut();
1169        let marker = match m_opt.as_mut() {
1170            Some(m) => m,
1171            None => return 0,
1172        };
1173        let mut work = 0usize;
1174        let mut budget = max_units;
1175        while budget > 0 {
1176            let next = match marker.gray_queue.pop() {
1177                Some(p) => p,
1178                None => break,
1179            };
1180            unsafe {
1181                let bx = next.as_ref();
1182                bx.header.color.set(Color::Black);
1183                bx.value.trace(marker);
1184            }
1185            work += 1;
1186            budget -= 1;
1187        }
1188        work
1189    }
1190
1191    fn run_atomic(&self, post_mark: &mut dyn FnMut(&mut Marker)) {
1192        let mut m_opt = self.marker.borrow_mut();
1193        if let Some(marker) = m_opt.as_mut() {
1194            marker.drain_gray_queue();
1195            post_mark(marker);
1196            marker.drain_gray_queue();
1197        }
1198        self.sweep_prev_next.set(Some(NonNull::from(&self.head)));
1199    }
1200
1201    fn sweep_budgeted(&self, max_units: isize) -> usize {
1202        let mut work = 0usize;
1203        let mut budget = max_units;
1204        let mut freed_bytes = 0usize;
1205        let current_white = self.current_white();
1206        let dead_white = self.other_white();
1207        let mut prev_next_ptr = match self.sweep_prev_next.get() {
1208            Some(p) => p,
1209            None => return 0,
1210        };
1211        while budget > 0 {
1212            let prev_cell = unsafe { prev_next_ptr.as_ref() };
1213            let cursor = prev_cell.get();
1214            let ptr = match cursor {
1215                Some(p) => p,
1216                None => {
1217                    self.sweep_prev_next.set(None);
1218                    break;
1219                }
1220            };
1221            let header = self.header_from_ptr(ptr);
1222            let next = header.next.get();
1223            let color = header.color.get();
1224            if color == dead_white {
1225                prev_cell.set(next);
1226                freed_bytes += header.size.get();
1227                self.allocation_tokens
1228                    .borrow_mut()
1229                    .remove(&(ptr.as_ptr() as *const () as usize));
1230                unsafe {
1231                    let _ = Box::from_raw(ptr.as_ptr());
1232                }
1233            } else {
1234                if matches!(color, Color::Black | Color::Gray) {
1235                    header.color.set(current_white);
1236                }
1237                prev_next_ptr = unsafe { NonNull::from(&(*ptr.as_ptr()).header.next) };
1238                self.sweep_prev_next.set(Some(prev_next_ptr));
1239            }
1240            work += 1;
1241            budget -= 1;
1242        }
1243        if freed_bytes > 0 {
1244            self.bytes.set(self.bytes.get().saturating_sub(freed_bytes));
1245        }
1246        work
1247    }
1248
1249    fn sweep_young(&self) {
1250        let mut freed_bytes = 0usize;
1251        let current_white = self.current_white();
1252        let mut prev_next_ptr = NonNull::from(&self.head);
1253        loop {
1254            let prev_cell = unsafe { prev_next_ptr.as_ref() };
1255            let Some(ptr) = prev_cell.get() else { break; };
1256            let header = self.header_from_ptr(ptr);
1257            let next = header.next.get();
1258            if header.color.get().is_white() && !header.age.get().is_old() {
1259                prev_cell.set(next);
1260                freed_bytes += header.size.get();
1261                self.allocation_tokens
1262                    .borrow_mut()
1263                    .remove(&(ptr.as_ptr() as *const () as usize));
1264                unsafe {
1265                    let _ = Box::from_raw(ptr.as_ptr());
1266                }
1267                continue;
1268            }
1269
1270            if !header.color.get().is_white() {
1271                let age = header.age.get();
1272                header.age.set(age.next_after_minor());
1273                match age {
1274                    GcAge::New => header.color.set(current_white),
1275                    GcAge::Touched1 | GcAge::Touched2 => header.color.set(Color::Black),
1276                    _ => {}
1277                }
1278            }
1279            prev_next_ptr = unsafe { NonNull::from(&(*ptr.as_ptr()).header.next) };
1280        }
1281        if freed_bytes > 0 {
1282            self.bytes.set(self.bytes.get().saturating_sub(freed_bytes));
1283        }
1284    }
1285
1286    fn finish_cycle(&self) {
1287        *self.marker.borrow_mut() = None;
1288        self.sweep_prev_next.set(None);
1289        let next = self
1290            .bytes
1291            .get()
1292            .saturating_mul(self.pause_multiplier.get())
1293            / 100;
1294        self.threshold.set(next.max(GC_MIN_THRESHOLD));
1295        self.collections.set(self.collections.get() + 1);
1296    }
1297
1298    fn abort_cycle(&self) {
1299        if !self.state.get().is_pause() {
1300            *self.marker.borrow_mut() = None;
1301            self.sweep_prev_next.set(None);
1302            let current_white = self.current_white();
1303            self.for_each_header(|header| {
1304                header.color.set(current_white);
1305            });
1306            self.state.set(GcState::Pause);
1307        }
1308    }
1309
1310    /// Returns the current state of the incremental collector.
1311    pub fn gc_state(&self) -> GcState {
1312        self.state.get()
1313    }
1314
1315    /// Approximate number of live GC boxes, computed by walking the allgc
1316    /// chain. Linear in heap size; used for cycle-cost estimation and tests.
1317    pub fn allgc_count(&self) -> usize {
1318        let mut count = 0usize;
1319        self.for_each_header(|_| {
1320            count += 1;
1321        });
1322        count
1323    }
1324
1325    /// Count live allgc objects whose concrete Rust type name matches
1326    /// `predicate`. This is diagnostic/testC telemetry only; collector logic
1327    /// must not depend on Rust type names.
1328    pub fn type_name_count(&self, mut predicate: impl FnMut(&'static str) -> bool) -> usize {
1329        let mut count = 0usize;
1330        self.for_each_header(|header| {
1331            if predicate(header.type_name) {
1332                count += 1;
1333            }
1334        });
1335        count
1336    }
1337
1338    /// Drop every allocation, ignoring reachability. Called at shutdown.
1339    /// After this returns, every outstanding `Gc<T>` is dangling — callers
1340    /// must ensure no `Gc<T>` outlives the `Heap`.
1341    pub fn drop_all(&self) {
1342        *self.marker.borrow_mut() = None;
1343        self.sweep_prev_next.set(None);
1344        self.state.set(GcState::Pause);
1345        let mut cursor = self.head.get();
1346        self.head.set(None);
1347        self.allocation_tokens.borrow_mut().clear();
1348        while let Some(ptr) = cursor {
1349            // SAFETY: same chain invariant as full_collect's sweep.
1350            let next = unsafe {
1351                let next = (*ptr.as_ptr()).header.next.get();
1352                let _ = Box::from_raw(ptr.as_ptr());
1353                next
1354            };
1355            cursor = next;
1356        }
1357        self.bytes.set(0);
1358    }
1359}
1360
1361impl Drop for Heap {
1362    fn drop(&mut self) {
1363        self.drop_all();
1364    }
1365}
1366
1367// ──────────────────────────────────────────────────────────────────────────
1368// Tests — confirm the skeleton's invariants before agents ever touch it.
1369// ──────────────────────────────────────────────────────────────────────────
1370
1371#[cfg(test)]
1372mod tests {
1373    use super::*;
1374
1375    /// A tiny GC-tracked type for the smoke test.
1376    struct Cell0 {
1377        next: Cell<Option<Gc<Cell0>>>,
1378        marker_calls: Cell<usize>,
1379    }
1380
1381    impl Trace for Cell0 {
1382        fn trace(&self, m: &mut Marker) {
1383            self.marker_calls.set(self.marker_calls.get() + 1);
1384            if let Some(n) = self.next.get() {
1385                m.mark(n);
1386            }
1387        }
1388    }
1389
1390    /// Roots for tests: just a single Gc<Cell0>, or none.
1391    struct OneRoot(Option<Gc<Cell0>>);
1392    impl Trace for OneRoot {
1393        fn trace(&self, m: &mut Marker) {
1394            if let Some(g) = self.0 {
1395                m.mark(g);
1396            }
1397        }
1398    }
1399
1400    #[test]
1401    fn alloc_and_drop_all() {
1402        let heap = Heap::new();
1403        heap.unpause();
1404        let _a = heap.allocate(Cell0 {
1405            next: Cell::new(None),
1406            marker_calls: Cell::new(0),
1407        });
1408        let _b = heap.allocate(Cell0 {
1409            next: Cell::new(None),
1410            marker_calls: Cell::new(0),
1411        });
1412        assert!(heap.bytes_used() > 0);
1413        heap.drop_all();
1414        assert_eq!(heap.bytes_used(), 0);
1415    }
1416
1417    #[test]
1418    fn account_buffer_refunded_on_sweep() {
1419        let heap = Heap::new();
1420        heap.unpause();
1421        let baseline = heap.bytes_used();
1422        {
1423            let a = heap.allocate(Cell0 {
1424                next: Cell::new(None),
1425                marker_calls: Cell::new(0),
1426            });
1427            assert!(heap.bytes_used() > baseline);
1428            a.account_buffer(&heap, 4096);
1429            assert_eq!(a.header().size.get(), std::mem::size_of::<GcBox<Cell0>>() + 4096);
1430        }
1431        // Drop the only root path (a is no longer Trace-visible). The +4096
1432        // must be refunded via header.size when the box is swept.
1433        heap.full_collect(&OneRoot(None));
1434        assert_eq!(
1435            heap.bytes_used(),
1436            baseline,
1437            "account_buffer charge must be fully refunded by sweep"
1438        );
1439    }
1440
1441    #[test]
1442    fn account_buffer_noop_on_uncollected() {
1443        let heap = Heap::new();
1444        heap.unpause();
1445        let g = Gc::new_uncollected(Cell0 {
1446            next: Cell::new(None),
1447            marker_calls: Cell::new(0),
1448        });
1449        let before = heap.bytes_used();
1450        g.account_buffer(&heap, 8192);
1451        assert_eq!(heap.bytes_used(), before, "uncollected box must not charge the pacer");
1452    }
1453
1454    #[test]
1455    fn collect_unreachable_frees_bytes() {
1456        let heap = Heap::new();
1457        heap.unpause();
1458        let _a = heap.allocate(Cell0 {
1459            next: Cell::new(None),
1460            marker_calls: Cell::new(0),
1461        });
1462        let bytes_before = heap.bytes_used();
1463        assert!(bytes_before > 0);
1464        // No roots — everything should sweep.
1465        heap.full_collect(&OneRoot(None));
1466        assert_eq!(heap.bytes_used(), 0);
1467        assert_eq!(heap.collections(), 1);
1468    }
1469
1470    #[test]
1471    fn collect_keeps_reachable() {
1472        let heap = Heap::new();
1473        heap.unpause();
1474        let root = heap.allocate(Cell0 {
1475            next: Cell::new(None),
1476            marker_calls: Cell::new(0),
1477        });
1478        let bytes_before = heap.bytes_used();
1479        heap.full_collect(&OneRoot(Some(root)));
1480        assert_eq!(heap.bytes_used(), bytes_before);
1481        assert_eq!(root.marker_calls.get(), 1);
1482    }
1483
1484    #[test]
1485    fn allocations_start_new_and_white() {
1486        let heap = Heap::new();
1487        heap.unpause();
1488        let obj = heap.allocate(Cell0 {
1489            next: Cell::new(None),
1490            marker_calls: Cell::new(0),
1491        });
1492        assert_eq!(obj.age(), GcAge::New);
1493        assert!(obj.color().is_white());
1494    }
1495
1496    #[test]
1497    fn allocation_tokens_track_exact_live_box() {
1498        let heap = Heap::new();
1499        heap.unpause();
1500        let obj = heap.allocate(Cell0 {
1501            next: Cell::new(None),
1502            marker_calls: Cell::new(0),
1503        });
1504        let id = obj.identity();
1505        let token = heap
1506            .allocation_token(id)
1507            .expect("heap allocation should get a token");
1508
1509        assert!(heap.contains_allocation(id, token));
1510        assert!(!heap.contains_allocation(id, token + 1));
1511
1512        heap.full_collect(&OneRoot(None));
1513        assert_eq!(heap.allocation_token(id), None);
1514        assert!(!heap.contains_allocation(id, token));
1515    }
1516
1517    #[test]
1518    fn allocation_during_incremental_sweep_survives_current_cycle() {
1519        let heap = Heap::new();
1520        heap.unpause();
1521        let old_dead = heap.allocate(Cell0 {
1522            next: Cell::new(None),
1523            marker_calls: Cell::new(0),
1524        });
1525        let old_id = old_dead.identity();
1526
1527        let outcome = heap.incremental_step_with_post_mark(
1528            &OneRoot(None),
1529            StepBudget::from_work(1),
1530            |_| {},
1531        );
1532        assert_eq!(outcome, StepOutcome::InProgress);
1533        assert_eq!(heap.gc_state(), GcState::Sweep);
1534
1535        let new_during_sweep = heap.allocate(Cell0 {
1536            next: Cell::new(None),
1537            marker_calls: Cell::new(0),
1538        });
1539        let new_id = new_during_sweep.identity();
1540
1541        loop {
1542            let outcome = heap.incremental_step_with_post_mark(
1543                &OneRoot(None),
1544                StepBudget::from_work(64),
1545                |_| {},
1546            );
1547            if matches!(outcome, StepOutcome::Paused) {
1548                break;
1549            }
1550        }
1551
1552        assert_eq!(heap.allocation_token(old_id), None);
1553        assert!(heap.allocation_token(new_id).is_some());
1554        assert_eq!(heap.allgc_count(), 1);
1555
1556        heap.full_collect(&OneRoot(None));
1557        assert_eq!(heap.allocation_token(new_id), None);
1558        assert_eq!(heap.allgc_count(), 0);
1559    }
1560
1561    #[test]
1562    fn promote_and_reset_all_ages() {
1563        let heap = Heap::new();
1564        heap.unpause();
1565        let a = heap.allocate(Cell0 {
1566            next: Cell::new(None),
1567            marker_calls: Cell::new(0),
1568        });
1569        let b = heap.allocate(Cell0 {
1570            next: Cell::new(None),
1571            marker_calls: Cell::new(0),
1572        });
1573
1574        heap.promote_all_to_old();
1575        assert_eq!(a.age(), GcAge::Old);
1576        assert_eq!(b.age(), GcAge::Old);
1577        assert_eq!(a.color(), Color::Black);
1578        assert_eq!(b.color(), Color::Black);
1579
1580        heap.reset_all_ages();
1581        assert_eq!(a.age(), GcAge::New);
1582        assert_eq!(b.age(), GcAge::New);
1583        assert!(a.color().is_white());
1584        assert!(b.color().is_white());
1585    }
1586
1587    #[test]
1588    fn generational_barriers_update_ages() {
1589        let heap = Heap::new();
1590        heap.unpause();
1591        let parent = heap.allocate(Cell0 {
1592            next: Cell::new(None),
1593            marker_calls: Cell::new(0),
1594        });
1595        let child = heap.allocate(Cell0 {
1596            next: Cell::new(None),
1597            marker_calls: Cell::new(0),
1598        });
1599
1600        parent.set_age(GcAge::Old);
1601        parent.set_color(Color::Black);
1602        heap.generational_backward_barrier(parent);
1603        assert_eq!(parent.age(), GcAge::Touched1);
1604        assert_eq!(parent.color(), Color::Gray);
1605
1606        heap.generational_forward_barrier(parent, child);
1607        assert_eq!(child.age(), GcAge::Old0);
1608    }
1609
1610    #[test]
1611    fn minor_collect_frees_young_and_keeps_old() {
1612        let heap = Heap::new();
1613        heap.unpause();
1614        let old_unreachable = heap.allocate(Cell0 {
1615            next: Cell::new(None),
1616            marker_calls: Cell::new(0),
1617        });
1618        old_unreachable.set_age(GcAge::Old);
1619        old_unreachable.set_color(Color::Black);
1620        let _young_unreachable = heap.allocate(Cell0 {
1621            next: Cell::new(None),
1622            marker_calls: Cell::new(0),
1623        });
1624        let young_survivor = heap.allocate(Cell0 {
1625            next: Cell::new(None),
1626            marker_calls: Cell::new(0),
1627        });
1628
1629        heap.minor_collect_with_post_mark(&OneRoot(Some(young_survivor)), |_| {});
1630
1631        assert_eq!(heap.allgc_count(), 2);
1632        assert_eq!(old_unreachable.age(), GcAge::Old);
1633        assert_eq!(young_survivor.age(), GcAge::Survival);
1634        assert!(young_survivor.color().is_white());
1635    }
1636
1637    #[test]
1638    fn collect_traverses_cycles() {
1639        let heap = Heap::new();
1640        heap.unpause();
1641        let a = heap.allocate(Cell0 {
1642            next: Cell::new(None),
1643            marker_calls: Cell::new(0),
1644        });
1645        let b = heap.allocate(Cell0 {
1646            next: Cell::new(Some(a)),
1647            marker_calls: Cell::new(0),
1648        });
1649        a.next.set(Some(b)); // cycle
1650        // With a as root, both should survive.
1651        heap.full_collect(&OneRoot(Some(a)));
1652        assert_eq!(a.marker_calls.get(), 1);
1653        assert_eq!(b.marker_calls.get(), 1);
1654        // Drop the only root path; cycle should now be collected.
1655        // (Note: `a` and `b` are still on the stack as Gc<Cell0> handles, but
1656        // they're not in a Trace-visible position.)
1657        heap.full_collect(&OneRoot(None));
1658        assert_eq!(heap.bytes_used(), 0);
1659    }
1660
1661    #[test]
1662    fn heap_guard_stacks() {
1663        assert!(with_current_heap(|heap| heap.is_none()), "no guard initially");
1664        let h1 = Heap::new();
1665        h1.unpause();
1666        {
1667            let _g1 = HeapGuard::push(&h1);
1668            assert!(with_current_heap(|heap| heap.is_some()));
1669            let h2 = Heap::new();
1670            h2.unpause();
1671            {
1672                let _g2 = HeapGuard::push(&h2);
1673                // top of stack is h2
1674                with_current_heap(|heap| {
1675                    assert!(std::ptr::addr_eq(
1676                        heap.unwrap() as *const _,
1677                        &h2 as *const _,
1678                    ));
1679                });
1680            }
1681            // _g2 dropped — top is back to h1
1682            with_current_heap(|heap| {
1683                assert!(std::ptr::addr_eq(
1684                    heap.unwrap() as *const _,
1685                    &h1 as *const _,
1686                ));
1687            });
1688        }
1689        assert!(
1690            with_current_heap(|heap| heap.is_none()),
1691            "stack empty after all guards drop"
1692        );
1693    }
1694
1695    #[test]
1696    fn step_threshold_triggers_collect() {
1697        let heap = Heap::new();
1698        heap.unpause();
1699        // Allocate enough boxes to exceed the default 64KB threshold.
1700        let mut keeps = Vec::new();
1701        for _ in 0..64 {
1702            // ~1KB per box (Cell0 is small, but allocating many headers
1703            // accumulates). For the threshold test we'd typically allocate
1704            // larger objects; this is a smoke test.
1705            keeps.push(heap.allocate(Cell0 {
1706                next: Cell::new(None),
1707                marker_calls: Cell::new(0),
1708            }));
1709        }
1710        // Build roots that retain all of keeps via individual marks.
1711        struct ManyRoots<'a>(&'a [Gc<Cell0>]);
1712        impl<'a> Trace for ManyRoots<'a> {
1713            fn trace(&self, m: &mut Marker) {
1714                for g in self.0.iter() {
1715                    m.mark(*g);
1716                }
1717            }
1718        }
1719        heap.step(&ManyRoots(&keeps));
1720        // step is a no-op below threshold; all roots retained.
1721        assert!(heap.bytes_used() > 0);
1722    }
1723
1724    #[test]
1725    fn threshold_floored_after_collecting_tiny_heap() {
1726        let heap = Heap::new();
1727        heap.unpause();
1728        struct NoRoots;
1729        impl Trace for NoRoots {
1730            fn trace(&self, _m: &mut Marker) {}
1731        }
1732        for _ in 0..200 {
1733            heap.allocate(Cell0 {
1734                next: Cell::new(None),
1735                marker_calls: Cell::new(0),
1736            });
1737        }
1738        heap.full_collect(&NoRoots);
1739        assert!(
1740            heap.threshold_bytes() >= GC_MIN_THRESHOLD,
1741            "threshold {} collapsed below floor {}; a churning program would full-collect per allocation",
1742            heap.threshold_bytes(),
1743            GC_MIN_THRESHOLD
1744        );
1745    }
1746
1747    fn build_chain(heap: &Heap, len: usize) -> Gc<Cell0> {
1748        let head = heap.allocate(Cell0 {
1749            next: Cell::new(None),
1750            marker_calls: Cell::new(0),
1751        });
1752        let mut cur = head;
1753        for _ in 1..len {
1754            let n = heap.allocate(Cell0 {
1755                next: Cell::new(None),
1756                marker_calls: Cell::new(0),
1757            });
1758            cur.next.set(Some(n));
1759            cur = n;
1760        }
1761        head
1762    }
1763
1764    #[test]
1765    fn budget_zero_does_some_work() {
1766        let heap = Heap::new();
1767        heap.unpause();
1768        let head = build_chain(&heap, 8);
1769        let roots = OneRoot(Some(head));
1770        let budget = StepBudget::from_work(0);
1771        let outcome = heap.incremental_step_with_post_mark(&roots, budget, |_| {});
1772        assert_ne!(outcome, StepOutcome::SkippedStopped);
1773        assert_ne!(heap.gc_state(), GcState::Pause);
1774    }
1775
1776    #[test]
1777    fn run_until_state_stops_before_next_phase_work() {
1778        let heap = Heap::new();
1779        heap.unpause();
1780        let head = build_chain(&heap, 8);
1781        let roots = OneRoot(Some(head));
1782        let atomic_calls = Cell::new(0);
1783
1784        let outcome = heap.incremental_run_until_state_with_post_mark(
1785            &roots,
1786            GcState::Atomic,
1787            1024,
1788            |_| atomic_calls.set(atomic_calls.get() + 1),
1789        );
1790        assert_eq!(outcome, StepOutcome::InProgress);
1791        assert_eq!(heap.gc_state(), GcState::Atomic);
1792        assert_eq!(atomic_calls.get(), 0, "atomic hook must not run before inspection");
1793
1794        let outcome = heap.incremental_run_until_state_with_post_mark(
1795            &roots,
1796            GcState::Sweep,
1797            1024,
1798            |_| atomic_calls.set(atomic_calls.get() + 1),
1799        );
1800        assert_eq!(outcome, StepOutcome::InProgress);
1801        assert_eq!(heap.gc_state(), GcState::Sweep);
1802        assert_eq!(atomic_calls.get(), 1, "entering sweep must run the atomic hook once");
1803    }
1804
1805    #[test]
1806    fn larger_budget_drains_more_gray_than_smaller() {
1807        let small_heap = Heap::new();
1808        small_heap.unpause();
1809        let h1 = build_chain(&small_heap, 64);
1810        let r1 = OneRoot(Some(h1));
1811        let mut small_calls = 0;
1812        loop {
1813            small_calls += 1;
1814            let outcome = small_heap.incremental_step_with_post_mark(
1815                &r1,
1816                StepBudget::from_work(2),
1817                |_| {},
1818            );
1819            if outcome == StepOutcome::Paused {
1820                break;
1821            }
1822            assert!(small_calls < 10_000, "did not converge");
1823        }
1824
1825        let big_heap = Heap::new();
1826        big_heap.unpause();
1827        let h2 = build_chain(&big_heap, 64);
1828        let r2 = OneRoot(Some(h2));
1829        let mut big_calls = 0;
1830        loop {
1831            big_calls += 1;
1832            let outcome = big_heap.incremental_step_with_post_mark(
1833                &r2,
1834                StepBudget::from_work(64),
1835                |_| {},
1836            );
1837            if outcome == StepOutcome::Paused {
1838                break;
1839            }
1840            assert!(big_calls < 10_000, "did not converge");
1841        }
1842
1843        assert!(
1844            big_calls < small_calls,
1845            "expected big_calls={} < small_calls={}",
1846            big_calls,
1847            small_calls
1848        );
1849    }
1850
1851    #[test]
1852    fn sweep_can_pause_and_resume() {
1853        let heap = Heap::new();
1854        heap.unpause();
1855        for _ in 0..16 {
1856            let _ = heap.allocate(Cell0 {
1857                next: Cell::new(None),
1858                marker_calls: Cell::new(0),
1859            });
1860        }
1861        let roots = OneRoot(None);
1862        let bytes_before = heap.bytes_used();
1863        assert!(bytes_before > 0);
1864        let mut step_count = 0;
1865        let mut saw_in_progress_during_sweep = false;
1866        loop {
1867            step_count += 1;
1868            let outcome = heap.incremental_step_with_post_mark(
1869                &roots,
1870                StepBudget::from_work(2),
1871                |_| {},
1872            );
1873            if heap.gc_state() == GcState::Sweep && outcome == StepOutcome::InProgress {
1874                saw_in_progress_during_sweep = true;
1875            }
1876            if outcome == StepOutcome::Paused {
1877                break;
1878            }
1879            assert!(step_count < 10_000, "did not converge");
1880        }
1881        assert!(saw_in_progress_during_sweep, "sweep never paused mid-list");
1882        assert_eq!(heap.bytes_used(), 0);
1883    }
1884
1885    #[test]
1886    fn post_mark_runs_once_per_atomic() {
1887        let heap = Heap::new();
1888        heap.unpause();
1889        for _ in 0..32 {
1890            let _ = heap.allocate(Cell0 {
1891                next: Cell::new(None),
1892                marker_calls: Cell::new(0),
1893            });
1894        }
1895        let roots = OneRoot(None);
1896        let call_count = std::cell::Cell::new(0);
1897        let mut step_count = 0;
1898        loop {
1899            step_count += 1;
1900            let outcome = heap.incremental_step_with_post_mark(
1901                &roots,
1902                StepBudget::from_work(2),
1903                |_| {
1904                    call_count.set(call_count.get() + 1);
1905                },
1906            );
1907            if outcome == StepOutcome::Paused {
1908                break;
1909            }
1910            assert!(step_count < 10_000, "did not converge");
1911        }
1912        assert_eq!(call_count.get(), 1, "post_mark must run exactly once per cycle");
1913    }
1914
1915    #[test]
1916    fn full_collect_equivalent_to_incremental_to_pause() {
1917        let h1 = Heap::new();
1918        h1.unpause();
1919        let head1 = h1.allocate(Cell0 {
1920            next: Cell::new(None),
1921            marker_calls: Cell::new(0),
1922        });
1923        let _orphan1 = h1.allocate(Cell0 {
1924            next: Cell::new(None),
1925            marker_calls: Cell::new(0),
1926        });
1927        let _orphan2 = h1.allocate(Cell0 {
1928            next: Cell::new(None),
1929            marker_calls: Cell::new(0),
1930        });
1931        let roots1 = OneRoot(Some(head1));
1932        h1.full_collect(&roots1);
1933        let bytes_full = h1.bytes_used();
1934
1935        let h2 = Heap::new();
1936        h2.unpause();
1937        let head2 = h2.allocate(Cell0 {
1938            next: Cell::new(None),
1939            marker_calls: Cell::new(0),
1940        });
1941        let _orphan3 = h2.allocate(Cell0 {
1942            next: Cell::new(None),
1943            marker_calls: Cell::new(0),
1944        });
1945        let _orphan4 = h2.allocate(Cell0 {
1946            next: Cell::new(None),
1947            marker_calls: Cell::new(0),
1948        });
1949        let roots2 = OneRoot(Some(head2));
1950        loop {
1951            let outcome = h2.incremental_step_with_post_mark(
1952                &roots2,
1953                StepBudget::from_work(1),
1954                |_| {},
1955            );
1956            if outcome == StepOutcome::Paused {
1957                break;
1958            }
1959        }
1960        assert_eq!(h2.bytes_used(), bytes_full);
1961    }
1962}
1963
1964// ──────────────────────────────────────────────────────────────────────────────
1965// PORT STATUS
1966//   source:        production Rust heap/collector substrate
1967//   target_crate:  lua-gc
1968//   confidence:    medium
1969//   todos:         0
1970//   port_notes:    0
1971//   unsafe_blocks: 13
1972//   notes:         Mark-and-sweep collector heap + the Gc<T> raw-pointer substrate. Uses
1973//                  NonNull<GcBox<T>> with linked-list allgc walking; unsafe is
1974//                  required for raw pointer ops and Box::into_raw/from_raw. See
1975//                  unsafe-budgets.toml for the per-crate ceiling.
1976// ──────────────────────────────────────────────────────────────────────────────