Skip to main content

lua_gc/
heap.rs

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