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