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