Skip to main content

lua_gc/
heap.rs

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