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