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 precise tracing collector with incremental and
6//! generational paths plus forward/backward write barriers.
7//!
8//! # Vocabulary
9//!
10//! - **Gc<T>**: a pointer-sized handle. `Copy + Clone`. Replaces `GcRef<T>`.
11//! - **GcBox<T>**: the heap allocation; contains a header and the value.
12//! - **GcHeader**: per-object metadata (color, age, finalized flag, intrusive
13//!   `next` pointer for exactly one heap owner list, and grayagain revisit link).
14//! - **Trace**: trait every GC-rooted type implements. The `trace` method
15//!   walks all `Gc<_>` fields and calls `Marker::mark` on each.
16//! - **Marker**: passed to `trace`; carries the gray queue.
17//! - **Heap**: owns the allgc/finobj/tobefnz list heads, byte counters, and
18//!   GC state machine.
19//!
20//! # Safety model
21//!
22//! All `unsafe` is confined to this crate (per `harness/unsafe-budgets.toml`).
23//! The invariants are:
24//!
25//! 1. Every `Gc<T>` points to a valid, allocated, not-yet-swept `GcBox<T>`.
26//! 2. The intrusive heap lists are consistent: traversing `header.next` from
27//!    `Heap.head`, `Heap.finobj`, and `Heap.tobefnz` reaches every live
28//!    heap-owned `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 heap owner lists.
39
40use std::cell::{Cell, RefCell};
41use std::collections::{HashMap, HashSet};
42use std::hash::{BuildHasherDefault, Hasher};
43use std::marker::PhantomData;
44use std::ptr::NonNull;
45
46#[derive(Default)]
47struct IdentityHasher {
48    value: u64,
49}
50
51impl Hasher for IdentityHasher {
52    #[inline]
53    fn write(&mut self, bytes: &[u8]) {
54        const PRIME: u64 = 0x0000_0100_0000_01b3;
55        for &byte in bytes {
56            self.value ^= u64::from(byte);
57            self.value = self.value.wrapping_mul(PRIME);
58        }
59    }
60
61    #[inline]
62    fn write_usize(&mut self, i: usize) {
63        self.value = i as u64;
64    }
65
66    #[inline]
67    fn write_u64(&mut self, i: u64) {
68        self.value = i;
69    }
70
71    #[inline]
72    fn finish(&self) -> u64 {
73        let mut x = self.value;
74        x ^= x >> 30;
75        x = x.wrapping_mul(0xbf58_476d_1ce4_e5b9);
76        x ^= x >> 27;
77        x = x.wrapping_mul(0x94d0_49bb_1331_11eb);
78        x ^ (x >> 31)
79    }
80}
81
82type IdentityBuildHasher = BuildHasherDefault<IdentityHasher>;
83type IdentityHashSet = HashSet<usize, IdentityBuildHasher>;
84type IdentityHashMap<V> = HashMap<usize, V, IdentityBuildHasher>;
85
86// ──────────────────────────────────────────────────────────────────────────
87// Phase D-1c — scoped thread-local HeapGuard
88//
89// Lua's C API supports multiple `lua_State`s on one OS thread (sandbox-per-
90// state is a real embedding pattern). We honor that by stacking the
91// currently-active heap rather than holding a single slot. `HeapGuard::push`
92// activates a heap; drop pops it.
93//
94// `with_current_heap(...)` exposes the top of the stack only for the dynamic
95// extent of a closure — used by `GcRef::new` call sites that don't have
96// `&mut LuaState` in scope.
97// ──────────────────────────────────────────────────────────────────────────
98
99thread_local! {
100    static CURRENT_HEAP_STACK: RefCell<Vec<NonNull<Heap>>> = const { RefCell::new(Vec::new()) };
101}
102
103/// A scoped guard for the currently-active heap. Pushed at entry to
104/// `state.run()` / `state.protected_call()` / `state.load()`; popped on
105/// drop. Supports nesting (multiple LuaStates on one thread).
106pub struct HeapGuard {
107    // Anchor a NonNull so the user can't accidentally drop the guard while
108    // an inner Lua state is still active. We rely on RAII.
109    _private: (),
110}
111
112impl HeapGuard {
113    /// Push `heap` onto the active stack. Returns a guard; dropping it pops.
114    ///
115    /// # Safety
116    ///
117    /// The pointer must remain valid for the lifetime of the guard. Callers
118    /// typically pass `&state.global.heap`, which lives as long as the
119    /// `GlobalState` (an `Rc<RefCell<_>>`); the guard must drop before the
120    /// state is dropped.
121    pub fn push(heap: &Heap) -> Self {
122        let ptr = NonNull::from(heap);
123        CURRENT_HEAP_STACK.with(|stack| stack.borrow_mut().push(ptr));
124        HeapGuard { _private: () }
125    }
126}
127
128impl Drop for HeapGuard {
129    fn drop(&mut self) {
130        CURRENT_HEAP_STACK.with(|stack| {
131            let popped = stack.borrow_mut().pop();
132            debug_assert!(popped.is_some(), "HeapGuard::drop with empty stack");
133        });
134    }
135}
136
137/// Runs `f` with a reference to the currently-active heap, or `None` if no
138/// `HeapGuard` is in scope.
139///
140/// The heap reference is deliberately scoped to the closure. This avoids the
141/// previous `current_heap() -> Option<&'static Heap>` lifetime lie while still
142/// supporting legacy `GcRef::new` call sites that do not receive `&mut LuaState`.
143pub fn with_current_heap<R>(f: impl for<'a> FnOnce(Option<&'a Heap>) -> R) -> R {
144    CURRENT_HEAP_STACK.with(|stack| {
145        let ptr = stack.borrow().last().copied();
146        // SAFETY: the top NonNull was produced from a live `&Heap` whose
147        // lifetime is bounded by the corresponding `HeapGuard`. The reference
148        // is only handed to `f`, and cannot escape through the return type.
149        let heap = ptr.map(|ptr| unsafe { &*ptr.as_ptr() });
150        f(heap)
151    })
152}
153
154#[derive(Copy, Clone, Debug)]
155pub struct HeapRef {
156    ptr: NonNull<Heap>,
157}
158
159impl HeapRef {
160    pub fn from_heap(heap: &Heap) -> Self {
161        HeapRef {
162            ptr: NonNull::from(heap),
163        }
164    }
165
166    pub fn contains_allocation(self, identity: usize, token: usize) -> bool {
167        // SAFETY: `HeapRef` is created only from a live `&Heap`. Runtime-owned
168        // weak handles store it inside `GlobalState`, whose heap field outlives
169        // those handles. The method only traverses heap metadata and never
170        // dereferences the weak target pointer.
171        unsafe { self.ptr.as_ref() }.contains_allocation(identity, token)
172    }
173}
174
175/// A traced color in the tri-color invariant.
176#[derive(Copy, Clone, PartialEq, Eq, Debug)]
177pub enum Color {
178    /// Not yet visited in the current cycle. The collector alternates between
179    /// two white bits so allocations made during sweep are not collected by
180    /// the cycle already in progress.
181    White0,
182    /// Alternate white bit.
183    White1,
184    /// Visited; outgoing references not yet traced.
185    Gray,
186    /// Fully traced; no outgoing pointers to white objects.
187    Black,
188}
189
190impl Color {
191    pub fn is_white(self) -> bool {
192        matches!(self, Color::White0 | Color::White1)
193    }
194
195    fn other_white(self) -> Self {
196        match self {
197            Color::White0 => Color::White1,
198            Color::White1 => Color::White0,
199            Color::Gray | Color::Black => self,
200        }
201    }
202}
203
204/// Object age used by Lua's generational collector.
205///
206/// Mirrors `G_NEW` through `G_TOUCHED2` in `lgc.h`.
207#[derive(Copy, Clone, PartialEq, Eq, Debug)]
208pub enum GcAge {
209    New,
210    Survival,
211    Old0,
212    Old1,
213    Old,
214    Touched1,
215    Touched2,
216}
217
218impl GcAge {
219    pub fn is_old(self) -> bool {
220        !matches!(self, GcAge::New | GcAge::Survival)
221    }
222
223    fn next_after_minor(self) -> Self {
224        match self {
225            GcAge::New => GcAge::Survival,
226            GcAge::Survival | GcAge::Old0 => GcAge::Old1,
227            GcAge::Old1 | GcAge::Old | GcAge::Touched2 => GcAge::Old,
228            GcAge::Touched1 => GcAge::Touched2,
229        }
230    }
231}
232
233/// Minimal metadata a finalizable VM object must expose for collector-owned
234/// finalizer-list bookkeeping.
235pub trait FinalizerEntry: Clone {
236    fn identity(&self) -> usize;
237    fn heap_ptr(&self) -> Option<NonNull<GcBox<dyn Trace>>> {
238        None
239    }
240    fn age(&self) -> GcAge;
241    fn is_finalized(&self) -> bool;
242    fn set_finalized(&self, finalized: bool);
243}
244
245/// Minimal operations needed for collector-owned weak-registry bookkeeping.
246pub trait WeakEntry: Clone {
247    type Strong: Clone;
248
249    fn identity(&self) -> usize;
250    fn list_kind(&self) -> WeakListKind;
251    fn upgrade(&self) -> Option<Self::Strong>;
252}
253
254#[derive(Copy, Clone, Debug, PartialEq, Eq)]
255pub enum WeakListKind {
256    WeakValues,
257    Ephemeron,
258    AllWeak,
259}
260
261#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
262pub struct WeakRegistryStats {
263    pub tracked: usize,
264    pub snapshot_live: usize,
265    pub snapshot_dead: usize,
266    pub retained: usize,
267    pub weak_values: usize,
268    pub ephemeron: usize,
269    pub all_weak: usize,
270}
271
272#[derive(Clone, Debug)]
273pub struct WeakRegistry<T: WeakEntry> {
274    weak_values: Vec<T>,
275    ephemeron: Vec<T>,
276    all_weak: Vec<T>,
277    last_stats: WeakRegistryStats,
278}
279
280#[derive(Clone, Debug, PartialEq, Eq)]
281pub struct WeakRegistrySnapshot<T> {
282    pub weak_values: Vec<T>,
283    pub ephemeron: Vec<T>,
284    pub all_weak: Vec<T>,
285}
286
287impl<T> Default for WeakRegistrySnapshot<T> {
288    fn default() -> Self {
289        Self {
290            weak_values: Vec::new(),
291            ephemeron: Vec::new(),
292            all_weak: Vec::new(),
293        }
294    }
295}
296
297impl<T> WeakRegistrySnapshot<T> {
298    pub fn len(&self) -> usize {
299        self.weak_values
300            .len()
301            .saturating_add(self.ephemeron.len())
302            .saturating_add(self.all_weak.len())
303    }
304
305    pub fn into_flat(self) -> Vec<T> {
306        self.weak_values
307            .into_iter()
308            .chain(self.ephemeron)
309            .chain(self.all_weak)
310            .collect()
311    }
312}
313
314impl<T: WeakEntry> Default for WeakRegistry<T> {
315    fn default() -> Self {
316        Self {
317            weak_values: Vec::new(),
318            ephemeron: Vec::new(),
319            all_weak: Vec::new(),
320            last_stats: WeakRegistryStats::default(),
321        }
322    }
323}
324
325impl<T: WeakEntry> WeakRegistry<T> {
326    pub fn len(&self) -> usize {
327        self.weak_values
328            .len()
329            .saturating_add(self.ephemeron.len())
330            .saturating_add(self.all_weak.len())
331    }
332
333    pub fn stats(&self) -> WeakRegistryStats {
334        self.last_stats
335    }
336
337    fn list_mut(&mut self, kind: WeakListKind) -> &mut Vec<T> {
338        match kind {
339            WeakListKind::WeakValues => &mut self.weak_values,
340            WeakListKind::Ephemeron => &mut self.ephemeron,
341            WeakListKind::AllWeak => &mut self.all_weak,
342        }
343    }
344
345    pub fn remove_identity(&mut self, id: usize) {
346        self.weak_values.retain(|entry| entry.identity() != id);
347        self.ephemeron.retain(|entry| entry.identity() != id);
348        self.all_weak.retain(|entry| entry.identity() != id);
349        self.last_stats.tracked = self.len();
350        self.last_stats.retained = self.len();
351        self.update_cohort_stats();
352    }
353
354    fn update_cohort_stats(&mut self) {
355        self.last_stats.weak_values = self.weak_values.len();
356        self.last_stats.ephemeron = self.ephemeron.len();
357        self.last_stats.all_weak = self.all_weak.len();
358    }
359
360    pub fn push_unique(&mut self, entry: T) {
361        let id = entry.identity();
362        self.remove_identity(id);
363        self.list_mut(entry.list_kind()).push(entry);
364        self.last_stats.tracked = self.len();
365        self.last_stats.retained = self.len();
366        self.update_cohort_stats();
367    }
368
369    pub fn live_snapshot_by_kind(&mut self) -> WeakRegistrySnapshot<T::Strong> {
370        let tracked_before = self.len();
371        let weak_values_capacity = self.weak_values.len();
372        let ephemeron_capacity = self.ephemeron.len();
373        let all_weak_capacity = self.all_weak.len();
374        let mut seen = std::collections::HashSet::<usize>::with_capacity(tracked_before);
375        let mut live = WeakRegistrySnapshot {
376            weak_values: Vec::with_capacity(weak_values_capacity),
377            ephemeron: Vec::with_capacity(ephemeron_capacity),
378            all_weak: Vec::with_capacity(all_weak_capacity),
379        };
380        let mut dead = 0usize;
381
382        let entries = std::mem::take(&mut self.weak_values)
383            .into_iter()
384            .chain(std::mem::take(&mut self.ephemeron))
385            .chain(std::mem::take(&mut self.all_weak));
386        for entry in entries {
387            if !seen.insert(entry.identity()) {
388                continue;
389            }
390            match entry.upgrade() {
391                Some(strong) => {
392                    match entry.list_kind() {
393                        WeakListKind::WeakValues => live.weak_values.push(strong),
394                        WeakListKind::Ephemeron => live.ephemeron.push(strong),
395                        WeakListKind::AllWeak => live.all_weak.push(strong),
396                    }
397                    self.list_mut(entry.list_kind()).push(entry);
398                }
399                None => dead += 1,
400            }
401        }
402
403        self.last_stats = WeakRegistryStats {
404            tracked: tracked_before,
405            snapshot_live: live.len(),
406            snapshot_dead: dead,
407            retained: self.len(),
408            weak_values: self.weak_values.len(),
409            ephemeron: self.ephemeron.len(),
410            all_weak: self.all_weak.len(),
411        };
412        live
413    }
414
415    pub fn live_snapshot(&mut self) -> Vec<T::Strong> {
416        self.live_snapshot_by_kind().into_flat()
417    }
418
419    pub fn retain_identities(&mut self, ids: &std::collections::HashSet<usize>) {
420        self.weak_values
421            .retain(|entry| ids.contains(&entry.identity()));
422        self.ephemeron
423            .retain(|entry| ids.contains(&entry.identity()));
424        self.all_weak
425            .retain(|entry| ids.contains(&entry.identity()));
426        self.last_stats.retained = self.len();
427        self.last_stats.tracked = self.len();
428        self.update_cohort_stats();
429    }
430}
431
432#[derive(Clone, Debug)]
433pub struct FinalizerRegistry<T: FinalizerEntry> {
434    pending: Vec<T>,
435    to_be_finalized: Vec<T>,
436    pending_reallyold: usize,
437    pending_old1: usize,
438    pending_survival: usize,
439}
440
441#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
442pub struct FinalizerRegistryStats {
443    pub pending_young: usize,
444    pub pending_old: usize,
445    pub to_be_finalized_young: usize,
446    pub to_be_finalized_old: usize,
447    pub finobj_new: usize,
448    pub finobj_survival: usize,
449    pub finobj_old1: usize,
450    pub finobj_reallyold: usize,
451    pub finobj_minor_scan: usize,
452}
453
454impl<T: FinalizerEntry> Default for FinalizerRegistry<T> {
455    fn default() -> Self {
456        Self {
457            pending: Vec::new(),
458            to_be_finalized: Vec::new(),
459            pending_reallyold: 0,
460            pending_old1: 0,
461            pending_survival: 0,
462        }
463    }
464}
465
466impl<T: FinalizerEntry> FinalizerRegistry<T> {
467    fn pending_new_len(&self) -> usize {
468        self.pending.len().saturating_sub(
469            self.pending_reallyold
470                .saturating_add(self.pending_old1)
471                .saturating_add(self.pending_survival),
472        )
473    }
474
475    fn minor_scan_start(&self) -> usize {
476        self.pending_reallyold.saturating_add(self.pending_old1)
477    }
478
479    fn debug_assert_pending_cohorts(&self) {
480        debug_assert!(
481            self.pending_reallyold
482                .saturating_add(self.pending_old1)
483                .saturating_add(self.pending_survival)
484                <= self.pending.len()
485        );
486    }
487
488    pub fn pending(&self) -> &[T] {
489        &self.pending
490    }
491
492    pub fn pending_snapshot(&self) -> Vec<T> {
493        self.pending.clone()
494    }
495
496    pub fn pending_minor_snapshot(&self) -> Vec<T> {
497        self.pending[self.minor_scan_start().min(self.pending.len())..].to_vec()
498    }
499
500    pub fn to_be_finalized(&self) -> &[T] {
501        &self.to_be_finalized
502    }
503
504    pub fn pending_len(&self) -> usize {
505        self.pending.len()
506    }
507
508    pub fn to_be_finalized_len(&self) -> usize {
509        self.to_be_finalized.len()
510    }
511
512    pub fn has_to_be_finalized(&self) -> bool {
513        !self.to_be_finalized.is_empty()
514    }
515
516    pub fn stats(&self) -> FinalizerRegistryStats {
517        fn count_by_age<T: FinalizerEntry>(objects: &[T]) -> (usize, usize) {
518            objects
519                .iter()
520                .fold((0usize, 0usize), |(young, old), object| {
521                    if object.age().is_old() {
522                        (young, old + 1)
523                    } else {
524                        (young + 1, old)
525                    }
526                })
527        }
528        let (pending_young, pending_old) = count_by_age(&self.pending);
529        let (to_be_finalized_young, to_be_finalized_old) = count_by_age(&self.to_be_finalized);
530        FinalizerRegistryStats {
531            pending_young,
532            pending_old,
533            to_be_finalized_young,
534            to_be_finalized_old,
535            finobj_new: self.pending_new_len(),
536            finobj_survival: self.pending_survival,
537            finobj_old1: self.pending_old1,
538            finobj_reallyold: self.pending_reallyold,
539            finobj_minor_scan: self.pending.len().saturating_sub(self.minor_scan_start()),
540        }
541    }
542
543    pub fn push_pending_unique(&mut self, object: T) -> bool {
544        if object.is_finalized() {
545            return false;
546        }
547        let id = object.identity();
548        if !self.pending.iter().any(|o| o.identity() == id) {
549            object.set_finalized(true);
550            self.pending.push(object);
551            self.debug_assert_pending_cohorts();
552            true
553        } else {
554            false
555        }
556    }
557
558    pub fn take_pending(&mut self) -> Vec<T> {
559        self.pending_reallyold = 0;
560        self.pending_old1 = 0;
561        self.pending_survival = 0;
562        std::mem::take(&mut self.pending)
563    }
564
565    fn retain_pending_not_in(&mut self, ids: &std::collections::HashSet<usize>) {
566        if ids.is_empty() {
567            return;
568        }
569        let original_reallyold = self.pending_reallyold;
570        let original_old1 = self.pending_old1;
571        let original_survival = self.pending_survival;
572        let mut retained_reallyold = original_reallyold;
573        let mut retained_old1 = original_old1;
574        let mut retained_survival = original_survival;
575        let mut retained = Vec::with_capacity(self.pending.len());
576        for (index, object) in std::mem::take(&mut self.pending).into_iter().enumerate() {
577            if ids.contains(&object.identity()) {
578                if index < original_reallyold {
579                    retained_reallyold -= 1;
580                } else if index < original_reallyold + original_old1 {
581                    retained_old1 -= 1;
582                } else if index < original_reallyold + original_old1 + original_survival {
583                    retained_survival -= 1;
584                }
585            } else {
586                retained.push(object);
587            }
588        }
589        self.pending = retained;
590        self.pending_reallyold = retained_reallyold;
591        self.pending_old1 = retained_old1;
592        self.pending_survival = retained_survival;
593        self.debug_assert_pending_cohorts();
594    }
595
596    pub fn push_to_be_finalized(&mut self, object: T) {
597        object.set_finalized(true);
598        self.to_be_finalized.push(object);
599    }
600
601    fn extend_to_be_finalized(&mut self, objects: Vec<T>) -> Vec<T> {
602        let drain_order: Vec<T> = objects.into_iter().rev().collect();
603        for object in drain_order.iter().cloned() {
604            self.push_to_be_finalized(object);
605        }
606        drain_order
607    }
608
609    pub fn promote_pending_to_finalized(&mut self, objects: Vec<T>) -> Vec<T> {
610        if objects.is_empty() {
611            return Vec::new();
612        }
613        let mut ids: std::collections::HashSet<usize> =
614            std::collections::HashSet::with_capacity(objects.len());
615        ids.extend(objects.iter().map(|object| object.identity()));
616        self.retain_pending_not_in(&ids);
617        self.extend_to_be_finalized(objects)
618    }
619
620    pub fn promote_all_pending_to_old(&mut self) {
621        self.pending_reallyold = self.pending.len();
622        self.pending_old1 = 0;
623        self.pending_survival = 0;
624    }
625
626    pub fn reset_generation_boundaries(&mut self) {
627        self.pending_reallyold = 0;
628        self.pending_old1 = 0;
629        self.pending_survival = 0;
630    }
631
632    pub fn finish_minor_collection(&mut self) {
633        let new = self.pending_new_len();
634        self.pending_reallyold = self.pending_reallyold.saturating_add(self.pending_old1);
635        self.pending_old1 = self.pending_survival;
636        self.pending_survival = new;
637        self.debug_assert_pending_cohorts();
638    }
639
640    pub fn pop_to_be_finalized(&mut self) -> Option<T> {
641        let object = if self.to_be_finalized.is_empty() {
642            None
643        } else {
644            Some(self.to_be_finalized.remove(0))
645        };
646        if let Some(ref object) = object {
647            object.set_finalized(false);
648        }
649        object
650    }
651}
652
653/// Per-object GC metadata. Lives at the start of every `GcBox`.
654#[repr(C)]
655pub struct GcHeader {
656    /// Hot fields read/written by the mark/sweep/barrier loops keep their
657    /// own bytes — packing them measurably taxed gc-heavy workloads
658    /// (recount 2026-06-10: +4% Ir on gc_pressure). The 64 -> 40 byte diet
659    /// comes from the COLD side instead: the diagnostics-only `type_name`
660    /// fat pointer became a `Trace` method, the three cold bool flags share
661    /// one byte, and the pacer size is u32.
662    color: Cell<Color>,
663    age: Cell<GcAge>,
664    /// Cold flags, one bit each: finalized (FINALIZEDBIT — set while the
665    /// object is registered in a pending/to-be-finalized list, cleared when
666    /// popped for `__gc`), collected (true iff this box is linked into a
667    /// heap owner list so it will be swept and its `size` refunded;
668    /// `new_uncollected` boxes stay false and must never have buffer bytes
669    /// charged — [`Gc::account_buffer`] no-ops), gray_listed (true while
670    /// linked into the grayagain revisit list).
671    flags: Cell<u8>,
672    /// Rough byte size charged to the pacer for this object. Starts at the
673    /// `GcBox<T>` size and is adjusted in place by [`Gc::account_buffer`]
674    /// when the value's owned heap buffers (table array/node Vecs) grow or
675    /// shrink. Invariant: this is always exactly the amount sweep will
676    /// refund to the heap's byte counter when this object is freed. `u32`:
677    /// a single object cannot meaningfully exceed 4 GiB; setters saturate.
678    size: Cell<u32>,
679    /// Intrusive link into exactly one heap owner list.
680    next: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
681    /// Intrusive link into the collector's grayagain-style revisit list.
682    gray_next: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
683}
684
685const HDR_FINALIZED: u8 = 1;
686const HDR_COLLECTED: u8 = 2;
687const HDR_GRAY_LISTED: u8 = 4;
688/// Set by sweep under `LUA_RS_GC_QUARANTINE=1` instead of freeing the box.
689/// Debug builds assert this bit is clear on every `Gc` dereference and on
690/// every `Marker::mark_box` visit, turning use-after-sweep into a
691/// deterministic panic with a backtrace (see `Heap::release_box`).
692const HDR_FREED: u8 = 8;
693
694impl GcHeader {
695    fn new_white(size: usize, color: Color, flags: u8) -> Self {
696        Self {
697            color: Cell::new(color),
698            age: Cell::new(GcAge::New),
699            flags: Cell::new(flags),
700            size: Cell::new(size.min(u32::MAX as usize) as u32),
701            next: Cell::new(None),
702            gray_next: Cell::new(None),
703        }
704    }
705
706    fn flag(&self, bit: u8) -> bool {
707        self.flags.get() & bit != 0
708    }
709
710    fn set_flag(&self, bit: u8, on: bool) {
711        let f = self.flags.get();
712        self.flags.set(if on { f | bit } else { f & !bit });
713    }
714
715    pub fn finalized(&self) -> bool {
716        self.flag(HDR_FINALIZED)
717    }
718
719    pub fn set_finalized(&self, finalized: bool) {
720        self.set_flag(HDR_FINALIZED, finalized);
721    }
722
723    pub fn collected(&self) -> bool {
724        self.flag(HDR_COLLECTED)
725    }
726
727    pub fn gray_listed(&self) -> bool {
728        self.flag(HDR_GRAY_LISTED)
729    }
730
731    pub fn set_gray_listed(&self, listed: bool) {
732        self.set_flag(HDR_GRAY_LISTED, listed);
733    }
734
735    pub fn size(&self) -> usize {
736        self.size.get() as usize
737    }
738
739    pub fn set_size(&self, size: usize) {
740        self.size.set(size.min(u32::MAX as usize) as u32);
741    }
742}
743
744/// A heap-allocated, GC-tracked value plus its header.
745#[repr(C)]
746pub struct GcBox<T: ?Sized> {
747    header: GcHeader,
748    value: T,
749}
750
751impl<T: ?Sized> GcBox<T> {
752    pub fn header(&self) -> &GcHeader {
753        &self.header
754    }
755    pub fn value(&self) -> &T {
756        &self.value
757    }
758}
759
760/// A GC-managed pointer. Copy + Clone (one-machine-word). Replaces `GcRef<T>`.
761pub struct Gc<T: ?Sized> {
762    ptr: NonNull<GcBox<T>>,
763    /// Marker so `Gc<T>` is treated as if it owns `T` for variance.
764    _marker: PhantomData<T>,
765}
766
767// SAFETY: Gc is just a pointer. The Cell-based interior mutability of the
768// header is single-threaded (the entire Lua runtime is single-threaded), so
769// no Send/Sync impls are needed and we don't provide them.
770impl<T: ?Sized> Copy for Gc<T> {}
771impl<T: ?Sized> Clone for Gc<T> {
772    fn clone(&self) -> Self {
773        *self
774    }
775}
776
777impl<T: ?Sized> PartialEq for Gc<T> {
778    fn eq(&self, other: &Self) -> bool {
779        std::ptr::addr_eq(self.ptr.as_ptr(), other.ptr.as_ptr())
780    }
781}
782impl<T: ?Sized> Eq for Gc<T> {}
783
784impl<T: ?Sized> std::hash::Hash for Gc<T> {
785    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
786        self.ptr.as_ptr().hash(state)
787    }
788}
789
790impl<T: ?Sized> std::fmt::Debug for Gc<T> {
791    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
792        write!(f, "Gc({:p})", self.ptr.as_ptr())
793    }
794}
795
796impl<T: Trace + 'static> Gc<T> {
797    /// Allocate a `GcBox<T>` outside any heap registry. Used by legacy
798    /// `GcRef::new` call sites until Phase D-1b migrates them. The returned
799    /// `Gc<T>` is reachable only through the caller's own retention path;
800    /// without joining a heap owner list, it will never be swept (so
801    /// effectively leaks until process exit — same as Rc behavior).
802    pub fn new_uncollected(value: T) -> Self {
803        let size = std::mem::size_of::<T>();
804        let boxed = Box::new(GcBox {
805            header: GcHeader::new_white(size, Color::White0, 0),
806            value,
807        });
808        Gc {
809            ptr: NonNull::new(Box::into_raw(boxed)).expect("Box::into_raw is non-null"),
810            _marker: PhantomData,
811        }
812    }
813
814    /// Erased heap-list pointer for collector-owned intrusive bookkeeping.
815    pub fn as_trace_ptr(self) -> NonNull<GcBox<dyn Trace>> {
816        self.ptr
817    }
818}
819
820impl<T: ?Sized> Gc<T> {
821    /// Two `Gc<T>`s are identity-equal iff they point at the same box.
822    pub fn ptr_eq(a: Self, b: Self) -> bool {
823        std::ptr::addr_eq(a.ptr.as_ptr(), b.ptr.as_ptr())
824    }
825
826    /// Identity as a usize — usable as a hash table key for "is the *same
827    /// object*" lookups.
828    pub fn identity(self) -> usize {
829        self.ptr.as_ptr() as *const () as usize
830    }
831
832    /// Access the underlying value. Returns `&T` so callers can read fields
833    /// without taking the `Gc` apart. Interior mutability lives inside T's
834    /// own fields (Cell, RefCell, etc.).
835    fn as_box(&self) -> &GcBox<T> {
836        // SAFETY: A Gc<T> is constructed only by allocate() or
837        // new_uncollected(), both of which produce a valid GcBox. The box
838        // outlives the Gc until sweep, which only frees boxes not reachable
839        // from any root — so as long as this Gc is on the stack or in a
840        // traced field, the box is live.
841        let bx = unsafe { self.ptr.as_ref() };
842        debug_assert!(
843            !bx.header.flag(HDR_FREED),
844            "use-after-sweep: Gc<{}> dereferenced after the collector swept it \
845             (caught by LUA_RS_GC_QUARANTINE; this is a rooting bug — the object \
846             was reachable by execution but not by the root trace)",
847            std::any::type_name::<T>()
848        );
849        bx
850    }
851
852    fn header(&self) -> &GcHeader {
853        &self.as_box().header
854    }
855
856    pub fn color(self) -> Color {
857        self.header().color.get()
858    }
859
860    pub fn set_color(self, color: Color) {
861        self.header().color.set(color);
862    }
863
864    pub fn age(self) -> GcAge {
865        self.header().age.get()
866    }
867
868    pub fn set_age(self, age: GcAge) {
869        self.header().age.set(age);
870    }
871
872    pub fn is_finalized(self) -> bool {
873        self.header().finalized()
874    }
875
876    pub fn set_finalized(self, finalized: bool) {
877        self.header().set_finalized(finalized);
878    }
879
880    /// Charge (`delta > 0`) or refund (`delta < 0`) `delta` bytes of this
881    /// object's owned heap buffers against the pacer, keeping `header.size`
882    /// as the single source of truth for what sweep will refund.
883    ///
884    /// No-op when `delta == 0` or when this box is not on a heap owner list
885    /// (`collected == false`): an uncollected box is never swept, so charging
886    /// it would permanently inflate the byte counter. On the collected path,
887    /// `header.size` and the heap's byte counter move together, so after sweep
888    /// frees this box it refunds exactly the bytes that were charged here.
889    pub fn account_buffer(&self, heap: &Heap, delta: isize) {
890        if delta == 0 {
891            return;
892        }
893        let header = self.header();
894        if !header.collected() {
895            return;
896        }
897        if delta >= 0 {
898            header.set_size(header.size().saturating_add(delta as usize));
899        } else {
900            header.set_size(header.size().saturating_sub((-delta) as usize));
901        }
902        heap.adjust_bytes(delta);
903    }
904}
905
906impl<T: ?Sized> std::ops::Deref for Gc<T> {
907    type Target = T;
908    fn deref(&self) -> &T {
909        &self.as_box().value
910    }
911}
912
913impl<T: ?Sized> AsRef<T> for Gc<T> {
914    fn as_ref(&self) -> &T {
915        &self.as_box().value
916    }
917}
918
919/// Every GC-rooted type implements `Trace` to expose its `Gc<_>` fields.
920///
921/// The `trace` method visits every reachable `Gc<_>` and calls
922/// `Marker::mark` on it. For container fields (`Vec<LuaValue>`, etc.) call
923/// `field.trace(m)` to delegate.
924///
925/// # Mechanical pattern
926///
927/// ```ignore
928/// impl Trace for LuaTable {
929///     fn trace(&self, m: &mut Marker) {
930///         for v in self.array.iter() { v.trace(m); }
931///         if let Some(mt) = self.metatable { m.mark(mt); }
932///     }
933/// }
934/// ```
935pub trait Trace {
936    fn trace(&self, m: &mut Marker);
937
938    /// Concrete Rust type name for diagnostic/testC telemetry
939    /// ([`Heap::type_name_count`]). Collector behavior must not branch on
940    /// this. The default covers container blanket impls, which are never
941    /// GC-boxed directly; concrete runtime types override it with
942    /// `std::any::type_name::<Self>()`.
943    fn type_name(&self) -> &'static str {
944        "unknown"
945    }
946}
947
948// Common blanket impls so most container types Just Work.
949impl<T: Trace> Trace for Vec<T> {
950    fn trace(&self, m: &mut Marker) {
951        for item in self.iter() {
952            item.trace(m);
953        }
954    }
955}
956
957impl<T: Trace> Trace for Option<T> {
958    fn trace(&self, m: &mut Marker) {
959        if let Some(v) = self {
960            v.trace(m);
961        }
962    }
963}
964
965impl<T: Trace + ?Sized> Trace for Box<T> {
966    fn trace(&self, m: &mut Marker) {
967        (**self).trace(m);
968    }
969}
970
971impl<T: Trace + ?Sized> Trace for std::rc::Rc<T> {
972    fn trace(&self, m: &mut Marker) {
973        (**self).trace(m);
974    }
975}
976
977impl<T: Trace> Trace for RefCell<T> {
978    fn trace(&self, m: &mut Marker) {
979        self.borrow().trace(m);
980    }
981}
982
983/// `Gc<T>` is itself traceable: marking it forwards to the contained `T`.
984impl<T: Trace + 'static> Trace for Gc<T> {
985    fn trace(&self, m: &mut Marker) {
986        m.mark(*self);
987    }
988}
989
990// Trivially-traceable primitive types: visiting does nothing.
991macro_rules! trace_noop {
992    ($($t:ty),*) => {
993        $(impl Trace for $t {
994            fn trace(&self, _m: &mut Marker) {}
995        })*
996    };
997}
998trace_noop!(
999    bool, u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64, char, String,
1000    str
1001);
1002
1003impl<T> Trace for std::marker::PhantomData<T> {
1004    fn trace(&self, _m: &mut Marker) {}
1005}
1006
1007/// Diagnostic counters for the latest mark phase.
1008///
1009/// These are read-only telemetry for testC/canaries and unit tests. Collector
1010/// decisions must continue to use object color/age metadata, not these counts.
1011#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
1012pub struct MarkerStats {
1013    pub marked: usize,
1014    pub marked_young: usize,
1015    pub marked_old: usize,
1016    pub traced: usize,
1017    pub traced_young: usize,
1018    pub traced_old: usize,
1019}
1020
1021/// Diagnostic counters for the latest sweep phase.
1022#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
1023pub struct SweepStats {
1024    pub visited: usize,
1025    pub visited_young: usize,
1026    pub visited_old: usize,
1027    pub revisit: usize,
1028    pub freed: usize,
1029    pub freed_bytes: usize,
1030}
1031
1032impl SweepStats {
1033    fn record_visit(&mut self, age: GcAge) {
1034        self.visited += 1;
1035        if age.is_old() {
1036            self.visited_old += 1;
1037        } else {
1038            self.visited_young += 1;
1039        }
1040    }
1041
1042    fn record_free(&mut self, bytes: usize) {
1043        self.freed += 1;
1044        self.freed_bytes += bytes;
1045    }
1046
1047    fn add(&mut self, other: Self) {
1048        self.visited += other.visited;
1049        self.visited_young += other.visited_young;
1050        self.visited_old += other.visited_old;
1051        self.revisit += other.revisit;
1052        self.freed += other.freed;
1053        self.freed_bytes += other.freed_bytes;
1054    }
1055}
1056
1057struct OldRevisitTracker {
1058    old_revisit_ids: Vec<usize>,
1059    processed_ids: Vec<usize>,
1060}
1061
1062impl OldRevisitTracker {
1063    fn new(old_revisit: &[NonNull<GcBox<dyn Trace>>]) -> Option<Self> {
1064        if old_revisit.is_empty() {
1065            return None;
1066        }
1067        let mut old_revisit_ids: Vec<usize> = old_revisit
1068            .iter()
1069            .map(|ptr| ptr.as_ptr() as *const () as usize)
1070            .collect();
1071        old_revisit_ids.sort_unstable();
1072        old_revisit_ids.dedup();
1073        Some(Self {
1074            old_revisit_ids,
1075            processed_ids: Vec::new(),
1076        })
1077    }
1078
1079    #[inline(always)]
1080    fn record_processed(&mut self, id: usize) {
1081        if self.old_revisit_ids.binary_search(&id).is_ok() {
1082            self.processed_ids.push(id);
1083        }
1084    }
1085
1086    fn finish(&mut self) {
1087        self.processed_ids.sort_unstable();
1088        self.processed_ids.dedup();
1089    }
1090
1091    #[inline(always)]
1092    fn was_processed(&self, id: usize) -> bool {
1093        self.processed_ids.binary_search(&id).is_ok()
1094    }
1095}
1096
1097/// Diagnostic counts for the allgc list split by generational cursors.
1098#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
1099pub struct AllGcCohortStats {
1100    pub new: usize,
1101    pub survival: usize,
1102    pub old1: usize,
1103    pub old: usize,
1104}
1105
1106#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1107enum MarkerMode {
1108    Full,
1109    Minor,
1110}
1111
1112/// Holds the gray queue during a mark phase. Passed to `Trace::trace`.
1113pub struct Marker {
1114    gray_queue: Vec<NonNull<GcBox<dyn Trace>>>,
1115    visited: IdentityHashSet,
1116    stats: MarkerStats,
1117    mode: MarkerMode,
1118}
1119
1120impl Marker {
1121    fn new_with_capacity(mode: MarkerMode, capacity: usize) -> Self {
1122        Self {
1123            gray_queue: Vec::with_capacity(256),
1124            visited: IdentityHashSet::with_capacity_and_hasher(
1125                capacity,
1126                IdentityBuildHasher::default(),
1127            ),
1128            stats: MarkerStats::default(),
1129            mode,
1130        }
1131    }
1132
1133    fn should_trace_age(&self, age: GcAge) -> bool {
1134        match self.mode {
1135            MarkerMode::Full => true,
1136            MarkerMode::Minor => !matches!(age, GcAge::Old),
1137        }
1138    }
1139
1140    /// Mark a `Gc<T>` as gray (reachable, but its outgoing edges not yet
1141    /// traced). Called by `Trace::trace` implementations.
1142    ///
1143    /// Per-cycle dedup uses `visited` (a HashSet of box identities) rather
1144    /// than the color flag. Color-based dedup would silently skip
1145    /// `new_uncollected` boxes left Black by the previous cycle — those
1146    /// allocations are NOT on a heap owner list, so the start-of-mark
1147    /// "reset heap-owned objects to White" loop does not reach them, and a Black
1148    /// uncollected box would be skipped without re-tracing its children
1149    /// (causing reachable allgc descendants to be swept). The visited set
1150    /// is rebuilt every `full_collect` (Marker::new), so this dedup is
1151    /// always per-cycle.
1152    pub fn mark<T: Trace + 'static>(&mut self, gc: Gc<T>) {
1153        let ptr: NonNull<GcBox<dyn Trace>> = gc.ptr;
1154        self.mark_box(ptr, gc.header(), gc.identity());
1155    }
1156
1157    fn mark_box(&mut self, ptr: NonNull<GcBox<dyn Trace>>, header: &GcHeader, id: usize) {
1158        debug_assert!(
1159            !header.flag(HDR_FREED),
1160            "GC marker reached a quarantined (swept) object at {id:#x} — a root \
1161             traced a stale GcRef (caught by LUA_RS_GC_QUARANTINE; bug-B class: \
1162             garbage slot fed into the marker)"
1163        );
1164        if self.visited.insert(id) {
1165            let age = header.age.get();
1166            self.stats.marked += 1;
1167            if age.is_old() {
1168                self.stats.marked_old += 1;
1169            } else {
1170                self.stats.marked_young += 1;
1171            }
1172            if self.should_trace_age(age) {
1173                header.color.set(Color::Gray);
1174                self.gray_queue.push(ptr);
1175            }
1176        }
1177    }
1178
1179    /// Record that an Rc-backed object (`GcRef<T>` in Phase A-D-0) has been
1180    /// visited and return whether this is the first visit. Used by recursive
1181    /// `Trace` impls to break cycles while the real `Gc<T>` gray-queue path is
1182    /// not yet wired (e.g. `_G._G == _G` would otherwise infinitely recurse).
1183    pub fn try_visit(&mut self, addr: usize) -> bool {
1184        self.visited.insert(addr)
1185    }
1186
1187    /// True iff `id` was reached during the mark phase. Used by the
1188    /// post-mark hook (`Heap::full_collect_with_post_mark`) to decide whether
1189    /// a weak-table entry's target is still strongly reachable. Read-only —
1190    /// callers cannot add entries.
1191    pub fn is_visited(&self, id: usize) -> bool {
1192        self.visited.contains(&id)
1193    }
1194
1195    /// True when the object was marked in this cycle, or when a minor cycle
1196    /// deliberately skipped an old object that the young sweep will not free.
1197    pub fn is_marked_or_old<T: Trace + 'static>(&self, gc: Gc<T>) -> bool {
1198        self.is_visited(gc.identity())
1199            || (matches!(self.mode, MarkerMode::Minor) && gc.age().is_old())
1200    }
1201
1202    /// Number of objects marked so far. Used by the post-mark hook's
1203    /// ephemeron-convergence fixed-point loop to detect when an iteration
1204    /// added no new reachable objects and the loop can terminate.
1205    pub fn visited_count(&self) -> usize {
1206        self.visited.len()
1207    }
1208
1209    /// Return diagnostic counters for the current mark phase.
1210    pub fn stats(&self) -> MarkerStats {
1211        self.stats
1212    }
1213
1214    /// Drain the gray queue, transitively marking children. Each gray box
1215    /// becomes black; its `Trace::trace` is called so the children it points
1216    /// at get pushed onto the queue. Repeats until the queue is empty.
1217    ///
1218    /// Exposed for the post-mark hook so it can run ephemeron convergence:
1219    /// after marking new values via [`Marker::mark`] (or `value.trace(self)`),
1220    /// the hook calls `drain_gray_queue` to propagate the new reachability,
1221    /// then re-checks for fixed-point.
1222    pub fn drain_gray_queue(&mut self) {
1223        while let Some(gray_ptr) = self.gray_queue.pop() {
1224            unsafe {
1225                let bx = gray_ptr.as_ref();
1226                self.stats.traced += 1;
1227                if bx.header.age.get().is_old() {
1228                    self.stats.traced_old += 1;
1229                } else {
1230                    self.stats.traced_young += 1;
1231                }
1232                bx.header.color.set(Color::Black);
1233                bx.value.trace(self);
1234            }
1235        }
1236    }
1237}
1238
1239/// Phases of the incremental collection cycle.
1240///
1241/// The state machine matches a simplified subset of C-Lua's `lgc.c` FSM and
1242/// is driven by [`Heap::incremental_step_with_post_mark`].
1243///
1244/// Transitions:
1245/// - `Pause` → `Propagate` (on first step: reset colors, trace roots).
1246/// - `Propagate` → `EnterAtomic` (when the gray queue empties).
1247/// - `EnterAtomic` → `Atomic` (atomic phase is about to run).
1248/// - `Atomic` → `SweepAllGc` (post-mark hook has run; sweep cursor is initialized).
1249/// - `SweepAllGc` → `SweepFinObj` (allgc sweep cursor reached the end).
1250/// - `SweepFinObj` → `SweepToBeFnz` (finobj sweep cursor reached the end).
1251/// - `SweepToBeFnz` → `SweepEnd` (tobefnz sweep cursor reached the end).
1252/// - `SweepEnd` → `CallFin` (finish sweep bookkeeping).
1253/// - `CallFin` → `Pause` (cycle is complete).
1254///
1255/// `Collecting` is kept as a compatibility alias for the old API (used by
1256/// `barrier`) — it means "anything but Pause."
1257#[derive(Copy, Clone, PartialEq, Eq, Debug)]
1258pub enum GcState {
1259    Pause,
1260    Propagate,
1261    EnterAtomic,
1262    Atomic,
1263    SweepAllGc,
1264    SweepFinObj,
1265    SweepToBeFnz,
1266    SweepEnd,
1267    CallFin,
1268}
1269
1270impl GcState {
1271    pub fn is_pause(self) -> bool {
1272        matches!(self, GcState::Pause)
1273    }
1274    pub fn is_propagate(self) -> bool {
1275        matches!(self, GcState::Propagate)
1276    }
1277    pub fn is_invariant(self) -> bool {
1278        matches!(
1279            self,
1280            GcState::Propagate | GcState::EnterAtomic | GcState::Atomic
1281        )
1282    }
1283    pub fn is_sweep(self) -> bool {
1284        matches!(
1285            self,
1286            GcState::SweepAllGc | GcState::SweepFinObj | GcState::SweepToBeFnz | GcState::SweepEnd
1287        )
1288    }
1289}
1290
1291/// Outcome of one call to [`Heap::incremental_step_with_post_mark`].
1292#[derive(Copy, Clone, PartialEq, Eq, Debug)]
1293pub enum StepOutcome {
1294    /// The step finished a cycle. The heap is back at `GcState::Pause`.
1295    Paused,
1296    /// The step performed work but the cycle is not finished. Caller may
1297    /// step again.
1298    InProgress,
1299    /// The heap is paused (via [`Heap::pause`]) or the caller asked for zero
1300    /// budget while no cycle was in progress and no work was needed.
1301    SkippedStopped,
1302}
1303
1304/// Work budget for one incremental step.
1305///
1306/// `remaining_work` counts down by one for each unit of work performed (one
1307/// gray object traced, one swept node visited, one finalizer dispatched).
1308/// `max_credit` caps how negative `remaining_work` may be allowed to go — a
1309/// step that overshoots its budget rolls the overrun into the caller's debt
1310/// rather than letting unbounded work happen in one call.
1311#[derive(Copy, Clone, Debug)]
1312pub struct StepBudget {
1313    pub remaining_work: isize,
1314    pub max_credit: isize,
1315}
1316
1317impl StepBudget {
1318    /// Build a budget from a number of allowed work units.
1319    pub fn from_work(work: isize) -> Self {
1320        Self {
1321            remaining_work: work.max(1),
1322            max_credit: work.max(1),
1323        }
1324    }
1325}
1326
1327/// The heap. One per `GlobalState`. Owns the intrusive allgc list of every
1328/// allocated `GcBox`, tracks total bytes, and runs collections.
1329/// Floor for the post-collection threshold. Without it, a tight
1330/// allocation loop drives the live set near zero, so `bytes * pause/100`
1331/// collapses toward zero and a full stop-the-world collection fires every
1332/// few allocations, re-tracing all roots each time (issue #38, GC path).
1333/// The floor bounds the wasted work: the collector waits until at least
1334/// this many bytes of garbage accumulate before collecting a small heap.
1335///
1336/// Raised from 256 KB to 1 MB once table array/node buffer bytes became
1337/// honestly accounted (see [`Gc::account_buffer`]): with real buffer bytes
1338/// flowing into the pacer, a 256 KB floor fires too eagerly on table-heavy
1339/// workloads, re-tracing all roots each time. 1 MB keeps the small-heap
1340/// over-collection guard while letting honest pressure drive the threshold.
1341const GC_MIN_THRESHOLD: usize = 1024 * 1024;
1342
1343pub struct Heap {
1344    /// Head of the singly-linked allgc list (heap-owned objects not currently
1345    /// registered for finalization).
1346    head: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
1347    /// Head of the singly-linked finobj list (objects registered for `__gc`).
1348    finobj: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
1349    /// Head of the singly-linked tobefnz list (objects awaiting `__gc`).
1350    tobefnz: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
1351    /// First object that survived one minor collection. Objects before this
1352    /// cursor are the current nursery/new generation.
1353    survival: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
1354    /// First object that survived two minor collections. Objects from
1355    /// `survival` up to this cursor are the survival generation.
1356    old1: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
1357    /// First regular old object. Objects from `old1` up to this cursor became
1358    /// old in the previous young collection.
1359    reallyold: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
1360    /// First OLD1 object when one may appear before the `old1` cursor due to
1361    /// barriers aging objects in younger list segments.
1362    firstold1: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
1363    /// First survival object in the finobj list.
1364    finobjsur: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
1365    /// First old1 object in the finobj list.
1366    finobjold1: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
1367    /// First really-old object in the finobj list.
1368    finobjrold: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
1369    /// Total bytes allocated (sum of header sizes; rough).
1370    bytes: Cell<usize>,
1371    /// Number of currently heap-owned GC boxes across all owner lists.
1372    objects: Cell<usize>,
1373    /// White bit used for new allocations and for survivors after a sweep.
1374    current_white: Cell<Color>,
1375    /// Heap-owned allocation tokens keyed by box address. Weak handles store
1376    /// these tokens so address reuse after sweep cannot resurrect a stale weak
1377    /// target.
1378    allocation_tokens: RefCell<IdentityHashMap<usize>>,
1379    /// Next non-zero token for a collected allocation.
1380    next_allocation_token: Cell<usize>,
1381    /// Threshold above which `step` triggers a collection.
1382    threshold: Cell<usize>,
1383    /// HARDMEMTESTS-style stress mode (env `LUA_RS_GC_STRESS=1`, read once
1384    /// at construction): `would_collect` fires at every checkpoint, so a
1385    /// collection happens at essentially every allocation boundary. Turns
1386    /// GC-cadence-dependent anchoring bugs (objects reachable from Rust
1387    /// locals but not from roots) into deterministic failures — pair with
1388    /// an ASAN build. Debug instrument only; never set in benchmarks.
1389    stress: bool,
1390    /// Quarantine mode (env `LUA_RS_GC_QUARANTINE=1`, read once at
1391    /// construction): sweep unlinks dead boxes but parks them on the
1392    /// `quarantined` list with `HDR_FREED` set instead of freeing, so a
1393    /// use-after-sweep dereference reads intact (still-allocated) memory and
1394    /// trips a `debug_assert` in `Gc::as_box` / `Marker::mark_box` — a
1395    /// deterministic Rust panic with a backtrace, no ASAN or nightly needed.
1396    /// All sweep accounting (byte refunds, token removal, object counts) is
1397    /// unchanged so collection cadence is identical to a normal run. Memory
1398    /// grows without bound; debug-build test instrument only — the asserts
1399    /// compile out in release. Pair with `LUA_RS_GC_STRESS=1`.
1400    quarantine: bool,
1401    /// Intrusive list of swept-but-not-freed boxes under quarantine mode,
1402    /// linked through `header.next` (unused once unlinked from the owner
1403    /// list). Freed for real in `drop_all`.
1404    quarantined: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
1405    /// Multiplier on bytes_used to set next threshold after collection.
1406    pause_multiplier: Cell<usize>,
1407    /// State machine for the incremental collector.
1408    state: Cell<GcState>,
1409    /// If true, `step` and `barrier` are no-ops (for bootstrap before the
1410    /// world is consistent).
1411    paused: Cell<bool>,
1412    /// Counter of completed collections performed (for diagnostics).
1413    collections: Cell<usize>,
1414    /// Counter of completed young-generation collections.
1415    minor_collections: Cell<usize>,
1416    /// Counter of explicit stop-the-world full-collection requests.
1417    full_collections: Cell<usize>,
1418    /// Diagnostic counters from the most recently completed mark phase.
1419    last_mark_stats: Cell<MarkerStats>,
1420    /// Diagnostic counters from the most recent sweep phase.
1421    last_sweep_stats: Cell<SweepStats>,
1422    /// Intrusive grayagain-style list of objects that young collections must
1423    /// revisit even if they are not reached through normal roots: OLD0/OLD1
1424    /// and touched old objects.
1425    grayagain: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
1426    /// In-progress marker state for incremental cycles. `Some` between
1427    /// `Propagate` start and `Sweep` start; `None` otherwise.
1428    marker: RefCell<Option<Marker>>,
1429    /// Recycled mark-phase buffers (gray queue + visited set). A mark phase
1430    /// sizes its visited set to the live-object count (hundreds of KB);
1431    /// without pooling, binarytrees churned 396 such buffers for 249 MB of
1432    /// allocator traffic (dhat, 2026-06-10). One buffer pair is kept and
1433    /// reused across cycles; capacity follows the heap's high-water mark.
1434    marker_pool: RefCell<Option<(Vec<NonNull<GcBox<dyn Trace>>>, IdentityHashSet)>>,
1435    /// Sweep cursor. Points at the `Cell` whose `Option<NonNull>` is the
1436    /// "current" link being inspected during the sweep phase. Encoded as a
1437    /// raw pointer because the cell lives inside a `GcHeader` (Cell, not Cell<Cell>).
1438    /// `None` means: sweep starts from `self.head`.
1439    sweep_prev_next: Cell<Option<NonNull<Cell<Option<NonNull<GcBox<dyn Trace>>>>>>>,
1440}
1441
1442impl Default for Heap {
1443    fn default() -> Self {
1444        Self::new()
1445    }
1446}
1447
1448impl Heap {
1449    pub fn new() -> Self {
1450        Self {
1451            head: Cell::new(None),
1452            finobj: Cell::new(None),
1453            tobefnz: Cell::new(None),
1454            survival: Cell::new(None),
1455            old1: Cell::new(None),
1456            reallyold: Cell::new(None),
1457            firstold1: Cell::new(None),
1458            finobjsur: Cell::new(None),
1459            finobjold1: Cell::new(None),
1460            finobjrold: Cell::new(None),
1461            bytes: Cell::new(0),
1462            objects: Cell::new(0),
1463            current_white: Cell::new(Color::White0),
1464            allocation_tokens: RefCell::new(IdentityHashMap::default()),
1465            next_allocation_token: Cell::new(1),
1466            threshold: Cell::new(64 * 1024), // initial threshold: 64 KB
1467            stress: std::env::var_os("LUA_RS_GC_STRESS").is_some_and(|v| v == "1"),
1468            quarantine: std::env::var_os("LUA_RS_GC_QUARANTINE").is_some_and(|v| v == "1"),
1469            quarantined: Cell::new(None),
1470            pause_multiplier: Cell::new(200), // 200% = collect when bytes 2x threshold
1471            state: Cell::new(GcState::Pause),
1472            paused: Cell::new(true), // start paused; caller enables when world is consistent
1473            collections: Cell::new(0),
1474            minor_collections: Cell::new(0),
1475            full_collections: Cell::new(0),
1476            last_mark_stats: Cell::new(MarkerStats::default()),
1477            last_sweep_stats: Cell::new(SweepStats::default()),
1478            grayagain: Cell::new(None),
1479            marker: RefCell::new(None),
1480            marker_pool: RefCell::new(None),
1481            sweep_prev_next: Cell::new(None),
1482        }
1483    }
1484
1485    /// Enable collection. Until this is called, `step` is a no-op (so the
1486    /// runtime can bootstrap without prematurely freeing objects).
1487    pub fn unpause(&self) {
1488        self.paused.set(false);
1489    }
1490
1491    pub fn is_paused(&self) -> bool {
1492        self.paused.get()
1493    }
1494
1495    /// Allocate a new `GcBox<T>` and prepend it to the allgc chain.
1496    pub fn allocate<T: Trace + 'static>(&self, value: T) -> Gc<T> {
1497        let size = std::mem::size_of::<GcBox<T>>();
1498        let boxed = Box::new(GcBox {
1499            header: GcHeader::new_white(size, self.current_white.get(), HDR_COLLECTED),
1500            value,
1501        });
1502        boxed.header.next.set(self.head.get());
1503        let raw: *mut GcBox<T> = Box::into_raw(boxed);
1504        let ptr: NonNull<GcBox<T>> = NonNull::new(raw).expect("Box::into_raw is non-null");
1505        let dyn_ptr: NonNull<GcBox<dyn Trace>> = ptr;
1506        let identity = ptr.as_ptr() as *const () as usize;
1507        let token = self.next_token();
1508        self.allocation_tokens.borrow_mut().insert(identity, token);
1509        self.head.set(Some(dyn_ptr));
1510        self.bytes.set(self.bytes.get() + size);
1511        self.objects.set(self.objects.get() + 1);
1512        Gc {
1513            ptr,
1514            _marker: PhantomData,
1515        }
1516    }
1517
1518    /// Bytes currently retained by GC-tracked objects (rough estimate).
1519    pub fn bytes_used(&self) -> usize {
1520        self.bytes.get()
1521    }
1522
1523    /// Adjust the heap's pacer byte counter by a signed delta, saturating at
1524    /// zero. Used by [`Gc::account_buffer`] to charge or refund the bytes of
1525    /// an object's owned heap buffers (table array/node Vecs) so collections
1526    /// fire at honest memory pressure rather than only on header sizes.
1527    pub fn adjust_bytes(&self, delta: isize) {
1528        if delta >= 0 {
1529            self.bytes
1530                .set(self.bytes.get().saturating_add(delta as usize));
1531        } else {
1532            self.bytes
1533                .set(self.bytes.get().saturating_sub((-delta) as usize));
1534        }
1535    }
1536
1537    /// Current collection threshold in bytes. When `bytes_used() >= threshold_bytes()`,
1538    /// the next `step()` will run a full collection (unless paused). Used by
1539    /// callers that want to short-circuit expensive prep work (e.g. snapshotting
1540    /// weak tables / pending finalizers) when no collection will actually fire.
1541    pub fn threshold_bytes(&self) -> usize {
1542        self.threshold.get()
1543    }
1544
1545    /// Override the next automatic collection threshold.
1546    ///
1547    /// The VM uses this when Lua-level GC pacing (`GCdebt`, minor-debt, and
1548    /// pause-debt calculations) has already computed a byte threshold from the
1549    /// collector-owned live-byte counter.
1550    pub fn set_threshold_bytes(&self, threshold: usize) {
1551        self.threshold.set(threshold.max(1));
1552    }
1553
1554    /// Cheap predicate: would a `step()` actually do work? Equivalent to
1555    /// `!paused && bytes_used() >= threshold_bytes()`. Callers that build
1556    /// snapshot state before invoking the heap should gate on this.
1557    pub fn would_collect(&self) -> bool {
1558        if self.stress {
1559            return !self.paused.get();
1560        }
1561        !self.paused.get() && self.bytes.get() >= self.threshold.get()
1562    }
1563
1564    pub fn collections(&self) -> usize {
1565        self.collections.get()
1566    }
1567
1568    pub fn minor_collections(&self) -> usize {
1569        self.minor_collections.get()
1570    }
1571
1572    pub fn full_collections(&self) -> usize {
1573        self.full_collections.get()
1574    }
1575
1576    pub fn last_mark_stats(&self) -> MarkerStats {
1577        self.last_mark_stats.get()
1578    }
1579
1580    pub fn last_sweep_stats(&self) -> SweepStats {
1581        self.last_sweep_stats.get()
1582    }
1583
1584    pub fn allgc_cohort_stats(&self) -> AllGcCohortStats {
1585        let survival = self.survival.get();
1586        let old1 = self.old1.get();
1587        let reallyold = self.reallyold.get();
1588        let mut stats = AllGcCohortStats::default();
1589        let mut cursor = self.head.get();
1590        let mut seen = IdentityHashSet::default();
1591        let mut cohort = 0u8;
1592        while let Some(ptr) = cursor {
1593            let id = ptr.as_ptr() as *const () as usize;
1594            if !seen.insert(id) {
1595                break;
1596            }
1597            if Some(ptr) == reallyold {
1598                cohort = 3;
1599            } else if Some(ptr) == old1 {
1600                cohort = 2;
1601            } else if Some(ptr) == survival {
1602                cohort = 1;
1603            }
1604            match cohort {
1605                0 => stats.new += 1,
1606                1 => stats.survival += 1,
1607                2 => stats.old1 += 1,
1608                _ => stats.old += 1,
1609            }
1610            cursor = self.header_from_ptr(ptr).next.get();
1611        }
1612        stats
1613    }
1614
1615    fn next_token(&self) -> usize {
1616        let token = self.next_allocation_token.get().max(1);
1617        let next = token.checked_add(1).unwrap_or(1).max(1);
1618        self.next_allocation_token.set(next);
1619        token
1620    }
1621
1622    fn current_white(&self) -> Color {
1623        self.current_white.get()
1624    }
1625
1626    fn other_white(&self) -> Color {
1627        self.current_white.get().other_white()
1628    }
1629
1630    fn flip_current_white(&self) {
1631        self.current_white.set(self.other_white());
1632    }
1633
1634    fn for_each_list_header(
1635        &self,
1636        head: Option<NonNull<GcBox<dyn Trace>>>,
1637        f: &mut impl FnMut(&GcHeader),
1638    ) {
1639        let mut cursor = head;
1640        while let Some(ptr) = cursor {
1641            let header = self.header_from_ptr(ptr);
1642            cursor = header.next.get();
1643            f(header);
1644        }
1645    }
1646
1647    fn for_each_header(&self, mut f: impl FnMut(&GcHeader)) {
1648        self.for_each_list_header(self.head.get(), &mut f);
1649        self.for_each_list_header(self.finobj.get(), &mut f);
1650        self.for_each_list_header(self.tobefnz.get(), &mut f);
1651    }
1652
1653    fn header_from_ptr<'a>(&'a self, ptr: NonNull<GcBox<dyn Trace>>) -> &'a GcHeader {
1654        unsafe { &(*ptr.as_ptr()).header }
1655    }
1656
1657    /// The single point where a swept box leaves the heap. The caller must
1658    /// already have unlinked `ptr` from its owner list and settled all
1659    /// accounting (byte refund, token removal, object count). Under
1660    /// quarantine mode the box is poisoned and parked instead of freed, so
1661    /// later use-after-sweep dereferences hit intact memory and the
1662    /// `HDR_FREED` debug asserts instead of undefined behavior.
1663    fn release_box(&self, ptr: NonNull<GcBox<dyn Trace>>) {
1664        if self.quarantine {
1665            let header = self.header_from_ptr(ptr);
1666            header.set_flag(HDR_FREED, true);
1667            header.next.set(self.quarantined.get());
1668            self.quarantined.set(Some(ptr));
1669        } else {
1670            // SAFETY: the caller unlinked `ptr` from its owner list, so no
1671            // heap chain reaches it; only stale (buggy) GcRefs could. This
1672            // is the sole runtime free of a GcBox.
1673            unsafe {
1674                let _ = Box::from_raw(ptr.as_ptr());
1675            }
1676        }
1677    }
1678
1679    fn clear_generation_cursors(&self) {
1680        self.survival.set(None);
1681        self.old1.set(None);
1682        self.reallyold.set(None);
1683        self.firstold1.set(None);
1684        self.finobjsur.set(None);
1685        self.finobjold1.set(None);
1686        self.finobjrold.set(None);
1687        self.clear_grayagain();
1688    }
1689
1690    fn set_all_cursors_to_head(&self) {
1691        let head = self.head.get();
1692        self.survival.set(head);
1693        self.old1.set(head);
1694        self.reallyold.set(head);
1695        self.firstold1.set(None);
1696        let finobj = self.finobj.get();
1697        self.finobjsur.set(finobj);
1698        self.finobjold1.set(finobj);
1699        self.finobjrold.set(finobj);
1700        self.clear_grayagain();
1701    }
1702
1703    fn correct_generation_pointers(
1704        &self,
1705        removed: NonNull<GcBox<dyn Trace>>,
1706        next: Option<NonNull<GcBox<dyn Trace>>>,
1707    ) {
1708        if self.survival.get() == Some(removed) {
1709            self.survival.set(next);
1710        }
1711        if self.old1.get() == Some(removed) {
1712            self.old1.set(next);
1713        }
1714        if self.reallyold.get() == Some(removed) {
1715            self.reallyold.set(next);
1716        }
1717        if self.firstold1.get() == Some(removed) {
1718            self.firstold1.set(next);
1719        }
1720        if self.finobjsur.get() == Some(removed) {
1721            self.finobjsur.set(next);
1722        }
1723        if self.finobjold1.get() == Some(removed) {
1724            self.finobjold1.set(next);
1725        }
1726        if self.finobjrold.get() == Some(removed) {
1727            self.finobjrold.set(next);
1728        }
1729        if self.header_from_ptr(removed).gray_listed() {
1730            self.unlink_grayagain(removed);
1731        }
1732    }
1733
1734    fn unlink_from_list(
1735        &self,
1736        list: &Cell<Option<NonNull<GcBox<dyn Trace>>>>,
1737        ptr: NonNull<GcBox<dyn Trace>>,
1738    ) -> bool {
1739        let mut prev_cell = list;
1740        loop {
1741            let Some(current) = prev_cell.get() else {
1742                return false;
1743            };
1744            let header = self.header_from_ptr(current);
1745            let next = header.next.get();
1746            if std::ptr::addr_eq(current.as_ptr(), ptr.as_ptr()) {
1747                prev_cell.set(next);
1748                let prev_next_ptr = NonNull::from(prev_cell);
1749                let removed_next_ptr = NonNull::from(&self.header_from_ptr(ptr).next);
1750                if self.sweep_prev_next.get() == Some(removed_next_ptr) {
1751                    self.sweep_prev_next.set(Some(prev_next_ptr));
1752                }
1753                self.correct_generation_pointers(ptr, next);
1754                header.next.set(None);
1755                return true;
1756            }
1757            prev_cell = &header.next;
1758        }
1759    }
1760
1761    fn link_to_head(
1762        &self,
1763        list: &Cell<Option<NonNull<GcBox<dyn Trace>>>>,
1764        ptr: NonNull<GcBox<dyn Trace>>,
1765    ) {
1766        let header = self.header_from_ptr(ptr);
1767        header.next.set(list.get());
1768        list.set(Some(ptr));
1769    }
1770
1771    fn link_to_tail(
1772        &self,
1773        list: &Cell<Option<NonNull<GcBox<dyn Trace>>>>,
1774        ptr: NonNull<GcBox<dyn Trace>>,
1775    ) {
1776        let mut last_cell = list;
1777        loop {
1778            let Some(current) = last_cell.get() else {
1779                let header = self.header_from_ptr(ptr);
1780                header.next.set(None);
1781                last_cell.set(Some(ptr));
1782                return;
1783            };
1784            last_cell = &self.header_from_ptr(current).next;
1785        }
1786    }
1787
1788    pub fn move_allgc_to_finobj(&self, ptr: NonNull<GcBox<dyn Trace>>) -> bool {
1789        let header = self.header_from_ptr(ptr);
1790        if !header.collected() {
1791            return false;
1792        }
1793        if !self.unlink_from_list(&self.head, ptr) {
1794            return false;
1795        }
1796        if self.state.get().is_sweep() {
1797            header.color.set(self.current_white());
1798        }
1799        self.link_to_head(&self.finobj, ptr);
1800        true
1801    }
1802
1803    pub fn move_finobj_to_tobefnz(&self, ptr: NonNull<GcBox<dyn Trace>>) -> bool {
1804        if !self.unlink_from_list(&self.finobj, ptr) {
1805            return false;
1806        }
1807        self.link_to_tail(&self.tobefnz, ptr);
1808        true
1809    }
1810
1811    pub fn move_tobefnz_to_allgc(&self, ptr: NonNull<GcBox<dyn Trace>>) -> bool {
1812        let header = self.header_from_ptr(ptr);
1813        if !self.unlink_from_list(&self.tobefnz, ptr) {
1814            return false;
1815        }
1816        if self.state.get().is_sweep() {
1817            header.color.set(self.current_white());
1818        }
1819        self.link_to_head(&self.head, ptr);
1820        if header.age.get() == GcAge::Old1 {
1821            self.firstold1.set(Some(ptr));
1822        }
1823        true
1824    }
1825
1826    fn remember_minor_revisit(&self, ptr: NonNull<GcBox<dyn Trace>>) {
1827        let header = self.header_from_ptr(ptr);
1828        if header.gray_listed() {
1829            return;
1830        }
1831        header.gray_next.set(self.grayagain.get());
1832        header.set_gray_listed(true);
1833        self.grayagain.set(Some(ptr));
1834    }
1835
1836    fn mark_minor_revisit_objects(&self, marker: &mut Marker) {
1837        let mut cursor = self.grayagain.get();
1838        while let Some(ptr) = cursor {
1839            let header = self.header_from_ptr(ptr);
1840            cursor = header.gray_next.get();
1841            let id = ptr.as_ptr() as *const () as usize;
1842            marker.mark_box(ptr, header, id);
1843        }
1844    }
1845
1846    fn clear_grayagain(&self) {
1847        let mut cursor = self.grayagain.get();
1848        self.grayagain.set(None);
1849        while let Some(ptr) = cursor {
1850            let header = self.header_from_ptr(ptr);
1851            cursor = header.gray_next.get();
1852            header.gray_next.set(None);
1853            header.set_gray_listed(false);
1854        }
1855    }
1856
1857    fn take_grayagain(&self) -> Vec<NonNull<GcBox<dyn Trace>>> {
1858        let mut objects = Vec::new();
1859        let mut cursor = self.grayagain.get();
1860        self.grayagain.set(None);
1861        while let Some(ptr) = cursor {
1862            let header = self.header_from_ptr(ptr);
1863            cursor = header.gray_next.get();
1864            header.gray_next.set(None);
1865            header.set_gray_listed(false);
1866            objects.push(ptr);
1867        }
1868        objects
1869    }
1870
1871    fn replace_grayagain(&self, objects: Vec<NonNull<GcBox<dyn Trace>>>) {
1872        self.clear_grayagain();
1873        for ptr in objects.into_iter().rev() {
1874            self.remember_minor_revisit(ptr);
1875        }
1876    }
1877
1878    fn unlink_grayagain(&self, removed: NonNull<GcBox<dyn Trace>>) {
1879        let keep = self
1880            .take_grayagain()
1881            .into_iter()
1882            .filter(|ptr| !std::ptr::addr_eq(ptr.as_ptr(), removed.as_ptr()))
1883            .collect();
1884        self.replace_grayagain(keep);
1885    }
1886
1887    pub fn grayagain_count(&self) -> usize {
1888        let mut count = 0usize;
1889        let mut cursor = self.grayagain.get();
1890        while let Some(ptr) = cursor {
1891            count += 1;
1892            cursor = self.header_from_ptr(ptr).gray_next.get();
1893        }
1894        count
1895    }
1896
1897    /// Return the current heap token for a live allocation identity.
1898    ///
1899    /// Weak handles use this before sweep to capture their target allocation
1900    /// without dereferencing stale pointers later.
1901    pub fn allocation_token(&self, identity: usize) -> Option<usize> {
1902        self.allocation_tokens.borrow().get(&identity).copied()
1903    }
1904
1905    /// Return true when `identity` still names the same heap allocation.
1906    ///
1907    /// The token check prevents allocator address reuse from making a stale
1908    /// weak handle look live again.
1909    pub fn contains_allocation(&self, identity: usize, token: usize) -> bool {
1910        self.allocation_token(identity) == Some(token)
1911    }
1912
1913    /// Forward write barrier: invoked when `parent` (already-traced black
1914    /// object) gains a new reference to `child`. To preserve the tri-color
1915    /// invariant ("no black points to white"), we mark the child gray
1916    /// immediately. Cheap: one branch + maybe one queue push.
1917    ///
1918    /// During incremental mode this prevents the marking phase from missing
1919    /// the new edge. In current stop-the-world mode it's still correct (a
1920    /// no-op when the collection is idle), so call sites can be wired now
1921    /// and the incremental upgrade is mechanical later.
1922    pub fn barrier<P, C>(&self, parent: Gc<P>, child: Gc<C>)
1923    where
1924        P: Trace + 'static,
1925        C: Trace + 'static,
1926    {
1927        if self.paused.get() || self.state.get().is_pause() {
1928            return;
1929        }
1930        if parent.header().color.get() != Color::Black {
1931            return;
1932        }
1933        if !child.header().color.get().is_white() {
1934            return;
1935        }
1936        child.header().color.set(Color::Gray);
1937        if let Ok(mut m_opt) = self.marker.try_borrow_mut() {
1938            if let Some(m) = m_opt.as_mut() {
1939                let ptr: NonNull<GcBox<dyn Trace>> = child.ptr;
1940                m.gray_queue.push(ptr);
1941                m.visited.insert(child.identity());
1942            }
1943        }
1944    }
1945
1946    /// Backward barrier: if a black object receives a reference to a white
1947    /// child, gray the parent so the in-progress cycle will rescan it.
1948    pub fn barrier_back<P, C>(&self, parent: Gc<P>, child: Gc<C>)
1949    where
1950        P: Trace + 'static,
1951        C: Trace + 'static,
1952    {
1953        if self.paused.get() || self.state.get().is_pause() {
1954            return;
1955        }
1956        if parent.header().color.get() != Color::Black {
1957            return;
1958        }
1959        if !child.header().color.get().is_white() {
1960            return;
1961        }
1962        parent.header().color.set(Color::Gray);
1963        if let Ok(mut m_opt) = self.marker.try_borrow_mut() {
1964            if let Some(m) = m_opt.as_mut() {
1965                let ptr: NonNull<GcBox<dyn Trace>> = parent.ptr;
1966                m.gray_queue.push(ptr);
1967                m.visited.insert(parent.identity());
1968            }
1969        }
1970    }
1971
1972    /// Generational forward barrier: if an old object receives a reference to a
1973    /// young object, the child cannot jump directly to OLD because it may still
1974    /// point at younger objects. Lua marks it OLD0 so later young collections
1975    /// advance it through OLD1 to OLD.
1976    pub fn generational_forward_barrier<P, C>(&self, parent: Gc<P>, child: Gc<C>)
1977    where
1978        P: Trace + 'static,
1979        C: Trace + 'static,
1980    {
1981        if parent.age().is_old() && !child.age().is_old() {
1982            child.set_age(GcAge::Old0);
1983            let ptr: NonNull<GcBox<dyn Trace>> = child.ptr;
1984            self.remember_minor_revisit(ptr);
1985        }
1986        self.barrier(parent, child);
1987    }
1988
1989    /// Generational backward barrier: an old object that now points to a young
1990    /// object is revisited by the next young collection. This mirrors
1991    /// `luaC_barrierback_`'s age transition to TOUCHED1.
1992    pub fn generational_backward_barrier<P>(&self, parent: Gc<P>)
1993    where
1994        P: Trace + 'static,
1995    {
1996        if parent.age().is_old() {
1997            parent.set_color(Color::Gray);
1998            parent.set_age(GcAge::Touched1);
1999            let ptr: NonNull<GcBox<dyn Trace>> = parent.ptr;
2000            self.remember_minor_revisit(ptr);
2001        }
2002    }
2003
2004    /// Possibly run a collection. Trigger: bytes_used > threshold.
2005    /// Caller passes the root set (the runtime — typically `GlobalState`
2006    /// implementing `Trace`).
2007    pub fn step(&self, roots: &dyn Trace) {
2008        self.step_with_post_mark(roots, |_: &mut Marker| {});
2009    }
2010
2011    /// Like [`step`] but invokes a `post_mark` hook when a collection
2012    /// actually fires (threshold reached). Hook is a no-op on the
2013    /// short-circuit path. The runtime uses this to bridge weak-table
2014    /// pruning into implicit GC steps fired from inside the VM loop.
2015    pub fn step_with_post_mark<F: FnMut(&mut Marker)>(&self, roots: &dyn Trace, post_mark: F) {
2016        if self.paused.get() {
2017            return;
2018        }
2019        if !self.stress && self.bytes.get() < self.threshold.get() {
2020            return;
2021        }
2022        self.full_collect_with_post_mark(roots, post_mark);
2023    }
2024
2025    /// Stop-the-world full collect. Marks every reachable object from
2026    /// `roots`, then sweeps white (unreachable) boxes from the heap owner lists.
2027    pub fn full_collect(&self, roots: &dyn Trace) {
2028        self.full_collect_with_post_mark(roots, |_: &mut Marker| {});
2029    }
2030
2031    /// Run only the mark/atomic hook portion of a collection, without sweeping.
2032    ///
2033    /// This is used by runtimes that need an atomic reachability snapshot for
2034    /// weak-table cleanup while they are deliberately avoiding object freeing.
2035    pub fn mark_only_with_post_mark<F: FnMut(&mut Marker)>(
2036        &self,
2037        roots: &dyn Trace,
2038        mut post_mark: F,
2039    ) {
2040        if self.paused.get() {
2041            return;
2042        }
2043        let mut marker = self.marker_from_pool(MarkerMode::Full);
2044        roots.trace(&mut marker);
2045        marker.drain_gray_queue();
2046        post_mark(&mut marker);
2047        marker.drain_gray_queue();
2048        self.last_mark_stats.set(marker.stats());
2049        self.recycle_marker(marker);
2050    }
2051
2052    /// Metadata transition used when entering generational mode after a full
2053    /// mark: all currently live objects become old.
2054    pub fn promote_all_to_old(&self) {
2055        self.for_each_header(|header| {
2056            header.age.set(GcAge::Old);
2057            header.color.set(Color::Black);
2058        });
2059        self.set_all_cursors_to_head();
2060    }
2061
2062    /// Metadata transition used when returning to incremental mode: Lua clears
2063    /// age information and treats all objects as new again.
2064    pub fn reset_all_ages(&self) {
2065        let current_white = self.current_white();
2066        self.for_each_header(|header| {
2067            header.age.set(GcAge::New);
2068            header.color.set(current_white);
2069        });
2070        self.clear_generation_cursors();
2071    }
2072
2073    /// Run a complete young-generation collection.
2074    ///
2075    /// This is the first generational path: it uses the normal root tracer for
2076    /// correctness, then limits sweep/freeing to young objects. Later work can
2077    /// replace the full root traversal with cohort-list traversal without
2078    /// changing the age/sweep contract introduced here.
2079    pub fn minor_collect_with_post_mark<F: FnMut(&mut Marker)>(
2080        &self,
2081        roots: &dyn Trace,
2082        mut post_mark: F,
2083    ) {
2084        if self.paused.get() {
2085            return;
2086        }
2087        if !self.state.get().is_pause() {
2088            self.abort_cycle();
2089        }
2090
2091        self.state.set(GcState::Propagate);
2092        let mut marker = self.marker_from_pool(MarkerMode::Minor);
2093        self.last_sweep_stats.set(SweepStats::default());
2094        self.mark_minor_revisit_objects(&mut marker);
2095        roots.trace(&mut marker);
2096        marker.drain_gray_queue();
2097
2098        self.state.set(GcState::EnterAtomic);
2099        self.state.set(GcState::Atomic);
2100        post_mark(&mut marker);
2101        marker.drain_gray_queue();
2102        self.last_mark_stats.set(marker.stats());
2103        self.recycle_marker(marker);
2104
2105        self.state.set(GcState::SweepAllGc);
2106        self.sweep_young();
2107        self.recycle_marker_cell();
2108        self.sweep_prev_next.set(None);
2109        self.state.set(GcState::Pause);
2110        self.collections.set(self.collections.get() + 1);
2111        self.minor_collections.set(self.minor_collections.get() + 1);
2112    }
2113
2114    /// Stop-the-world full collect with a post-mark hook.
2115    ///
2116    /// Internally drives the incremental state machine to completion with
2117    /// an unbounded budget — equivalent to repeatedly calling
2118    /// [`incremental_step_with_post_mark`] until it returns `Paused`. The
2119    /// post-mark hook is invoked exactly once, during the atomic transition.
2120    pub fn full_collect_with_post_mark<F: FnMut(&mut Marker)>(
2121        &self,
2122        roots: &dyn Trace,
2123        mut post_mark: F,
2124    ) {
2125        if self.paused.get() {
2126            return;
2127        }
2128        if !self.state.get().is_pause() {
2129            self.abort_cycle();
2130        }
2131        self.full_collections.set(self.full_collections.get() + 1);
2132        let unlimited = StepBudget {
2133            remaining_work: isize::MAX,
2134            max_credit: isize::MAX,
2135        };
2136        loop {
2137            let outcome = self.incremental_step_with_post_mark(roots, unlimited, &mut post_mark);
2138            if matches!(outcome, StepOutcome::Paused | StepOutcome::SkippedStopped) {
2139                break;
2140            }
2141        }
2142    }
2143
2144    /// Run one budgeted step of the incremental collector.
2145    ///
2146    /// The state machine advances `Pause → Propagate → EnterAtomic → Atomic →
2147    /// SweepAllGc → SweepFinObj → SweepToBeFnz → SweepEnd → CallFin → Pause`.
2148    /// Each phase consumes budget; the call returns when the budget runs out
2149    /// or the cycle reaches `Pause`. The `post_mark`
2150    /// hook is invoked exactly once per cycle, during the `Atomic`
2151    /// transition (after the initial gray-queue drain, before sweep starts).
2152    ///
2153    /// Returns:
2154    /// - [`StepOutcome::Paused`] — the cycle completed.
2155    /// - [`StepOutcome::InProgress`] — budget exhausted before the cycle
2156    ///   finished; caller may step again.
2157    /// - [`StepOutcome::SkippedStopped`] — heap is paused; nothing happened.
2158    pub fn incremental_step_with_post_mark<F: FnMut(&mut Marker)>(
2159        &self,
2160        roots: &dyn Trace,
2161        mut budget: StepBudget,
2162        mut post_mark: F,
2163    ) -> StepOutcome {
2164        if self.paused.get() {
2165            return StepOutcome::SkippedStopped;
2166        }
2167        self.run_budgeted(roots, &mut budget, &mut post_mark);
2168        if self.state.get().is_pause() {
2169            StepOutcome::Paused
2170        } else {
2171            StepOutcome::InProgress
2172        }
2173    }
2174
2175    fn run_budgeted(
2176        &self,
2177        roots: &dyn Trace,
2178        budget: &mut StepBudget,
2179        post_mark: &mut dyn FnMut(&mut Marker),
2180    ) -> bool {
2181        self.run_budgeted_until(roots, budget, post_mark, None)
2182    }
2183
2184    fn run_budgeted_until(
2185        &self,
2186        roots: &dyn Trace,
2187        budget: &mut StepBudget,
2188        post_mark: &mut dyn FnMut(&mut Marker),
2189        stop_at: Option<GcState>,
2190    ) -> bool {
2191        let mut did_work = false;
2192        loop {
2193            if stop_at == Some(self.state.get()) {
2194                return did_work;
2195            }
2196            if budget.remaining_work <= -budget.max_credit {
2197                return did_work;
2198            }
2199            match self.state.get() {
2200                GcState::Pause => {
2201                    self.start_cycle(roots);
2202                    self.state.set(GcState::Propagate);
2203                    budget.remaining_work -= 1;
2204                    did_work = true;
2205                    if stop_at == Some(GcState::Propagate) {
2206                        return did_work;
2207                    }
2208                }
2209                GcState::Propagate => {
2210                    let work = self.drain_gray_budgeted(budget.remaining_work.max(1));
2211                    budget.remaining_work -= work as isize;
2212                    did_work = did_work || work > 0;
2213                    let empty = {
2214                        let m = self.marker.borrow();
2215                        m.as_ref().map(|m| m.gray_queue.is_empty()).unwrap_or(true)
2216                    };
2217                    if empty {
2218                        self.state.set(GcState::EnterAtomic);
2219                        if stop_at == Some(GcState::EnterAtomic) {
2220                            return did_work;
2221                        }
2222                    } else if budget.remaining_work <= 0 {
2223                        return did_work;
2224                    }
2225                }
2226                GcState::EnterAtomic => {
2227                    self.state.set(GcState::Atomic);
2228                    budget.remaining_work -= 1;
2229                    did_work = true;
2230                    if stop_at == Some(GcState::Atomic) || budget.remaining_work <= 0 {
2231                        return did_work;
2232                    }
2233                }
2234                GcState::Atomic => {
2235                    self.run_atomic(post_mark);
2236                    self.state.set(GcState::SweepAllGc);
2237                    budget.remaining_work -= 1;
2238                    did_work = true;
2239                    if stop_at == Some(GcState::SweepAllGc) {
2240                        return did_work;
2241                    }
2242                }
2243                GcState::SweepAllGc => {
2244                    let work = self.sweep_budgeted(budget.remaining_work.max(1));
2245                    budget.remaining_work -= work as isize;
2246                    did_work = did_work || work > 0;
2247                    if self.sweep_prev_next.get().is_none() {
2248                        self.state.set(GcState::SweepFinObj);
2249                        self.sweep_prev_next.set(Some(NonNull::from(&self.finobj)));
2250                        if stop_at == Some(GcState::SweepFinObj) {
2251                            return did_work;
2252                        }
2253                    } else if budget.remaining_work <= 0 {
2254                        return did_work;
2255                    }
2256                }
2257                GcState::SweepFinObj => {
2258                    let work = self.sweep_budgeted(budget.remaining_work.max(1));
2259                    budget.remaining_work -= work as isize;
2260                    did_work = did_work || work > 0;
2261                    if self.sweep_prev_next.get().is_none() {
2262                        self.state.set(GcState::SweepToBeFnz);
2263                        self.sweep_prev_next.set(Some(NonNull::from(&self.tobefnz)));
2264                        if stop_at == Some(GcState::SweepToBeFnz) {
2265                            return did_work;
2266                        }
2267                    } else if budget.remaining_work <= 0 {
2268                        return did_work;
2269                    }
2270                }
2271                GcState::SweepToBeFnz => {
2272                    let work = self.sweep_budgeted(budget.remaining_work.max(1));
2273                    budget.remaining_work -= work as isize;
2274                    did_work = did_work || work > 0;
2275                    if self.sweep_prev_next.get().is_none() {
2276                        self.state.set(GcState::SweepEnd);
2277                        if stop_at == Some(GcState::SweepEnd) {
2278                            return did_work;
2279                        }
2280                    } else if budget.remaining_work <= 0 {
2281                        return did_work;
2282                    }
2283                }
2284                GcState::SweepEnd => {
2285                    self.state.set(GcState::CallFin);
2286                    budget.remaining_work -= 1;
2287                    did_work = true;
2288                    if stop_at == Some(GcState::CallFin) || budget.remaining_work <= 0 {
2289                        return did_work;
2290                    }
2291                }
2292                GcState::CallFin => {
2293                    self.finish_cycle();
2294                    self.state.set(GcState::Pause);
2295                    if stop_at == Some(GcState::Pause) {
2296                        return did_work;
2297                    }
2298                    return did_work;
2299                }
2300            }
2301        }
2302    }
2303
2304    /// Drive an incremental cycle until `target` is entered, stopping before any
2305    /// subsequent phase work. Intended for testC-style inspection of mid-cycle
2306    /// color/barrier invariants; normal collector pacing uses
2307    /// [`Self::incremental_step_with_post_mark`].
2308    pub fn incremental_run_until_state_with_post_mark<F: FnMut(&mut Marker)>(
2309        &self,
2310        roots: &dyn Trace,
2311        target: GcState,
2312        max_work: isize,
2313        mut post_mark: F,
2314    ) -> StepOutcome {
2315        if self.paused.get() {
2316            return StepOutcome::SkippedStopped;
2317        }
2318        let work = max_work.max(1);
2319        let mut budget = StepBudget {
2320            remaining_work: work,
2321            max_credit: work,
2322        };
2323        self.run_budgeted_until(roots, &mut budget, &mut post_mark, Some(target));
2324        if self.state.get().is_pause() {
2325            StepOutcome::Paused
2326        } else {
2327            StepOutcome::InProgress
2328        }
2329    }
2330
2331    /// Take the pooled mark buffers (or build fresh ones sized to the live
2332    /// set). Pair with [`Heap::recycle_marker`] when the mark phase ends.
2333    fn marker_from_pool(&self, mode: MarkerMode) -> Marker {
2334        match self.marker_pool.borrow_mut().take() {
2335            Some((gray_queue, visited)) => Marker {
2336                gray_queue,
2337                visited,
2338                stats: MarkerStats::default(),
2339                mode,
2340            },
2341            None => Marker::new_with_capacity(mode, self.objects.get()),
2342        }
2343    }
2344
2345    fn recycle_marker(&self, marker: Marker) {
2346        let Marker {
2347            mut gray_queue,
2348            mut visited,
2349            ..
2350        } = marker;
2351        gray_queue.clear();
2352        visited.clear();
2353        *self.marker_pool.borrow_mut() = Some((gray_queue, visited));
2354    }
2355
2356    fn recycle_marker_cell(&self) {
2357        if let Some(marker) = self.marker.borrow_mut().take() {
2358            self.recycle_marker(marker);
2359        }
2360    }
2361
2362    fn start_cycle(&self, roots: &dyn Trace) {
2363        self.flip_current_white();
2364        let dead_white = self.other_white();
2365        self.for_each_header(|header| {
2366            header.color.set(dead_white);
2367        });
2368        let mut marker = self.marker_from_pool(MarkerMode::Full);
2369        roots.trace(&mut marker);
2370        *self.marker.borrow_mut() = Some(marker);
2371        self.sweep_prev_next.set(None);
2372    }
2373
2374    fn drain_gray_budgeted(&self, max_units: isize) -> usize {
2375        let mut m_opt = self.marker.borrow_mut();
2376        let marker = match m_opt.as_mut() {
2377            Some(m) => m,
2378            None => return 0,
2379        };
2380        let mut work = 0usize;
2381        let mut budget = max_units;
2382        while budget > 0 {
2383            let next = match marker.gray_queue.pop() {
2384                Some(p) => p,
2385                None => break,
2386            };
2387            unsafe {
2388                let bx = next.as_ref();
2389                marker.stats.traced += 1;
2390                if bx.header.age.get().is_old() {
2391                    marker.stats.traced_old += 1;
2392                } else {
2393                    marker.stats.traced_young += 1;
2394                }
2395                bx.header.color.set(Color::Black);
2396                bx.value.trace(marker);
2397            }
2398            work += 1;
2399            budget -= 1;
2400        }
2401        work
2402    }
2403
2404    fn run_atomic(&self, post_mark: &mut dyn FnMut(&mut Marker)) {
2405        let mut m_opt = self.marker.borrow_mut();
2406        if let Some(marker) = m_opt.as_mut() {
2407            marker.drain_gray_queue();
2408            post_mark(marker);
2409            marker.drain_gray_queue();
2410        }
2411        self.sweep_prev_next.set(Some(NonNull::from(&self.head)));
2412        self.last_sweep_stats.set(SweepStats::default());
2413    }
2414
2415    fn sweep_budgeted(&self, max_units: isize) -> usize {
2416        let mut work = 0usize;
2417        let mut budget = max_units;
2418        let mut freed_bytes = 0usize;
2419        let mut stats = SweepStats::default();
2420        let current_white = self.current_white();
2421        let dead_white = self.other_white();
2422        let mut prev_next_ptr = match self.sweep_prev_next.get() {
2423            Some(p) => p,
2424            None => return 0,
2425        };
2426        while budget > 0 {
2427            let prev_cell = unsafe { prev_next_ptr.as_ref() };
2428            let cursor = prev_cell.get();
2429            let ptr = match cursor {
2430                Some(p) => p,
2431                None => {
2432                    self.sweep_prev_next.set(None);
2433                    break;
2434                }
2435            };
2436            let header = self.header_from_ptr(ptr);
2437            let next = header.next.get();
2438            let age = header.age.get();
2439            stats.record_visit(age);
2440            let color = header.color.get();
2441            if color == dead_white {
2442                prev_cell.set(next);
2443                let size = header.size();
2444                freed_bytes += size;
2445                stats.record_free(size);
2446                self.correct_generation_pointers(ptr, next);
2447                self.allocation_tokens
2448                    .borrow_mut()
2449                    .remove(&(ptr.as_ptr() as *const () as usize));
2450                self.objects.set(self.objects.get().saturating_sub(1));
2451                self.release_box(ptr);
2452            } else {
2453                if matches!(color, Color::Black | Color::Gray) {
2454                    header.color.set(current_white);
2455                }
2456                prev_next_ptr = unsafe { NonNull::from(&(*ptr.as_ptr()).header.next) };
2457                self.sweep_prev_next.set(Some(prev_next_ptr));
2458            }
2459            work += 1;
2460            budget -= 1;
2461        }
2462        if freed_bytes > 0 {
2463            self.bytes.set(self.bytes.get().saturating_sub(freed_bytes));
2464        }
2465        if stats.visited > 0 {
2466            let mut total = self.last_sweep_stats.get();
2467            total.add(stats);
2468            self.last_sweep_stats.set(total);
2469        }
2470        work
2471    }
2472
2473    fn push_next_revisit(
2474        next_revisit: &mut Vec<NonNull<GcBox<dyn Trace>>>,
2475        seen: &mut IdentityHashSet,
2476        ptr: NonNull<GcBox<dyn Trace>>,
2477        age: GcAge,
2478    ) {
2479        if matches!(
2480            age,
2481            GcAge::Old0 | GcAge::Old1 | GcAge::Touched1 | GcAge::Touched2
2482        ) {
2483            let id = ptr.as_ptr() as *const () as usize;
2484            if seen.insert(id) {
2485                next_revisit.push(ptr);
2486            }
2487        }
2488    }
2489
2490    fn sweep_young_range(
2491        &self,
2492        mut prev_next_ptr: NonNull<Cell<Option<NonNull<GcBox<dyn Trace>>>>>,
2493        limit: Option<NonNull<GcBox<dyn Trace>>>,
2494        next_revisit: &mut Vec<NonNull<GcBox<dyn Trace>>>,
2495        next_revisit_seen: &mut IdentityHashSet,
2496        processed: &mut Option<OldRevisitTracker>,
2497        firstold1: &mut Option<NonNull<GcBox<dyn Trace>>>,
2498        freed_bytes: &mut usize,
2499        stats: &mut SweepStats,
2500    ) -> (
2501        NonNull<Cell<Option<NonNull<GcBox<dyn Trace>>>>>,
2502        Option<NonNull<GcBox<dyn Trace>>>,
2503    ) {
2504        let current_white = self.current_white();
2505        loop {
2506            let prev_cell = unsafe { prev_next_ptr.as_ref() };
2507            let Some(ptr) = prev_cell.get() else {
2508                return (prev_next_ptr, None);
2509            };
2510            if Some(ptr) == limit {
2511                return (prev_next_ptr, Some(ptr));
2512            }
2513            let header = self.header_from_ptr(ptr);
2514            let next = header.next.get();
2515            let age = header.age.get();
2516            stats.record_visit(age);
2517            if let Some(processed) = processed.as_mut() {
2518                processed.record_processed(ptr.as_ptr() as *const () as usize);
2519            }
2520            if header.color.get().is_white() && !age.is_old() {
2521                prev_cell.set(next);
2522                let size = header.size();
2523                *freed_bytes += size;
2524                stats.record_free(size);
2525                self.correct_generation_pointers(ptr, next);
2526                self.allocation_tokens
2527                    .borrow_mut()
2528                    .remove(&(ptr.as_ptr() as *const () as usize));
2529                self.objects.set(self.objects.get().saturating_sub(1));
2530                self.release_box(ptr);
2531                continue;
2532            }
2533
2534            if !header.color.get().is_white() {
2535                let next_age = age.next_after_minor();
2536                header.age.set(next_age);
2537                if next_age == GcAge::Old1 && firstold1.is_none() {
2538                    *firstold1 = Some(ptr);
2539                }
2540                match age {
2541                    GcAge::New => header.color.set(current_white),
2542                    GcAge::Touched1 | GcAge::Touched2 => header.color.set(Color::Black),
2543                    _ => {}
2544                }
2545                Self::push_next_revisit(next_revisit, next_revisit_seen, ptr, next_age);
2546            }
2547            prev_next_ptr = unsafe { NonNull::from(&(*ptr.as_ptr()).header.next) };
2548        }
2549    }
2550
2551    fn sweep_young(&self) {
2552        let mut freed_bytes = 0usize;
2553        let mut next_revisit = Vec::new();
2554        let mut next_revisit_seen = IdentityHashSet::default();
2555        let mut firstold1 = None;
2556        let mut stats = SweepStats::default();
2557        let old_revisit = self.take_grayagain();
2558        let mut processed = OldRevisitTracker::new(&old_revisit);
2559        let survival = self.survival.get();
2560        let old1 = self.old1.get();
2561
2562        let (psurvival, new_old1) = self.sweep_young_range(
2563            NonNull::from(&self.head),
2564            survival,
2565            &mut next_revisit,
2566            &mut next_revisit_seen,
2567            &mut processed,
2568            &mut firstold1,
2569            &mut freed_bytes,
2570            &mut stats,
2571        );
2572        self.sweep_young_range(
2573            psurvival,
2574            old1,
2575            &mut next_revisit,
2576            &mut next_revisit_seen,
2577            &mut processed,
2578            &mut firstold1,
2579            &mut freed_bytes,
2580            &mut stats,
2581        );
2582
2583        let finobjsur = self.finobjsur.get();
2584        let finobjold1 = self.finobjold1.get();
2585        let mut dummy_firstold1 = None;
2586        let (pfinobjsur, new_finobjold1) = self.sweep_young_range(
2587            NonNull::from(&self.finobj),
2588            finobjsur,
2589            &mut next_revisit,
2590            &mut next_revisit_seen,
2591            &mut processed,
2592            &mut dummy_firstold1,
2593            &mut freed_bytes,
2594            &mut stats,
2595        );
2596        self.sweep_young_range(
2597            pfinobjsur,
2598            finobjold1,
2599            &mut next_revisit,
2600            &mut next_revisit_seen,
2601            &mut processed,
2602            &mut dummy_firstold1,
2603            &mut freed_bytes,
2604            &mut stats,
2605        );
2606        self.sweep_young_range(
2607            NonNull::from(&self.tobefnz),
2608            None,
2609            &mut next_revisit,
2610            &mut next_revisit_seen,
2611            &mut processed,
2612            &mut dummy_firstold1,
2613            &mut freed_bytes,
2614            &mut stats,
2615        );
2616
2617        if let Some(processed) = processed.as_mut() {
2618            processed.finish();
2619        }
2620
2621        for ptr in old_revisit {
2622            let id = ptr.as_ptr() as *const () as usize;
2623            if processed
2624                .as_ref()
2625                .is_some_and(|processed| processed.was_processed(id))
2626            {
2627                continue;
2628            }
2629            stats.revisit += 1;
2630            let header = self.header_from_ptr(ptr);
2631            if header.color.get().is_white() {
2632                continue;
2633            }
2634            let age = header.age.get();
2635            let next_age = age.next_after_minor();
2636            header.age.set(next_age);
2637            if next_age == GcAge::Old1 && firstold1.is_none() {
2638                firstold1 = Some(ptr);
2639            }
2640            if matches!(age, GcAge::Touched1 | GcAge::Touched2) {
2641                header.color.set(Color::Black);
2642            }
2643            Self::push_next_revisit(&mut next_revisit, &mut next_revisit_seen, ptr, next_age);
2644        }
2645
2646        if freed_bytes > 0 {
2647            self.bytes.set(self.bytes.get().saturating_sub(freed_bytes));
2648        }
2649        self.replace_grayagain(next_revisit);
2650        self.reallyold.set(old1);
2651        self.old1.set(new_old1);
2652        self.survival.set(self.head.get());
2653        self.firstold1.set(firstold1);
2654        self.finobjrold.set(finobjold1);
2655        self.finobjold1.set(new_finobjold1);
2656        self.finobjsur.set(self.finobj.get());
2657        self.last_sweep_stats.set(stats);
2658    }
2659
2660    fn finish_cycle(&self) {
2661        let stats = self
2662            .marker
2663            .borrow()
2664            .as_ref()
2665            .map(|marker| marker.stats())
2666            .unwrap_or_default();
2667        self.last_mark_stats.set(stats);
2668        self.recycle_marker_cell();
2669        self.sweep_prev_next.set(None);
2670        let next = self.bytes.get().saturating_mul(self.pause_multiplier.get()) / 100;
2671        self.threshold.set(next.max(GC_MIN_THRESHOLD));
2672        self.collections.set(self.collections.get() + 1);
2673    }
2674
2675    /// Finish an idle `CallFin` phase after the runtime has drained any
2676    /// pending to-be-finalized objects.
2677    pub fn finish_callfin_phase(&self) -> bool {
2678        if self.state.get() != GcState::CallFin {
2679            return false;
2680        }
2681        self.finish_cycle();
2682        self.state.set(GcState::Pause);
2683        true
2684    }
2685
2686    fn abort_cycle(&self) {
2687        if !self.state.get().is_pause() {
2688            self.recycle_marker_cell();
2689            self.sweep_prev_next.set(None);
2690            let current_white = self.current_white();
2691            self.for_each_header(|header| {
2692                header.color.set(current_white);
2693            });
2694            self.state.set(GcState::Pause);
2695        }
2696    }
2697
2698    /// Returns the current state of the incremental collector.
2699    pub fn gc_state(&self) -> GcState {
2700        self.state.get()
2701    }
2702
2703    /// Approximate number of live GC boxes across all heap owner lists.
2704    pub fn allgc_count(&self) -> usize {
2705        self.objects.get()
2706    }
2707
2708    /// Count live allgc objects whose concrete Rust type name matches
2709    /// `predicate`. This is diagnostic/testC telemetry only; collector logic
2710    /// must not depend on Rust type names.
2711    pub fn type_name_count(&self, mut predicate: impl FnMut(&'static str) -> bool) -> usize {
2712        let mut count = 0usize;
2713        for head in [self.head.get(), self.finobj.get(), self.tobefnz.get()] {
2714            let mut cursor = head;
2715            while let Some(ptr) = cursor {
2716                let bx = unsafe { ptr.as_ref() };
2717                cursor = bx.header.next.get();
2718                if predicate(bx.value().type_name()) {
2719                    count += 1;
2720                }
2721            }
2722        }
2723        count
2724    }
2725
2726    /// Drop every allocation, ignoring reachability. Called at shutdown.
2727    /// After this returns, every outstanding `Gc<T>` is dangling — callers
2728    /// must ensure no `Gc<T>` outlives the `Heap`.
2729    pub fn drop_all(&self) {
2730        self.recycle_marker_cell();
2731        self.sweep_prev_next.set(None);
2732        self.clear_generation_cursors();
2733        self.state.set(GcState::Pause);
2734        self.allocation_tokens.borrow_mut().clear();
2735        self.drop_list(&self.head);
2736        self.drop_list(&self.finobj);
2737        self.drop_list(&self.tobefnz);
2738        self.drop_list(&self.quarantined);
2739        self.bytes.set(0);
2740        self.objects.set(0);
2741    }
2742
2743    fn drop_list(&self, list: &Cell<Option<NonNull<GcBox<dyn Trace>>>>) {
2744        let mut cursor = list.get();
2745        list.set(None);
2746        while let Some(ptr) = cursor {
2747            // SAFETY: same chain invariant as full_collect's sweep.
2748            let next = unsafe {
2749                let next = (*ptr.as_ptr()).header.next.get();
2750                let _ = Box::from_raw(ptr.as_ptr());
2751                next
2752            };
2753            cursor = next;
2754        }
2755    }
2756}
2757
2758impl Drop for Heap {
2759    fn drop(&mut self) {
2760        self.drop_all();
2761    }
2762}
2763
2764// ──────────────────────────────────────────────────────────────────────────
2765// Tests — confirm the skeleton's invariants before agents ever touch it.
2766// ──────────────────────────────────────────────────────────────────────────
2767
2768#[cfg(test)]
2769mod tests {
2770    use super::*;
2771
2772    /// A tiny GC-tracked type for the smoke test.
2773    struct Cell0 {
2774        next: Cell<Option<Gc<Cell0>>>,
2775        marker_calls: Cell<usize>,
2776    }
2777
2778    impl Trace for Cell0 {
2779        fn trace(&self, m: &mut Marker) {
2780            self.marker_calls.set(self.marker_calls.get() + 1);
2781            if let Some(n) = self.next.get() {
2782                m.mark(n);
2783            }
2784        }
2785    }
2786
2787    /// Roots for tests: just a single Gc<Cell0>, or none.
2788    struct OneRoot(Option<Gc<Cell0>>);
2789    impl Trace for OneRoot {
2790        fn trace(&self, m: &mut Marker) {
2791            if let Some(g) = self.0 {
2792                m.mark(g);
2793            }
2794        }
2795    }
2796
2797    struct TwoRoots {
2798        first: Option<Gc<Cell0>>,
2799        second: Option<Gc<Cell0>>,
2800    }
2801
2802    impl Trace for TwoRoots {
2803        fn trace(&self, m: &mut Marker) {
2804            if let Some(g) = self.first {
2805                m.mark(g);
2806            }
2807            if let Some(g) = self.second {
2808                m.mark(g);
2809            }
2810        }
2811    }
2812
2813    #[derive(Clone)]
2814    struct FinalizerCell {
2815        id: usize,
2816        age: GcAge,
2817        finalized: std::rc::Rc<Cell<bool>>,
2818    }
2819
2820    impl FinalizerCell {
2821        fn new(id: usize) -> Self {
2822            Self {
2823                id,
2824                age: GcAge::New,
2825                finalized: std::rc::Rc::new(Cell::new(false)),
2826            }
2827        }
2828    }
2829
2830    impl FinalizerEntry for FinalizerCell {
2831        fn identity(&self) -> usize {
2832            self.id
2833        }
2834
2835        fn age(&self) -> GcAge {
2836            self.age
2837        }
2838
2839        fn is_finalized(&self) -> bool {
2840            self.finalized.get()
2841        }
2842
2843        fn set_finalized(&self, finalized: bool) {
2844            self.finalized.set(finalized);
2845        }
2846    }
2847
2848    fn finalizer_ids(objects: &[FinalizerCell]) -> Vec<usize> {
2849        objects.iter().map(|object| object.id).collect()
2850    }
2851
2852    #[derive(Clone)]
2853    struct WeakCell {
2854        id: usize,
2855        live: bool,
2856        kind: WeakListKind,
2857    }
2858
2859    impl WeakEntry for WeakCell {
2860        type Strong = usize;
2861
2862        fn identity(&self) -> usize {
2863            self.id
2864        }
2865
2866        fn list_kind(&self) -> WeakListKind {
2867            self.kind
2868        }
2869
2870        fn upgrade(&self) -> Option<Self::Strong> {
2871            self.live.then_some(self.id)
2872        }
2873    }
2874
2875    #[test]
2876    fn weak_registry_dedups_snapshots_and_retains_live_ids() {
2877        let mut registry = WeakRegistry::default();
2878        registry.push_unique(WeakCell {
2879            id: 1,
2880            live: true,
2881            kind: WeakListKind::WeakValues,
2882        });
2883        registry.push_unique(WeakCell {
2884            id: 1,
2885            live: true,
2886            kind: WeakListKind::Ephemeron,
2887        });
2888        registry.push_unique(WeakCell {
2889            id: 2,
2890            live: false,
2891            kind: WeakListKind::AllWeak,
2892        });
2893        registry.push_unique(WeakCell {
2894            id: 3,
2895            live: true,
2896            kind: WeakListKind::WeakValues,
2897        });
2898
2899        let stats = registry.stats();
2900        assert_eq!(stats.weak_values, 1);
2901        assert_eq!(stats.ephemeron, 1);
2902        assert_eq!(stats.all_weak, 1);
2903
2904        let snapshot = registry.live_snapshot();
2905        assert_eq!(snapshot, vec![3, 1]);
2906        assert_eq!(
2907            registry.stats(),
2908            WeakRegistryStats {
2909                tracked: 3,
2910                snapshot_live: 2,
2911                snapshot_dead: 1,
2912                retained: 2,
2913                weak_values: 1,
2914                ephemeron: 1,
2915                all_weak: 0,
2916            }
2917        );
2918
2919        let keep: std::collections::HashSet<usize> = [3usize].into_iter().collect();
2920        registry.retain_identities(&keep);
2921        assert_eq!(registry.len(), 1);
2922        assert_eq!(registry.stats().retained, 1);
2923        assert_eq!(registry.stats().weak_values, 1);
2924        registry.remove_identity(3);
2925        assert_eq!(registry.len(), 0);
2926    }
2927
2928    #[test]
2929    fn finalizer_registry_tracks_generational_cohorts() {
2930        let mut registry = FinalizerRegistry::default();
2931        registry.push_pending_unique(FinalizerCell::new(1));
2932        registry.push_pending_unique(FinalizerCell::new(2));
2933
2934        let stats = registry.stats();
2935        assert_eq!(stats.finobj_new, 2);
2936        assert_eq!(stats.finobj_survival, 0);
2937        assert_eq!(stats.finobj_old1, 0);
2938        assert_eq!(stats.finobj_reallyold, 0);
2939        assert_eq!(stats.finobj_minor_scan, 2);
2940
2941        registry.finish_minor_collection();
2942        let stats = registry.stats();
2943        assert_eq!(stats.finobj_new, 0);
2944        assert_eq!(stats.finobj_survival, 2);
2945        assert_eq!(stats.finobj_old1, 0);
2946        assert_eq!(stats.finobj_reallyold, 0);
2947        assert_eq!(stats.finobj_minor_scan, 2);
2948
2949        registry.push_pending_unique(FinalizerCell::new(3));
2950        registry.finish_minor_collection();
2951        let stats = registry.stats();
2952        assert_eq!(stats.finobj_new, 0);
2953        assert_eq!(stats.finobj_survival, 1);
2954        assert_eq!(stats.finobj_old1, 2);
2955        assert_eq!(stats.finobj_reallyold, 0);
2956        assert_eq!(stats.finobj_minor_scan, 1);
2957
2958        registry.finish_minor_collection();
2959        let stats = registry.stats();
2960        assert_eq!(stats.finobj_new, 0);
2961        assert_eq!(stats.finobj_survival, 0);
2962        assert_eq!(stats.finobj_old1, 1);
2963        assert_eq!(stats.finobj_reallyold, 2);
2964        assert_eq!(stats.finobj_minor_scan, 0);
2965    }
2966
2967    #[test]
2968    fn finalizer_registry_minor_snapshot_uses_cohort_boundaries() {
2969        let mut registry = FinalizerRegistry::default();
2970        registry.push_pending_unique(FinalizerCell::new(1));
2971        registry.push_pending_unique(FinalizerCell::new(2));
2972        registry.push_pending_unique(FinalizerCell::new(3));
2973        registry.finish_minor_collection();
2974        registry.finish_minor_collection();
2975        registry.push_pending_unique(FinalizerCell::new(4));
2976        registry.push_pending_unique(FinalizerCell::new(5));
2977
2978        assert_eq!(
2979            finalizer_ids(&registry.pending_minor_snapshot()),
2980            vec![4, 5],
2981            "minor finalizer scan must skip the old1/reallyold prefix"
2982        );
2983
2984        registry.push_to_be_finalized(FinalizerCell::new(99));
2985        registry.promote_pending_to_finalized(vec![
2986            FinalizerCell::new(1),
2987            FinalizerCell::new(2),
2988            FinalizerCell::new(4),
2989        ]);
2990
2991        let stats = registry.stats();
2992        assert_eq!(stats.finobj_old1, 1);
2993        assert_eq!(stats.finobj_new, 1);
2994        assert_eq!(stats.finobj_minor_scan, 1);
2995        assert_eq!(finalizer_ids(registry.pending()), vec![3, 5]);
2996        assert_eq!(
2997            finalizer_ids(registry.to_be_finalized()),
2998            vec![99, 4, 2, 1],
2999            "new to-be-finalized batches append behind older queued finalizers"
3000        );
3001    }
3002
3003    #[test]
3004    fn finalizer_registry_marks_and_clears_finalized_bit() {
3005        let mut registry = FinalizerRegistry::default();
3006        let object = FinalizerCell::new(1);
3007
3008        assert!(!object.is_finalized());
3009        registry.push_pending_unique(object.clone());
3010        assert!(object.is_finalized());
3011
3012        registry.push_pending_unique(object.clone());
3013        assert_eq!(registry.pending_len(), 1);
3014
3015        registry.promote_pending_to_finalized(vec![object.clone()]);
3016        assert!(object.is_finalized());
3017        assert_eq!(registry.pending_len(), 0);
3018        assert_eq!(registry.to_be_finalized_len(), 1);
3019
3020        let popped = registry.pop_to_be_finalized().unwrap();
3021        assert_eq!(popped.id, 1);
3022        assert!(!object.is_finalized());
3023
3024        registry.push_pending_unique(object.clone());
3025        assert!(object.is_finalized());
3026        assert_eq!(registry.pending_len(), 1);
3027    }
3028
3029    #[test]
3030    fn alloc_and_drop_all() {
3031        let heap = Heap::new();
3032        heap.unpause();
3033        let _a = heap.allocate(Cell0 {
3034            next: Cell::new(None),
3035            marker_calls: Cell::new(0),
3036        });
3037        let _b = heap.allocate(Cell0 {
3038            next: Cell::new(None),
3039            marker_calls: Cell::new(0),
3040        });
3041        assert!(heap.bytes_used() > 0);
3042        heap.drop_all();
3043        assert_eq!(heap.bytes_used(), 0);
3044    }
3045
3046    fn list_len(heap: &Heap, mut cursor: Option<NonNull<GcBox<dyn Trace>>>) -> usize {
3047        let mut count = 0usize;
3048        while let Some(ptr) = cursor {
3049            count += 1;
3050            cursor = heap.header_from_ptr(ptr).next.get();
3051        }
3052        count
3053    }
3054
3055    #[test]
3056    fn finalizer_intrusive_lists_sweep_and_drop() {
3057        let heap = Heap::new();
3058        heap.unpause();
3059        let _normal = heap.allocate(Cell0 {
3060            next: Cell::new(None),
3061            marker_calls: Cell::new(0),
3062        });
3063        let finobj = heap.allocate(Cell0 {
3064            next: Cell::new(None),
3065            marker_calls: Cell::new(0),
3066        });
3067        let tobefnz = heap.allocate(Cell0 {
3068            next: Cell::new(None),
3069            marker_calls: Cell::new(0),
3070        });
3071
3072        assert!(heap.move_allgc_to_finobj(finobj.as_trace_ptr()));
3073        assert!(heap.move_allgc_to_finobj(tobefnz.as_trace_ptr()));
3074        assert!(heap.move_finobj_to_tobefnz(tobefnz.as_trace_ptr()));
3075        assert_eq!(list_len(&heap, heap.head.get()), 1);
3076        assert_eq!(list_len(&heap, heap.finobj.get()), 1);
3077        assert_eq!(list_len(&heap, heap.tobefnz.get()), 1);
3078        assert_eq!(heap.allgc_count(), 3);
3079
3080        heap.full_collect(&TwoRoots {
3081            first: Some(finobj),
3082            second: Some(tobefnz),
3083        });
3084        assert_eq!(list_len(&heap, heap.head.get()), 0);
3085        assert_eq!(list_len(&heap, heap.finobj.get()), 1);
3086        assert_eq!(list_len(&heap, heap.tobefnz.get()), 1);
3087        assert_eq!(heap.allgc_count(), 2);
3088
3089        assert!(heap.move_tobefnz_to_allgc(tobefnz.as_trace_ptr()));
3090        heap.full_collect(&OneRoot(Some(tobefnz)));
3091        assert_eq!(list_len(&heap, heap.head.get()), 1);
3092        assert_eq!(list_len(&heap, heap.finobj.get()), 0);
3093        assert_eq!(list_len(&heap, heap.tobefnz.get()), 0);
3094        assert_eq!(heap.allgc_count(), 1);
3095
3096        heap.drop_all();
3097        assert_eq!(heap.bytes_used(), 0);
3098    }
3099
3100    #[test]
3101    fn account_buffer_refunded_on_sweep() {
3102        let heap = Heap::new();
3103        heap.unpause();
3104        let baseline = heap.bytes_used();
3105        {
3106            let a = heap.allocate(Cell0 {
3107                next: Cell::new(None),
3108                marker_calls: Cell::new(0),
3109            });
3110            assert!(heap.bytes_used() > baseline);
3111            a.account_buffer(&heap, 4096);
3112            assert_eq!(
3113                a.header().size(),
3114                std::mem::size_of::<GcBox<Cell0>>() + 4096
3115            );
3116        }
3117        // Drop the only root path (a is no longer Trace-visible). The +4096
3118        // must be refunded via header.size when the box is swept.
3119        heap.full_collect(&OneRoot(None));
3120        assert_eq!(
3121            heap.bytes_used(),
3122            baseline,
3123            "account_buffer charge must be fully refunded by sweep"
3124        );
3125    }
3126
3127    #[test]
3128    fn account_buffer_noop_on_uncollected() {
3129        let heap = Heap::new();
3130        heap.unpause();
3131        let g = Gc::new_uncollected(Cell0 {
3132            next: Cell::new(None),
3133            marker_calls: Cell::new(0),
3134        });
3135        let before = heap.bytes_used();
3136        g.account_buffer(&heap, 8192);
3137        assert_eq!(
3138            heap.bytes_used(),
3139            before,
3140            "uncollected box must not charge the pacer"
3141        );
3142    }
3143
3144    #[test]
3145    fn collect_unreachable_frees_bytes() {
3146        let heap = Heap::new();
3147        heap.unpause();
3148        let _a = heap.allocate(Cell0 {
3149            next: Cell::new(None),
3150            marker_calls: Cell::new(0),
3151        });
3152        let bytes_before = heap.bytes_used();
3153        assert!(bytes_before > 0);
3154        // No roots — everything should sweep.
3155        heap.full_collect(&OneRoot(None));
3156        assert_eq!(heap.bytes_used(), 0);
3157        assert_eq!(heap.collections(), 1);
3158    }
3159
3160    #[test]
3161    fn collect_keeps_reachable() {
3162        let heap = Heap::new();
3163        heap.unpause();
3164        let root = heap.allocate(Cell0 {
3165            next: Cell::new(None),
3166            marker_calls: Cell::new(0),
3167        });
3168        let bytes_before = heap.bytes_used();
3169        heap.full_collect(&OneRoot(Some(root)));
3170        assert_eq!(heap.bytes_used(), bytes_before);
3171        assert_eq!(root.marker_calls.get(), 1);
3172    }
3173
3174    #[test]
3175    fn allocations_start_new_and_white() {
3176        let heap = Heap::new();
3177        heap.unpause();
3178        let obj = heap.allocate(Cell0 {
3179            next: Cell::new(None),
3180            marker_calls: Cell::new(0),
3181        });
3182        assert_eq!(obj.age(), GcAge::New);
3183        assert!(obj.color().is_white());
3184    }
3185
3186    #[test]
3187    fn allocation_tokens_track_exact_live_box() {
3188        let heap = Heap::new();
3189        heap.unpause();
3190        let obj = heap.allocate(Cell0 {
3191            next: Cell::new(None),
3192            marker_calls: Cell::new(0),
3193        });
3194        let id = obj.identity();
3195        let token = heap
3196            .allocation_token(id)
3197            .expect("heap allocation should get a token");
3198
3199        assert!(heap.contains_allocation(id, token));
3200        assert!(!heap.contains_allocation(id, token + 1));
3201
3202        heap.full_collect(&OneRoot(None));
3203        assert_eq!(heap.allocation_token(id), None);
3204        assert!(!heap.contains_allocation(id, token));
3205    }
3206
3207    #[test]
3208    fn allocation_during_incremental_sweep_survives_current_cycle() {
3209        let heap = Heap::new();
3210        heap.unpause();
3211        let old_dead = heap.allocate(Cell0 {
3212            next: Cell::new(None),
3213            marker_calls: Cell::new(0),
3214        });
3215        let old_id = old_dead.identity();
3216
3217        let outcome = heap.incremental_run_until_state_with_post_mark(
3218            &OneRoot(None),
3219            GcState::SweepAllGc,
3220            1024,
3221            |_| {},
3222        );
3223        assert_eq!(outcome, StepOutcome::InProgress);
3224        assert_eq!(heap.gc_state(), GcState::SweepAllGc);
3225
3226        let new_during_sweep = heap.allocate(Cell0 {
3227            next: Cell::new(None),
3228            marker_calls: Cell::new(0),
3229        });
3230        let new_id = new_during_sweep.identity();
3231
3232        loop {
3233            let outcome = heap.incremental_step_with_post_mark(
3234                &OneRoot(None),
3235                StepBudget::from_work(64),
3236                |_| {},
3237            );
3238            if matches!(outcome, StepOutcome::Paused) {
3239                break;
3240            }
3241        }
3242
3243        assert_eq!(heap.allocation_token(old_id), None);
3244        assert!(heap.allocation_token(new_id).is_some());
3245        assert_eq!(heap.allgc_count(), 1);
3246
3247        heap.full_collect(&OneRoot(None));
3248        assert_eq!(heap.allocation_token(new_id), None);
3249        assert_eq!(heap.allgc_count(), 0);
3250    }
3251
3252    #[test]
3253    fn promote_and_reset_all_ages() {
3254        let heap = Heap::new();
3255        heap.unpause();
3256        let a = heap.allocate(Cell0 {
3257            next: Cell::new(None),
3258            marker_calls: Cell::new(0),
3259        });
3260        let b = heap.allocate(Cell0 {
3261            next: Cell::new(None),
3262            marker_calls: Cell::new(0),
3263        });
3264
3265        heap.promote_all_to_old();
3266        assert_eq!(a.age(), GcAge::Old);
3267        assert_eq!(b.age(), GcAge::Old);
3268        assert_eq!(a.color(), Color::Black);
3269        assert_eq!(b.color(), Color::Black);
3270
3271        heap.reset_all_ages();
3272        assert_eq!(a.age(), GcAge::New);
3273        assert_eq!(b.age(), GcAge::New);
3274        assert!(a.color().is_white());
3275        assert!(b.color().is_white());
3276    }
3277
3278    #[test]
3279    fn generational_barriers_update_ages() {
3280        let heap = Heap::new();
3281        heap.unpause();
3282        let parent = heap.allocate(Cell0 {
3283            next: Cell::new(None),
3284            marker_calls: Cell::new(0),
3285        });
3286        let child = heap.allocate(Cell0 {
3287            next: Cell::new(None),
3288            marker_calls: Cell::new(0),
3289        });
3290
3291        parent.set_age(GcAge::Old);
3292        parent.set_color(Color::Black);
3293        heap.generational_backward_barrier(parent);
3294        assert_eq!(parent.age(), GcAge::Touched1);
3295        assert_eq!(parent.color(), Color::Gray);
3296
3297        heap.generational_forward_barrier(parent, child);
3298        assert_eq!(child.age(), GcAge::Old0);
3299    }
3300
3301    #[test]
3302    fn minor_collect_frees_young_and_keeps_old() {
3303        let heap = Heap::new();
3304        heap.unpause();
3305        let old_unreachable = heap.allocate(Cell0 {
3306            next: Cell::new(None),
3307            marker_calls: Cell::new(0),
3308        });
3309        old_unreachable.set_age(GcAge::Old);
3310        old_unreachable.set_color(Color::Black);
3311        let _young_unreachable = heap.allocate(Cell0 {
3312            next: Cell::new(None),
3313            marker_calls: Cell::new(0),
3314        });
3315        let young_survivor = heap.allocate(Cell0 {
3316            next: Cell::new(None),
3317            marker_calls: Cell::new(0),
3318        });
3319
3320        heap.minor_collect_with_post_mark(&OneRoot(Some(young_survivor)), |_| {});
3321
3322        assert_eq!(heap.allgc_count(), 2);
3323        assert_eq!(old_unreachable.age(), GcAge::Old);
3324        assert_eq!(young_survivor.age(), GcAge::Survival);
3325        assert!(young_survivor.color().is_white());
3326    }
3327
3328    #[test]
3329    fn minor_collect_skips_untouched_old_root_scan_work() {
3330        let heap = Heap::new();
3331        heap.unpause();
3332        let old_root = heap.allocate(Cell0 {
3333            next: Cell::new(None),
3334            marker_calls: Cell::new(0),
3335        });
3336        old_root.set_age(GcAge::Old);
3337        old_root.set_color(Color::Black);
3338        let young_root = heap.allocate(Cell0 {
3339            next: Cell::new(None),
3340            marker_calls: Cell::new(0),
3341        });
3342
3343        heap.minor_collect_with_post_mark(
3344            &TwoRoots {
3345                first: Some(old_root),
3346                second: Some(young_root),
3347            },
3348            |_| {},
3349        );
3350
3351        let stats = heap.last_mark_stats();
3352        assert_eq!(stats.marked, 2);
3353        assert_eq!(stats.marked_old, 1);
3354        assert_eq!(stats.marked_young, 1);
3355        assert_eq!(stats.traced, 1);
3356        assert_eq!(stats.traced_old, 0);
3357        assert_eq!(stats.traced_young, 1);
3358        assert_eq!(old_root.marker_calls.get(), 0);
3359        assert_eq!(young_root.marker_calls.get(), 1);
3360        assert_eq!(old_root.age(), GcAge::Old);
3361        assert_eq!(young_root.age(), GcAge::Survival);
3362    }
3363
3364    #[test]
3365    fn minor_collect_traces_touched_old_parent() {
3366        let heap = Heap::new();
3367        heap.unpause();
3368        let old_root = heap.allocate(Cell0 {
3369            next: Cell::new(None),
3370            marker_calls: Cell::new(0),
3371        });
3372        old_root.set_age(GcAge::Old);
3373        old_root.set_color(Color::Black);
3374        let young_child = heap.allocate(Cell0 {
3375            next: Cell::new(None),
3376            marker_calls: Cell::new(0),
3377        });
3378        old_root.next.set(Some(young_child));
3379        heap.generational_backward_barrier(old_root);
3380
3381        heap.minor_collect_with_post_mark(&OneRoot(Some(old_root)), |_| {});
3382
3383        let stats = heap.last_mark_stats();
3384        assert_eq!(stats.marked, 2);
3385        assert_eq!(stats.marked_old, 1);
3386        assert_eq!(stats.marked_young, 1);
3387        assert_eq!(stats.traced, 2);
3388        assert_eq!(stats.traced_old, 1);
3389        assert_eq!(stats.traced_young, 1);
3390        assert_eq!(old_root.marker_calls.get(), 1);
3391        assert_eq!(young_child.marker_calls.get(), 1);
3392        assert_eq!(old_root.age(), GcAge::Touched2);
3393        assert_eq!(young_child.age(), GcAge::Survival);
3394    }
3395
3396    #[test]
3397    fn minor_sweep_uses_generation_cursors_to_skip_old_tail() {
3398        let heap = Heap::new();
3399        heap.unpause();
3400        let mut old_objects = Vec::new();
3401        for _ in 0..64 {
3402            old_objects.push(heap.allocate(Cell0 {
3403                next: Cell::new(None),
3404                marker_calls: Cell::new(0),
3405            }));
3406        }
3407        heap.promote_all_to_old();
3408        let young_root = heap.allocate(Cell0 {
3409            next: Cell::new(None),
3410            marker_calls: Cell::new(0),
3411        });
3412
3413        heap.minor_collect_with_post_mark(&OneRoot(Some(young_root)), |_| {});
3414
3415        let stats = heap.last_sweep_stats();
3416        assert_eq!(stats.visited, 1);
3417        assert_eq!(stats.visited_young, 1);
3418        assert_eq!(stats.visited_old, 0);
3419        assert_eq!(heap.allgc_count(), old_objects.len() + 1);
3420        assert_eq!(young_root.age(), GcAge::Survival);
3421    }
3422
3423    #[test]
3424    fn full_sweep_corrects_generation_cursors_when_cursor_object_is_freed() {
3425        let heap = Heap::new();
3426        heap.unpause();
3427        let _old = heap.allocate(Cell0 {
3428            next: Cell::new(None),
3429            marker_calls: Cell::new(0),
3430        });
3431        heap.promote_all_to_old();
3432        assert!(heap.survival.get().is_some());
3433        assert!(heap.old1.get().is_some());
3434        assert!(heap.reallyold.get().is_some());
3435
3436        heap.full_collect(&OneRoot(None));
3437
3438        assert_eq!(heap.allgc_count(), 0);
3439        assert_eq!(heap.survival.get(), None);
3440        assert_eq!(heap.old1.get(), None);
3441        assert_eq!(heap.reallyold.get(), None);
3442        assert_eq!(heap.firstold1.get(), None);
3443        assert_eq!(heap.last_sweep_stats().freed, 1);
3444    }
3445
3446    #[test]
3447    fn full_sweep_unlinks_freed_grayagain_entries() {
3448        let heap = Heap::new();
3449        heap.unpause();
3450        let parent = heap.allocate(Cell0 {
3451            next: Cell::new(None),
3452            marker_calls: Cell::new(0),
3453        });
3454        heap.promote_all_to_old();
3455        heap.generational_backward_barrier(parent);
3456        assert_eq!(heap.grayagain_count(), 1);
3457
3458        heap.full_collect(&OneRoot(None));
3459
3460        assert_eq!(heap.allgc_count(), 0);
3461        assert_eq!(heap.grayagain_count(), 0);
3462
3463        let young = heap.allocate(Cell0 {
3464            next: Cell::new(None),
3465            marker_calls: Cell::new(0),
3466        });
3467        heap.minor_collect_with_post_mark(&OneRoot(Some(young)), |_| {});
3468        assert_eq!(young.age(), GcAge::Survival);
3469    }
3470
3471    #[test]
3472    fn grayagain_links_object_once() {
3473        let heap = Heap::new();
3474        heap.unpause();
3475        let parent = heap.allocate(Cell0 {
3476            next: Cell::new(None),
3477            marker_calls: Cell::new(0),
3478        });
3479        parent.set_age(GcAge::Old);
3480        parent.set_color(Color::Black);
3481
3482        heap.generational_backward_barrier(parent);
3483        heap.generational_backward_barrier(parent);
3484
3485        assert_eq!(heap.grayagain_count(), 1);
3486    }
3487
3488    #[test]
3489    fn grayagain_list_carries_old1_until_old() {
3490        let heap = Heap::new();
3491        heap.unpause();
3492        let survivor = heap.allocate(Cell0 {
3493            next: Cell::new(None),
3494            marker_calls: Cell::new(0),
3495        });
3496
3497        heap.minor_collect_with_post_mark(&OneRoot(Some(survivor)), |_| {});
3498        assert_eq!(survivor.age(), GcAge::Survival);
3499
3500        heap.minor_collect_with_post_mark(&OneRoot(Some(survivor)), |_| {});
3501        assert_eq!(survivor.age(), GcAge::Old1);
3502
3503        heap.minor_collect_with_post_mark(&OneRoot(None), |_| {});
3504        assert_eq!(survivor.age(), GcAge::Old);
3505        assert_eq!(heap.allgc_count(), 1);
3506    }
3507
3508    #[test]
3509    fn grayagain_list_carries_touched2_until_old() {
3510        let heap = Heap::new();
3511        heap.unpause();
3512        let parent = heap.allocate(Cell0 {
3513            next: Cell::new(None),
3514            marker_calls: Cell::new(0),
3515        });
3516        parent.set_age(GcAge::Old);
3517        parent.set_color(Color::Black);
3518        heap.generational_backward_barrier(parent);
3519
3520        heap.minor_collect_with_post_mark(&OneRoot(None), |_| {});
3521        assert_eq!(parent.age(), GcAge::Touched2);
3522
3523        heap.minor_collect_with_post_mark(&OneRoot(None), |_| {});
3524        assert_eq!(parent.age(), GcAge::Old);
3525        assert_eq!(parent.marker_calls.get(), 2);
3526    }
3527
3528    #[test]
3529    fn collect_traverses_cycles() {
3530        let heap = Heap::new();
3531        heap.unpause();
3532        let a = heap.allocate(Cell0 {
3533            next: Cell::new(None),
3534            marker_calls: Cell::new(0),
3535        });
3536        let b = heap.allocate(Cell0 {
3537            next: Cell::new(Some(a)),
3538            marker_calls: Cell::new(0),
3539        });
3540        a.next.set(Some(b)); // cycle
3541                             // With a as root, both should survive.
3542        heap.full_collect(&OneRoot(Some(a)));
3543        assert_eq!(a.marker_calls.get(), 1);
3544        assert_eq!(b.marker_calls.get(), 1);
3545        // Drop the only root path; cycle should now be collected.
3546        // (Note: `a` and `b` are still on the stack as Gc<Cell0> handles, but
3547        // they're not in a Trace-visible position.)
3548        heap.full_collect(&OneRoot(None));
3549        assert_eq!(heap.bytes_used(), 0);
3550    }
3551
3552    #[test]
3553    fn heap_guard_stacks() {
3554        assert!(
3555            with_current_heap(|heap| heap.is_none()),
3556            "no guard initially"
3557        );
3558        let h1 = Heap::new();
3559        h1.unpause();
3560        {
3561            let _g1 = HeapGuard::push(&h1);
3562            assert!(with_current_heap(|heap| heap.is_some()));
3563            let h2 = Heap::new();
3564            h2.unpause();
3565            {
3566                let _g2 = HeapGuard::push(&h2);
3567                // top of stack is h2
3568                with_current_heap(|heap| {
3569                    assert!(std::ptr::addr_eq(
3570                        heap.unwrap() as *const _,
3571                        &h2 as *const _,
3572                    ));
3573                });
3574            }
3575            // _g2 dropped — top is back to h1
3576            with_current_heap(|heap| {
3577                assert!(std::ptr::addr_eq(
3578                    heap.unwrap() as *const _,
3579                    &h1 as *const _,
3580                ));
3581            });
3582        }
3583        assert!(
3584            with_current_heap(|heap| heap.is_none()),
3585            "stack empty after all guards drop"
3586        );
3587    }
3588
3589    #[test]
3590    fn step_threshold_triggers_collect() {
3591        let heap = Heap::new();
3592        heap.unpause();
3593        // Allocate enough boxes to exceed the default 64KB threshold.
3594        let mut keeps = Vec::new();
3595        for _ in 0..64 {
3596            // ~1KB per box (Cell0 is small, but allocating many headers
3597            // accumulates). For the threshold test we'd typically allocate
3598            // larger objects; this is a smoke test.
3599            keeps.push(heap.allocate(Cell0 {
3600                next: Cell::new(None),
3601                marker_calls: Cell::new(0),
3602            }));
3603        }
3604        // Build roots that retain all of keeps via individual marks.
3605        struct ManyRoots<'a>(&'a [Gc<Cell0>]);
3606        impl<'a> Trace for ManyRoots<'a> {
3607            fn trace(&self, m: &mut Marker) {
3608                for g in self.0.iter() {
3609                    m.mark(*g);
3610                }
3611            }
3612        }
3613        heap.step(&ManyRoots(&keeps));
3614        // step is a no-op below threshold; all roots retained.
3615        assert!(heap.bytes_used() > 0);
3616    }
3617
3618    #[test]
3619    fn threshold_floored_after_collecting_tiny_heap() {
3620        let heap = Heap::new();
3621        heap.unpause();
3622        struct NoRoots;
3623        impl Trace for NoRoots {
3624            fn trace(&self, _m: &mut Marker) {}
3625        }
3626        for _ in 0..200 {
3627            heap.allocate(Cell0 {
3628                next: Cell::new(None),
3629                marker_calls: Cell::new(0),
3630            });
3631        }
3632        heap.full_collect(&NoRoots);
3633        assert!(
3634            heap.threshold_bytes() >= GC_MIN_THRESHOLD,
3635            "threshold {} collapsed below floor {}; a churning program would full-collect per allocation",
3636            heap.threshold_bytes(),
3637            GC_MIN_THRESHOLD
3638        );
3639    }
3640
3641    fn build_chain(heap: &Heap, len: usize) -> Gc<Cell0> {
3642        let head = heap.allocate(Cell0 {
3643            next: Cell::new(None),
3644            marker_calls: Cell::new(0),
3645        });
3646        let mut cur = head;
3647        for _ in 1..len {
3648            let n = heap.allocate(Cell0 {
3649                next: Cell::new(None),
3650                marker_calls: Cell::new(0),
3651            });
3652            cur.next.set(Some(n));
3653            cur = n;
3654        }
3655        head
3656    }
3657
3658    #[test]
3659    fn budget_zero_does_some_work() {
3660        let heap = Heap::new();
3661        heap.unpause();
3662        let head = build_chain(&heap, 8);
3663        let roots = OneRoot(Some(head));
3664        let budget = StepBudget::from_work(0);
3665        let outcome = heap.incremental_step_with_post_mark(&roots, budget, |_| {});
3666        assert_ne!(outcome, StepOutcome::SkippedStopped);
3667        assert_ne!(heap.gc_state(), GcState::Pause);
3668    }
3669
3670    #[test]
3671    fn run_until_state_stops_before_next_phase_work() {
3672        let heap = Heap::new();
3673        heap.unpause();
3674        let head = build_chain(&heap, 8);
3675        let roots = OneRoot(Some(head));
3676        let atomic_calls = Cell::new(0);
3677
3678        let outcome =
3679            heap.incremental_run_until_state_with_post_mark(&roots, GcState::Atomic, 1024, |_| {
3680                atomic_calls.set(atomic_calls.get() + 1)
3681            });
3682        assert_eq!(outcome, StepOutcome::InProgress);
3683        assert_eq!(heap.gc_state(), GcState::Atomic);
3684        assert_eq!(
3685            atomic_calls.get(),
3686            0,
3687            "atomic hook must not run before inspection"
3688        );
3689
3690        let outcome = heap.incremental_run_until_state_with_post_mark(
3691            &roots,
3692            GcState::SweepAllGc,
3693            1024,
3694            |_| atomic_calls.set(atomic_calls.get() + 1),
3695        );
3696        assert_eq!(outcome, StepOutcome::InProgress);
3697        assert_eq!(heap.gc_state(), GcState::SweepAllGc);
3698        assert_eq!(
3699            atomic_calls.get(),
3700            1,
3701            "entering sweep must run the atomic hook once"
3702        );
3703    }
3704
3705    #[test]
3706    fn larger_budget_drains_more_gray_than_smaller() {
3707        let small_heap = Heap::new();
3708        small_heap.unpause();
3709        let h1 = build_chain(&small_heap, 64);
3710        let r1 = OneRoot(Some(h1));
3711        let mut small_calls = 0;
3712        loop {
3713            small_calls += 1;
3714            let outcome =
3715                small_heap.incremental_step_with_post_mark(&r1, StepBudget::from_work(2), |_| {});
3716            if outcome == StepOutcome::Paused {
3717                break;
3718            }
3719            assert!(small_calls < 10_000, "did not converge");
3720        }
3721
3722        let big_heap = Heap::new();
3723        big_heap.unpause();
3724        let h2 = build_chain(&big_heap, 64);
3725        let r2 = OneRoot(Some(h2));
3726        let mut big_calls = 0;
3727        loop {
3728            big_calls += 1;
3729            let outcome =
3730                big_heap.incremental_step_with_post_mark(&r2, StepBudget::from_work(64), |_| {});
3731            if outcome == StepOutcome::Paused {
3732                break;
3733            }
3734            assert!(big_calls < 10_000, "did not converge");
3735        }
3736
3737        assert!(
3738            big_calls < small_calls,
3739            "expected big_calls={} < small_calls={}",
3740            big_calls,
3741            small_calls
3742        );
3743    }
3744
3745    #[test]
3746    fn sweep_can_pause_and_resume() {
3747        let heap = Heap::new();
3748        heap.unpause();
3749        for _ in 0..16 {
3750            let _ = heap.allocate(Cell0 {
3751                next: Cell::new(None),
3752                marker_calls: Cell::new(0),
3753            });
3754        }
3755        let roots = OneRoot(None);
3756        let bytes_before = heap.bytes_used();
3757        assert!(bytes_before > 0);
3758        let mut step_count = 0;
3759        let mut saw_in_progress_during_sweep = false;
3760        loop {
3761            step_count += 1;
3762            let outcome =
3763                heap.incremental_step_with_post_mark(&roots, StepBudget::from_work(2), |_| {});
3764            if heap.gc_state().is_sweep() && outcome == StepOutcome::InProgress {
3765                saw_in_progress_during_sweep = true;
3766            }
3767            if outcome == StepOutcome::Paused {
3768                break;
3769            }
3770            assert!(step_count < 10_000, "did not converge");
3771        }
3772        assert!(saw_in_progress_during_sweep, "sweep never paused mid-list");
3773        assert_eq!(heap.bytes_used(), 0);
3774    }
3775
3776    #[test]
3777    fn post_mark_runs_once_per_atomic() {
3778        let heap = Heap::new();
3779        heap.unpause();
3780        for _ in 0..32 {
3781            let _ = heap.allocate(Cell0 {
3782                next: Cell::new(None),
3783                marker_calls: Cell::new(0),
3784            });
3785        }
3786        let roots = OneRoot(None);
3787        let call_count = std::cell::Cell::new(0);
3788        let mut step_count = 0;
3789        loop {
3790            step_count += 1;
3791            let outcome =
3792                heap.incremental_step_with_post_mark(&roots, StepBudget::from_work(2), |_| {
3793                    call_count.set(call_count.get() + 1);
3794                });
3795            if outcome == StepOutcome::Paused {
3796                break;
3797            }
3798            assert!(step_count < 10_000, "did not converge");
3799        }
3800        assert_eq!(
3801            call_count.get(),
3802            1,
3803            "post_mark must run exactly once per cycle"
3804        );
3805    }
3806
3807    #[test]
3808    fn full_collect_equivalent_to_incremental_to_pause() {
3809        let h1 = Heap::new();
3810        h1.unpause();
3811        let head1 = h1.allocate(Cell0 {
3812            next: Cell::new(None),
3813            marker_calls: Cell::new(0),
3814        });
3815        let _orphan1 = h1.allocate(Cell0 {
3816            next: Cell::new(None),
3817            marker_calls: Cell::new(0),
3818        });
3819        let _orphan2 = h1.allocate(Cell0 {
3820            next: Cell::new(None),
3821            marker_calls: Cell::new(0),
3822        });
3823        let roots1 = OneRoot(Some(head1));
3824        h1.full_collect(&roots1);
3825        let bytes_full = h1.bytes_used();
3826
3827        let h2 = Heap::new();
3828        h2.unpause();
3829        let head2 = h2.allocate(Cell0 {
3830            next: Cell::new(None),
3831            marker_calls: Cell::new(0),
3832        });
3833        let _orphan3 = h2.allocate(Cell0 {
3834            next: Cell::new(None),
3835            marker_calls: Cell::new(0),
3836        });
3837        let _orphan4 = h2.allocate(Cell0 {
3838            next: Cell::new(None),
3839            marker_calls: Cell::new(0),
3840        });
3841        let roots2 = OneRoot(Some(head2));
3842        loop {
3843            let outcome =
3844                h2.incremental_step_with_post_mark(&roots2, StepBudget::from_work(1), |_| {});
3845            if outcome == StepOutcome::Paused {
3846                break;
3847            }
3848        }
3849        assert_eq!(h2.bytes_used(), bytes_full);
3850    }
3851}
3852
3853// ──────────────────────────────────────────────────────────────────────────────
3854// PORT STATUS
3855//   source:        production Rust heap/collector substrate
3856//   target_crate:  lua-gc
3857//   confidence:    medium
3858//   todos:         0
3859//   port_notes:    0
3860//   unsafe_blocks: 13
3861//   notes:         Mark-and-sweep collector heap + the Gc<T> raw-pointer substrate. Uses
3862//                  NonNull<GcBox<T>> with linked-list allgc walking; unsafe is
3863//                  required for raw pointer ops and Box::into_raw/from_raw. See
3864//                  unsafe-budgets.toml for the per-crate ceiling.
3865// ──────────────────────────────────────────────────────────────────────────────