Skip to main content

lua_gc/
heap.rs

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