Skip to main content

lua_gc/
heap.rs

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