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