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, stop-the-world, precise tracing collector with a
6//! forward write barrier. Incremental marking is a future enhancement;
7//! the current implementation does a full collect each time `step` decides
8//! it's time.
9//!
10//! # Vocabulary
11//!
12//! - **Gc<T>**: a pointer-sized handle. `Copy + Clone`. Replaces `GcRef<T>`.
13//! - **GcBox<T>**: the heap allocation; contains a header and the value.
14//! - **GcHeader**: per-object metadata (color, finalized flag, intrusive
15//!   `next` pointer for the allgc list).
16//! - **Trace**: trait every GC-rooted type implements. The `trace` method
17//!   walks all `Gc<_>` fields and calls `Marker::mark` on each.
18//! - **Marker**: passed to `trace`; carries the gray queue.
19//! - **Heap**: owns the allgc list head, byte counters, GC state machine.
20//!
21//! # Safety model
22//!
23//! All `unsafe` is confined to this crate (per `harness/unsafe-budgets.toml`).
24//! The invariants are:
25//!
26//! 1. Every `Gc<T>` points to a valid, allocated, not-yet-swept `GcBox<T>`.
27//! 2. The allgc intrusive list is consistent: traversing `header.next` from
28//!    `Heap.head` reaches every live `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 allgc chain.
39
40use std::cell::{Cell, RefCell};
41use std::marker::PhantomData;
42use std::ptr::NonNull;
43
44// ──────────────────────────────────────────────────────────────────────────
45// Phase D-1c — scoped thread-local HeapGuard
46//
47// Lua's C API supports multiple `lua_State`s on one OS thread (sandbox-per-
48// state is a real embedding pattern). We honor that by stacking the
49// currently-active heap rather than holding a single slot. `HeapGuard::push`
50// activates a heap; drop pops it.
51//
52// `with_current_heap(...)` exposes the top of the stack only for the dynamic
53// extent of a closure — used by `GcRef::new` call sites that don't have
54// `&mut LuaState` in scope.
55// ──────────────────────────────────────────────────────────────────────────
56
57thread_local! {
58    static CURRENT_HEAP_STACK: RefCell<Vec<NonNull<Heap>>> = const { RefCell::new(Vec::new()) };
59}
60
61/// A scoped guard for the currently-active heap. Pushed at entry to
62/// `state.run()` / `state.protected_call()` / `state.load()`; popped on
63/// drop. Supports nesting (multiple LuaStates on one thread).
64pub struct HeapGuard {
65    // Anchor a NonNull so the user can't accidentally drop the guard while
66    // an inner Lua state is still active. We rely on RAII.
67    _private: (),
68}
69
70impl HeapGuard {
71    /// Push `heap` onto the active stack. Returns a guard; dropping it pops.
72    ///
73    /// # Safety
74    ///
75    /// The pointer must remain valid for the lifetime of the guard. Callers
76    /// typically pass `&state.global.heap`, which lives as long as the
77    /// `GlobalState` (an `Rc<RefCell<_>>`); the guard must drop before the
78    /// state is dropped.
79    pub fn push(heap: &Heap) -> Self {
80        let ptr = NonNull::from(heap);
81        CURRENT_HEAP_STACK.with(|stack| stack.borrow_mut().push(ptr));
82        HeapGuard { _private: () }
83    }
84}
85
86impl Drop for HeapGuard {
87    fn drop(&mut self) {
88        CURRENT_HEAP_STACK.with(|stack| {
89            let popped = stack.borrow_mut().pop();
90            debug_assert!(popped.is_some(), "HeapGuard::drop with empty stack");
91        });
92    }
93}
94
95/// Runs `f` with a reference to the currently-active heap, or `None` if no
96/// `HeapGuard` is in scope.
97///
98/// The heap reference is deliberately scoped to the closure. This avoids the
99/// previous `current_heap() -> Option<&'static Heap>` lifetime lie while still
100/// supporting legacy `GcRef::new` call sites that do not receive `&mut LuaState`.
101pub fn with_current_heap<R>(f: impl for<'a> FnOnce(Option<&'a Heap>) -> R) -> R {
102    CURRENT_HEAP_STACK.with(|stack| {
103        let ptr = stack.borrow().last().copied();
104        // SAFETY: the top NonNull was produced from a live `&Heap` whose
105        // lifetime is bounded by the corresponding `HeapGuard`. The reference
106        // is only handed to `f`, and cannot escape through the return type.
107        let heap = ptr.map(|ptr| unsafe { &*ptr.as_ptr() });
108        f(heap)
109    })
110}
111
112/// A traced color in the tri-color invariant.
113#[derive(Copy, Clone, PartialEq, Eq, Debug)]
114pub enum Color {
115    /// Not yet visited this cycle. Candidate for sweep.
116    White,
117    /// Visited; outgoing references not yet traced.
118    Gray,
119    /// Fully traced; no outgoing pointers to white objects.
120    Black,
121}
122
123/// Per-object GC metadata. Lives at the start of every `GcBox`.
124#[repr(C)]
125pub struct GcHeader {
126    color: Cell<Color>,
127    /// Set true after this object's finalizer (`__gc` metamethod) has run.
128    /// Phase D defers finalizers; this is reserved.
129    finalized: Cell<bool>,
130    /// Intrusive link into the heap's allgc chain.
131    next: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
132    /// Rough byte size of the contained value; used for memory accounting.
133    size: usize,
134}
135
136impl GcHeader {
137    fn new_white(size: usize) -> Self {
138        Self {
139            color: Cell::new(Color::White),
140            finalized: Cell::new(false),
141            next: Cell::new(None),
142            size,
143        }
144    }
145}
146
147/// A heap-allocated, GC-tracked value plus its header.
148#[repr(C)]
149pub struct GcBox<T: ?Sized> {
150    header: GcHeader,
151    value: T,
152}
153
154impl<T: ?Sized> GcBox<T> {
155    pub fn header(&self) -> &GcHeader {
156        &self.header
157    }
158    pub fn value(&self) -> &T {
159        &self.value
160    }
161}
162
163/// A GC-managed pointer. Copy + Clone (one-machine-word). Replaces `GcRef<T>`.
164pub struct Gc<T: ?Sized> {
165    ptr: NonNull<GcBox<T>>,
166    /// Marker so `Gc<T>` is treated as if it owns `T` for variance.
167    _marker: PhantomData<T>,
168}
169
170// SAFETY: Gc is just a pointer. The Cell-based interior mutability of the
171// header is single-threaded (the entire Lua runtime is single-threaded), so
172// no Send/Sync impls are needed and we don't provide them.
173impl<T: ?Sized> Copy for Gc<T> {}
174impl<T: ?Sized> Clone for Gc<T> {
175    fn clone(&self) -> Self {
176        *self
177    }
178}
179
180impl<T: ?Sized> PartialEq for Gc<T> {
181    fn eq(&self, other: &Self) -> bool {
182        std::ptr::addr_eq(self.ptr.as_ptr(), other.ptr.as_ptr())
183    }
184}
185impl<T: ?Sized> Eq for Gc<T> {}
186
187impl<T: ?Sized> std::hash::Hash for Gc<T> {
188    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
189        self.ptr.as_ptr().hash(state)
190    }
191}
192
193impl<T: ?Sized> std::fmt::Debug for Gc<T> {
194    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195        write!(f, "Gc({:p})", self.ptr.as_ptr())
196    }
197}
198
199impl<T: Trace + 'static> Gc<T> {
200    /// Allocate a `GcBox<T>` outside any heap registry. Used by legacy
201    /// `GcRef::new` call sites until Phase D-1b migrates them. The returned
202    /// `Gc<T>` is reachable only through the caller's own retention path;
203    /// without joining a heap's allgc chain, it will never be swept (so
204    /// effectively leaks until process exit — same as Rc behavior).
205    pub fn new_uncollected(value: T) -> Self {
206        let size = std::mem::size_of::<T>();
207        let boxed = Box::new(GcBox {
208            header: GcHeader::new_white(size),
209            value,
210        });
211        Gc {
212            ptr: NonNull::new(Box::into_raw(boxed)).expect("Box::into_raw is non-null"),
213            _marker: PhantomData,
214        }
215    }
216}
217
218impl<T: ?Sized> Gc<T> {
219    /// Two `Gc<T>`s are identity-equal iff they point at the same box.
220    pub fn ptr_eq(a: Self, b: Self) -> bool {
221        std::ptr::addr_eq(a.ptr.as_ptr(), b.ptr.as_ptr())
222    }
223
224    /// Identity as a usize — usable as a hash table key for "is the *same
225    /// object*" lookups.
226    pub fn identity(self) -> usize {
227        self.ptr.as_ptr() as *const () as usize
228    }
229
230    /// Access the underlying value. Returns `&T` so callers can read fields
231    /// without taking the `Gc` apart. Interior mutability lives inside T's
232    /// own fields (Cell, RefCell, etc.).
233    fn as_box(&self) -> &GcBox<T> {
234        // SAFETY: A Gc<T> is constructed only by allocate() or
235        // new_uncollected(), both of which produce a valid GcBox. The box
236        // outlives the Gc until sweep, which only frees boxes not reachable
237        // from any root — so as long as this Gc is on the stack or in a
238        // traced field, the box is live.
239        unsafe { self.ptr.as_ref() }
240    }
241
242    fn header(&self) -> &GcHeader {
243        &self.as_box().header
244    }
245}
246
247impl<T: ?Sized> std::ops::Deref for Gc<T> {
248    type Target = T;
249    fn deref(&self) -> &T {
250        &self.as_box().value
251    }
252}
253
254impl<T: ?Sized> AsRef<T> for Gc<T> {
255    fn as_ref(&self) -> &T {
256        &self.as_box().value
257    }
258}
259
260/// Every GC-rooted type implements `Trace` to expose its `Gc<_>` fields.
261///
262/// The `trace` method visits every reachable `Gc<_>` and calls
263/// `Marker::mark` on it. For container fields (`Vec<LuaValue>`, etc.) call
264/// `field.trace(m)` to delegate.
265///
266/// # Mechanical pattern
267///
268/// ```ignore
269/// impl Trace for LuaTable {
270///     fn trace(&self, m: &mut Marker) {
271///         for v in self.array.iter() { v.trace(m); }
272///         if let Some(mt) = self.metatable { m.mark(mt); }
273///     }
274/// }
275/// ```
276pub trait Trace {
277    fn trace(&self, m: &mut Marker);
278}
279
280// Common blanket impls so most container types Just Work.
281impl<T: Trace> Trace for Vec<T> {
282    fn trace(&self, m: &mut Marker) {
283        for item in self.iter() {
284            item.trace(m);
285        }
286    }
287}
288
289impl<T: Trace> Trace for Option<T> {
290    fn trace(&self, m: &mut Marker) {
291        if let Some(v) = self {
292            v.trace(m);
293        }
294    }
295}
296
297impl<T: Trace + ?Sized> Trace for Box<T> {
298    fn trace(&self, m: &mut Marker) {
299        (**self).trace(m);
300    }
301}
302
303impl<T: Trace + ?Sized> Trace for std::rc::Rc<T> {
304    fn trace(&self, m: &mut Marker) {
305        (**self).trace(m);
306    }
307}
308
309impl<T: Trace> Trace for RefCell<T> {
310    fn trace(&self, m: &mut Marker) {
311        self.borrow().trace(m);
312    }
313}
314
315/// `Gc<T>` is itself traceable: marking it forwards to the contained `T`.
316impl<T: Trace + 'static> Trace for Gc<T> {
317    fn trace(&self, m: &mut Marker) {
318        m.mark(*self);
319    }
320}
321
322// Trivially-traceable primitive types: visiting does nothing.
323macro_rules! trace_noop {
324    ($($t:ty),*) => {
325        $(impl Trace for $t {
326            fn trace(&self, _m: &mut Marker) {}
327        })*
328    };
329}
330trace_noop!(
331    bool, u8, u16, u32, u64, u128, usize,
332    i8, i16, i32, i64, i128, isize,
333    f32, f64, char, String, str
334);
335
336impl<T> Trace for std::marker::PhantomData<T> {
337    fn trace(&self, _m: &mut Marker) {}
338}
339
340/// Holds the gray queue during a mark phase. Passed to `Trace::trace`.
341pub struct Marker {
342    gray_queue: Vec<NonNull<GcBox<dyn Trace>>>,
343    visited: std::collections::HashSet<usize>,
344}
345
346impl Marker {
347    fn new() -> Self {
348        Self {
349            gray_queue: Vec::with_capacity(256),
350            visited: std::collections::HashSet::new(),
351        }
352    }
353
354    /// Mark a `Gc<T>` as gray (reachable, but its outgoing edges not yet
355    /// traced). Called by `Trace::trace` implementations.
356    ///
357    /// Per-cycle dedup uses `visited` (a HashSet of box identities) rather
358    /// than the color flag. Color-based dedup would silently skip
359    /// `new_uncollected` boxes left Black by the previous cycle — those
360    /// allocations are NOT on the heap's allgc chain, so the start-of-mark
361    /// "reset all allgc to White" loop does not reach them, and a Black
362    /// uncollected box would be skipped without re-tracing its children
363    /// (causing reachable allgc descendants to be swept). The visited set
364    /// is rebuilt every `full_collect` (Marker::new), so this dedup is
365    /// always per-cycle.
366    pub fn mark<T: Trace + 'static>(&mut self, gc: Gc<T>) {
367        let id = gc.identity();
368        if self.visited.insert(id) {
369            let header = gc.header();
370            header.color.set(Color::Gray);
371            let ptr: NonNull<GcBox<dyn Trace>> = gc.ptr;
372            self.gray_queue.push(ptr);
373        }
374    }
375
376    /// Record that an Rc-backed object (`GcRef<T>` in Phase A-D-0) has been
377    /// visited and return whether this is the first visit. Used by recursive
378    /// `Trace` impls to break cycles while the real `Gc<T>` gray-queue path is
379    /// not yet wired (e.g. `_G._G == _G` would otherwise infinitely recurse).
380    pub fn try_visit(&mut self, addr: usize) -> bool {
381        self.visited.insert(addr)
382    }
383
384    /// True iff `id` was reached during the mark phase. Used by the
385    /// post-mark hook (`Heap::full_collect_with_post_mark`) to decide whether
386    /// a weak-table entry's target is still strongly reachable. Read-only —
387    /// callers cannot add entries.
388    pub fn is_visited(&self, id: usize) -> bool {
389        self.visited.contains(&id)
390    }
391
392    /// Number of objects marked so far. Used by the post-mark hook's
393    /// ephemeron-convergence fixed-point loop to detect when an iteration
394    /// added no new reachable objects and the loop can terminate.
395    pub fn visited_count(&self) -> usize {
396        self.visited.len()
397    }
398
399    /// Drain the gray queue, transitively marking children. Each gray box
400    /// becomes black; its `Trace::trace` is called so the children it points
401    /// at get pushed onto the queue. Repeats until the queue is empty.
402    ///
403    /// Exposed for the post-mark hook so it can run ephemeron convergence:
404    /// after marking new values via [`Marker::mark`] (or `value.trace(self)`),
405    /// the hook calls `drain_gray_queue` to propagate the new reachability,
406    /// then re-checks for fixed-point.
407    pub fn drain_gray_queue(&mut self) {
408        while let Some(gray_ptr) = self.gray_queue.pop() {
409            unsafe {
410                let bx = gray_ptr.as_ref();
411                bx.header.color.set(Color::Black);
412                bx.value.trace(self);
413            }
414        }
415    }
416}
417
418/// Phases of the incremental collection cycle.
419///
420/// The state machine matches a simplified subset of C-Lua's `lgc.c` FSM and
421/// is driven by [`Heap::incremental_step_with_post_mark`].
422///
423/// Transitions:
424/// - `Pause` → `Propagate` (on first step: reset colors, trace roots).
425/// - `Propagate` → `Atomic` (when the gray queue empties).
426/// - `Atomic` → `Sweep` (post-mark hook has run; sweep cursor is initialized).
427/// - `Sweep` → `Finalize` (sweep cursor reached the end of allgc).
428/// - `Finalize` → `Pause` (any pending finalize work has been drained).
429///
430/// `Collecting` is kept as a compatibility alias for the old API (used by
431/// `barrier`) — it means "anything but Pause."
432#[derive(Copy, Clone, PartialEq, Eq, Debug)]
433pub enum GcState {
434    Pause,
435    Propagate,
436    Atomic,
437    Sweep,
438    Finalize,
439}
440
441impl GcState {
442    pub fn is_pause(self) -> bool {
443        matches!(self, GcState::Pause)
444    }
445    pub fn is_propagate(self) -> bool {
446        matches!(self, GcState::Propagate)
447    }
448    pub fn is_sweep(self) -> bool {
449        matches!(self, GcState::Sweep)
450    }
451}
452
453/// Outcome of one call to [`Heap::incremental_step_with_post_mark`].
454#[derive(Copy, Clone, PartialEq, Eq, Debug)]
455pub enum StepOutcome {
456    /// The step finished a cycle. The heap is back at `GcState::Pause`.
457    Paused,
458    /// The step performed work but the cycle is not finished. Caller may
459    /// step again.
460    InProgress,
461    /// The heap is paused (via [`Heap::pause`]) or the caller asked for zero
462    /// budget while no cycle was in progress and no work was needed.
463    SkippedStopped,
464}
465
466/// Work budget for one incremental step.
467///
468/// `remaining_work` counts down by one for each unit of work performed (one
469/// gray object traced, one swept node visited, one finalizer dispatched).
470/// `max_credit` caps how negative `remaining_work` may be allowed to go — a
471/// step that overshoots its budget rolls the overrun into the caller's debt
472/// rather than letting unbounded work happen in one call.
473#[derive(Copy, Clone, Debug)]
474pub struct StepBudget {
475    pub remaining_work: isize,
476    pub max_credit: isize,
477}
478
479impl StepBudget {
480    /// Build a budget from a number of allowed work units.
481    pub fn from_work(work: isize) -> Self {
482        Self { remaining_work: work.max(1), max_credit: work.max(1) }
483    }
484}
485
486/// The heap. One per `GlobalState`. Owns the intrusive allgc list of every
487/// allocated `GcBox`, tracks total bytes, and runs collections.
488/// Floor for the post-collection threshold. Without it, a tight
489/// allocation loop drives the live set near zero, so `bytes * pause/100`
490/// collapses toward zero and a full stop-the-world collection fires every
491/// few allocations, re-tracing all roots each time (issue #38, GC path).
492/// The floor bounds the wasted work: the collector waits until at least
493/// this many bytes of garbage accumulate before collecting a small heap.
494const GC_MIN_THRESHOLD: usize = 256 * 1024;
495
496pub struct Heap {
497    /// Head of the singly-linked allgc list (every live `GcBox`).
498    head: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
499    /// Total bytes allocated (sum of header sizes; rough).
500    bytes: Cell<usize>,
501    /// Threshold above which `step` triggers a collection.
502    threshold: Cell<usize>,
503    /// Multiplier on bytes_used to set next threshold after collection.
504    pause_multiplier: Cell<usize>,
505    /// State machine for the incremental collector.
506    state: Cell<GcState>,
507    /// If true, `step` and `barrier` are no-ops (for bootstrap before the
508    /// world is consistent).
509    paused: Cell<bool>,
510    /// Counter of full collections performed (for diagnostics).
511    collections: Cell<usize>,
512    /// In-progress marker state for incremental cycles. `Some` between
513    /// `Propagate` start and `Sweep` start; `None` otherwise.
514    marker: RefCell<Option<Marker>>,
515    /// Sweep cursor. Points at the `Cell` whose `Option<NonNull>` is the
516    /// "current" link being inspected during the sweep phase. Encoded as a
517    /// raw pointer because the cell lives inside a `GcHeader` (Cell, not Cell<Cell>).
518    /// `None` means: sweep starts from `self.head`.
519    sweep_prev_next: Cell<Option<NonNull<Cell<Option<NonNull<GcBox<dyn Trace>>>>>>>,
520}
521
522impl Default for Heap {
523    fn default() -> Self {
524        Self::new()
525    }
526}
527
528impl Heap {
529    pub fn new() -> Self {
530        Self {
531            head: Cell::new(None),
532            bytes: Cell::new(0),
533            threshold: Cell::new(64 * 1024), // initial threshold: 64 KB
534            pause_multiplier: Cell::new(200), // 200% = collect when bytes 2x threshold
535            state: Cell::new(GcState::Pause),
536            paused: Cell::new(true), // start paused; caller enables when world is consistent
537            collections: Cell::new(0),
538            marker: RefCell::new(None),
539            sweep_prev_next: Cell::new(None),
540        }
541    }
542
543    /// Enable collection. Until this is called, `step` is a no-op (so the
544    /// runtime can bootstrap without prematurely freeing objects).
545    pub fn unpause(&self) {
546        self.paused.set(false);
547    }
548
549    pub fn is_paused(&self) -> bool {
550        self.paused.get()
551    }
552
553    /// Allocate a new `GcBox<T>` and prepend it to the allgc chain.
554    pub fn allocate<T: Trace + 'static>(&self, value: T) -> Gc<T> {
555        let size = std::mem::size_of::<GcBox<T>>();
556        let boxed = Box::new(GcBox {
557            header: GcHeader::new_white(size),
558            value,
559        });
560        boxed.header.next.set(self.head.get());
561        let raw: *mut GcBox<T> = Box::into_raw(boxed);
562        let ptr: NonNull<GcBox<T>> =
563            NonNull::new(raw).expect("Box::into_raw is non-null");
564        let dyn_ptr: NonNull<GcBox<dyn Trace>> = ptr;
565        self.head.set(Some(dyn_ptr));
566        self.bytes.set(self.bytes.get() + size);
567        Gc {
568            ptr,
569            _marker: PhantomData,
570        }
571    }
572
573    /// Bytes currently retained by GC-tracked objects (rough estimate).
574    pub fn bytes_used(&self) -> usize {
575        self.bytes.get()
576    }
577
578    /// Current collection threshold in bytes. When `bytes_used() >= threshold_bytes()`,
579    /// the next `step()` will run a full collection (unless paused). Used by
580    /// callers that want to short-circuit expensive prep work (e.g. snapshotting
581    /// weak tables / pending finalizers) when no collection will actually fire.
582    pub fn threshold_bytes(&self) -> usize {
583        self.threshold.get()
584    }
585
586    /// Cheap predicate: would a `step()` actually do work? Equivalent to
587    /// `!paused && bytes_used() >= threshold_bytes()`. Callers that build
588    /// snapshot state before invoking the heap should gate on this.
589    pub fn would_collect(&self) -> bool {
590        !self.paused.get() && self.bytes.get() >= self.threshold.get()
591    }
592
593    pub fn collections(&self) -> usize {
594        self.collections.get()
595    }
596
597    /// Forward write barrier: invoked when `parent` (already-traced black
598    /// object) gains a new reference to `child`. To preserve the tri-color
599    /// invariant ("no black points to white"), we mark the child gray
600    /// immediately. Cheap: one branch + maybe one queue push.
601    ///
602    /// During incremental mode this prevents the marking phase from missing
603    /// the new edge. In current stop-the-world mode it's still correct (a
604    /// no-op when the collection is idle), so call sites can be wired now
605    /// and the incremental upgrade is mechanical later.
606    pub fn barrier<P, C>(&self, parent: Gc<P>, child: Gc<C>)
607    where
608        P: Trace + 'static,
609        C: Trace + 'static,
610    {
611        if self.paused.get() || self.state.get().is_pause() {
612            return;
613        }
614        if parent.header().color.get() != Color::Black {
615            return;
616        }
617        if child.header().color.get() != Color::White {
618            return;
619        }
620        child.header().color.set(Color::Gray);
621        if let Ok(mut m_opt) = self.marker.try_borrow_mut() {
622            if let Some(m) = m_opt.as_mut() {
623                let ptr: NonNull<GcBox<dyn Trace>> = child.ptr;
624                m.gray_queue.push(ptr);
625                m.visited.insert(child.identity());
626            }
627        }
628    }
629
630    /// Possibly run a collection. Trigger: bytes_used > threshold.
631    /// Caller passes the root set (the runtime — typically `GlobalState`
632    /// implementing `Trace`).
633    pub fn step(&self, roots: &dyn Trace) {
634        self.step_with_post_mark(roots, |_: &mut Marker| {});
635    }
636
637    /// Like [`step`] but invokes a `post_mark` hook when a collection
638    /// actually fires (threshold reached). Hook is a no-op on the
639    /// short-circuit path. The runtime uses this to bridge weak-table
640    /// pruning into implicit GC steps fired from inside the VM loop.
641    pub fn step_with_post_mark<F: FnMut(&mut Marker)>(
642        &self,
643        roots: &dyn Trace,
644        post_mark: F,
645    ) {
646        if self.paused.get() {
647            return;
648        }
649        if self.bytes.get() < self.threshold.get() {
650            return;
651        }
652        self.full_collect_with_post_mark(roots, post_mark);
653    }
654
655    /// Stop-the-world full collect. Marks every reachable object from
656    /// `roots`, then sweeps white (unreachable) boxes from the allgc chain.
657    pub fn full_collect(&self, roots: &dyn Trace) {
658        self.full_collect_with_post_mark(roots, |_: &mut Marker| {});
659    }
660
661    /// Run only the mark/atomic hook portion of a collection, without sweeping.
662    ///
663    /// This is used by runtimes that need an atomic reachability snapshot for
664    /// weak-table cleanup while they are deliberately avoiding object freeing.
665    pub fn mark_only_with_post_mark<F: FnMut(&mut Marker)>(
666        &self,
667        roots: &dyn Trace,
668        mut post_mark: F,
669    ) {
670        if self.paused.get() {
671            return;
672        }
673        let mut cursor = self.head.get();
674        while let Some(ptr) = cursor {
675            let header = unsafe { &(*ptr.as_ptr()).header };
676            header.color.set(Color::White);
677            cursor = header.next.get();
678        }
679        let mut marker = Marker::new();
680        roots.trace(&mut marker);
681        marker.drain_gray_queue();
682        post_mark(&mut marker);
683        marker.drain_gray_queue();
684    }
685
686    /// Stop-the-world full collect with a post-mark hook.
687    ///
688    /// Internally drives the incremental state machine to completion with
689    /// an unbounded budget — equivalent to repeatedly calling
690    /// [`incremental_step_with_post_mark`] until it returns `Paused`. The
691    /// post-mark hook is invoked exactly once, during the atomic transition.
692    pub fn full_collect_with_post_mark<F: FnMut(&mut Marker)>(
693        &self,
694        roots: &dyn Trace,
695        mut post_mark: F,
696    ) {
697        if self.paused.get() {
698            return;
699        }
700        if !self.state.get().is_pause() {
701            self.abort_cycle();
702        }
703        let unlimited = StepBudget { remaining_work: isize::MAX, max_credit: isize::MAX };
704        loop {
705            let outcome = self.incremental_step_with_post_mark(roots, unlimited, &mut post_mark);
706            if matches!(outcome, StepOutcome::Paused | StepOutcome::SkippedStopped) {
707                break;
708            }
709        }
710    }
711
712    /// Run one budgeted step of the incremental collector.
713    ///
714    /// The state machine advances `Pause → Propagate → Atomic → Sweep →
715    /// Finalize → Pause`. Each phase consumes budget; the call returns when
716    /// the budget runs out or the cycle reaches `Pause`. The `post_mark`
717    /// hook is invoked exactly once per cycle, during the `Atomic`
718    /// transition (after the initial gray-queue drain, before sweep starts).
719    ///
720    /// Returns:
721    /// - [`StepOutcome::Paused`] — the cycle completed.
722    /// - [`StepOutcome::InProgress`] — budget exhausted before the cycle
723    ///   finished; caller may step again.
724    /// - [`StepOutcome::SkippedStopped`] — heap is paused; nothing happened.
725    pub fn incremental_step_with_post_mark<F: FnMut(&mut Marker)>(
726        &self,
727        roots: &dyn Trace,
728        mut budget: StepBudget,
729        mut post_mark: F,
730    ) -> StepOutcome {
731        if self.paused.get() {
732            return StepOutcome::SkippedStopped;
733        }
734        self.run_budgeted(roots, &mut budget, &mut post_mark);
735        if self.state.get().is_pause() {
736            StepOutcome::Paused
737        } else {
738            StepOutcome::InProgress
739        }
740    }
741
742    fn run_budgeted(
743        &self,
744        roots: &dyn Trace,
745        budget: &mut StepBudget,
746        post_mark: &mut dyn FnMut(&mut Marker),
747    ) -> bool {
748        let mut did_work = false;
749        loop {
750            if budget.remaining_work <= -budget.max_credit {
751                return did_work;
752            }
753            match self.state.get() {
754                GcState::Pause => {
755                    self.start_cycle(roots);
756                    self.state.set(GcState::Propagate);
757                    budget.remaining_work -= 1;
758                    did_work = true;
759                }
760                GcState::Propagate => {
761                    let work = self.drain_gray_budgeted(budget.remaining_work.max(1));
762                    budget.remaining_work -= work as isize;
763                    did_work = did_work || work > 0;
764                    let empty = {
765                        let m = self.marker.borrow();
766                        m.as_ref().map(|m| m.gray_queue.is_empty()).unwrap_or(true)
767                    };
768                    if empty {
769                        self.state.set(GcState::Atomic);
770                    } else if budget.remaining_work <= 0 {
771                        return did_work;
772                    }
773                }
774                GcState::Atomic => {
775                    self.run_atomic(post_mark);
776                    self.state.set(GcState::Sweep);
777                    budget.remaining_work -= 1;
778                    did_work = true;
779                }
780                GcState::Sweep => {
781                    let work = self.sweep_budgeted(budget.remaining_work.max(1));
782                    budget.remaining_work -= work as isize;
783                    did_work = did_work || work > 0;
784                    if self.sweep_prev_next.get().is_none() {
785                        self.state.set(GcState::Finalize);
786                    } else if budget.remaining_work <= 0 {
787                        return did_work;
788                    }
789                }
790                GcState::Finalize => {
791                    self.finish_cycle();
792                    self.state.set(GcState::Pause);
793                    return did_work;
794                }
795            }
796        }
797    }
798
799    fn start_cycle(&self, roots: &dyn Trace) {
800        let mut cursor = self.head.get();
801        while let Some(ptr) = cursor {
802            let header = unsafe { &(*ptr.as_ptr()).header };
803            header.color.set(Color::White);
804            cursor = header.next.get();
805        }
806        let mut marker = Marker::new();
807        roots.trace(&mut marker);
808        *self.marker.borrow_mut() = Some(marker);
809        self.sweep_prev_next.set(None);
810    }
811
812    fn drain_gray_budgeted(&self, max_units: isize) -> usize {
813        let mut m_opt = self.marker.borrow_mut();
814        let marker = match m_opt.as_mut() {
815            Some(m) => m,
816            None => return 0,
817        };
818        let mut work = 0usize;
819        let mut budget = max_units;
820        while budget > 0 {
821            let next = match marker.gray_queue.pop() {
822                Some(p) => p,
823                None => break,
824            };
825            unsafe {
826                let bx = next.as_ref();
827                bx.header.color.set(Color::Black);
828                bx.value.trace(marker);
829            }
830            work += 1;
831            budget -= 1;
832        }
833        work
834    }
835
836    fn run_atomic(&self, post_mark: &mut dyn FnMut(&mut Marker)) {
837        let mut m_opt = self.marker.borrow_mut();
838        if let Some(marker) = m_opt.as_mut() {
839            marker.drain_gray_queue();
840            post_mark(marker);
841            marker.drain_gray_queue();
842        }
843        self.sweep_prev_next.set(Some(NonNull::from(&self.head)));
844    }
845
846    fn sweep_budgeted(&self, max_units: isize) -> usize {
847        let mut work = 0usize;
848        let mut budget = max_units;
849        let mut freed_bytes = 0usize;
850        let mut prev_next_ptr = match self.sweep_prev_next.get() {
851            Some(p) => p,
852            None => return 0,
853        };
854        while budget > 0 {
855            let prev_cell = unsafe { prev_next_ptr.as_ref() };
856            let cursor = prev_cell.get();
857            let ptr = match cursor {
858                Some(p) => p,
859                None => {
860                    self.sweep_prev_next.set(None);
861                    break;
862                }
863            };
864            let header = unsafe { &(*ptr.as_ptr()).header };
865            let next = header.next.get();
866            match header.color.get() {
867                Color::White => {
868                    prev_cell.set(next);
869                    freed_bytes += header.size;
870                    unsafe {
871                        let _ = Box::from_raw(ptr.as_ptr());
872                    }
873                }
874                Color::Black | Color::Gray => {
875                    header.color.set(Color::White);
876                    prev_next_ptr = unsafe {
877                        NonNull::from(&(*ptr.as_ptr()).header.next)
878                    };
879                    self.sweep_prev_next.set(Some(prev_next_ptr));
880                }
881            }
882            work += 1;
883            budget -= 1;
884        }
885        if freed_bytes > 0 {
886            self.bytes.set(self.bytes.get().saturating_sub(freed_bytes));
887        }
888        work
889    }
890
891    fn finish_cycle(&self) {
892        *self.marker.borrow_mut() = None;
893        self.sweep_prev_next.set(None);
894        let next = self
895            .bytes
896            .get()
897            .saturating_mul(self.pause_multiplier.get())
898            / 100;
899        self.threshold.set(next.max(GC_MIN_THRESHOLD));
900        self.collections.set(self.collections.get() + 1);
901    }
902
903    fn abort_cycle(&self) {
904        if !self.state.get().is_pause() {
905            *self.marker.borrow_mut() = None;
906            self.sweep_prev_next.set(None);
907            self.state.set(GcState::Pause);
908        }
909    }
910
911    /// Returns the current state of the incremental collector.
912    pub fn gc_state(&self) -> GcState {
913        self.state.get()
914    }
915
916    /// Approximate number of live GC boxes, computed by walking the allgc
917    /// chain. Linear in heap size; used for cycle-cost estimation and tests.
918    pub fn allgc_count(&self) -> usize {
919        let mut count = 0usize;
920        let mut cursor = self.head.get();
921        while let Some(ptr) = cursor {
922            count += 1;
923            let header = unsafe { &(*ptr.as_ptr()).header };
924            cursor = header.next.get();
925        }
926        count
927    }
928
929    /// Drop every allocation, ignoring reachability. Called at shutdown.
930    /// After this returns, every outstanding `Gc<T>` is dangling — callers
931    /// must ensure no `Gc<T>` outlives the `Heap`.
932    pub fn drop_all(&self) {
933        *self.marker.borrow_mut() = None;
934        self.sweep_prev_next.set(None);
935        self.state.set(GcState::Pause);
936        let mut cursor = self.head.get();
937        self.head.set(None);
938        while let Some(ptr) = cursor {
939            // SAFETY: same chain invariant as full_collect's sweep.
940            let next = unsafe { (*ptr.as_ptr()).header.next.get() };
941            unsafe {
942                let _ = Box::from_raw(ptr.as_ptr());
943            }
944            cursor = next;
945        }
946        self.bytes.set(0);
947    }
948}
949
950impl Drop for Heap {
951    fn drop(&mut self) {
952        self.drop_all();
953    }
954}
955
956// ──────────────────────────────────────────────────────────────────────────
957// Tests — confirm the skeleton's invariants before agents ever touch it.
958// ──────────────────────────────────────────────────────────────────────────
959
960#[cfg(test)]
961mod tests {
962    use super::*;
963
964    /// A tiny GC-tracked type for the smoke test.
965    struct Cell0 {
966        next: Cell<Option<Gc<Cell0>>>,
967        marker_calls: Cell<usize>,
968    }
969
970    impl Trace for Cell0 {
971        fn trace(&self, m: &mut Marker) {
972            self.marker_calls.set(self.marker_calls.get() + 1);
973            if let Some(n) = self.next.get() {
974                m.mark(n);
975            }
976        }
977    }
978
979    /// Roots for tests: just a single Gc<Cell0>, or none.
980    struct OneRoot(Option<Gc<Cell0>>);
981    impl Trace for OneRoot {
982        fn trace(&self, m: &mut Marker) {
983            if let Some(g) = self.0 {
984                m.mark(g);
985            }
986        }
987    }
988
989    #[test]
990    fn alloc_and_drop_all() {
991        let heap = Heap::new();
992        heap.unpause();
993        let _a = heap.allocate(Cell0 {
994            next: Cell::new(None),
995            marker_calls: Cell::new(0),
996        });
997        let _b = heap.allocate(Cell0 {
998            next: Cell::new(None),
999            marker_calls: Cell::new(0),
1000        });
1001        assert!(heap.bytes_used() > 0);
1002        heap.drop_all();
1003        assert_eq!(heap.bytes_used(), 0);
1004    }
1005
1006    #[test]
1007    fn collect_unreachable_frees_bytes() {
1008        let heap = Heap::new();
1009        heap.unpause();
1010        let _a = heap.allocate(Cell0 {
1011            next: Cell::new(None),
1012            marker_calls: Cell::new(0),
1013        });
1014        let bytes_before = heap.bytes_used();
1015        assert!(bytes_before > 0);
1016        // No roots — everything should sweep.
1017        heap.full_collect(&OneRoot(None));
1018        assert_eq!(heap.bytes_used(), 0);
1019        assert_eq!(heap.collections(), 1);
1020    }
1021
1022    #[test]
1023    fn collect_keeps_reachable() {
1024        let heap = Heap::new();
1025        heap.unpause();
1026        let root = heap.allocate(Cell0 {
1027            next: Cell::new(None),
1028            marker_calls: Cell::new(0),
1029        });
1030        let bytes_before = heap.bytes_used();
1031        heap.full_collect(&OneRoot(Some(root)));
1032        assert_eq!(heap.bytes_used(), bytes_before);
1033        assert_eq!(root.marker_calls.get(), 1);
1034    }
1035
1036    #[test]
1037    fn collect_traverses_cycles() {
1038        let heap = Heap::new();
1039        heap.unpause();
1040        let a = heap.allocate(Cell0 {
1041            next: Cell::new(None),
1042            marker_calls: Cell::new(0),
1043        });
1044        let b = heap.allocate(Cell0 {
1045            next: Cell::new(Some(a)),
1046            marker_calls: Cell::new(0),
1047        });
1048        a.next.set(Some(b)); // cycle
1049        // With a as root, both should survive.
1050        heap.full_collect(&OneRoot(Some(a)));
1051        assert_eq!(a.marker_calls.get(), 1);
1052        assert_eq!(b.marker_calls.get(), 1);
1053        // Drop the only root path; cycle should now be collected.
1054        // (Note: `a` and `b` are still on the stack as Gc<Cell0> handles, but
1055        // they're not in a Trace-visible position.)
1056        heap.full_collect(&OneRoot(None));
1057        assert_eq!(heap.bytes_used(), 0);
1058    }
1059
1060    #[test]
1061    fn heap_guard_stacks() {
1062        assert!(with_current_heap(|heap| heap.is_none()), "no guard initially");
1063        let h1 = Heap::new();
1064        h1.unpause();
1065        {
1066            let _g1 = HeapGuard::push(&h1);
1067            assert!(with_current_heap(|heap| heap.is_some()));
1068            let h2 = Heap::new();
1069            h2.unpause();
1070            {
1071                let _g2 = HeapGuard::push(&h2);
1072                // top of stack is h2
1073                with_current_heap(|heap| {
1074                    assert!(std::ptr::addr_eq(
1075                        heap.unwrap() as *const _,
1076                        &h2 as *const _,
1077                    ));
1078                });
1079            }
1080            // _g2 dropped — top is back to h1
1081            with_current_heap(|heap| {
1082                assert!(std::ptr::addr_eq(
1083                    heap.unwrap() as *const _,
1084                    &h1 as *const _,
1085                ));
1086            });
1087        }
1088        assert!(
1089            with_current_heap(|heap| heap.is_none()),
1090            "stack empty after all guards drop"
1091        );
1092    }
1093
1094    #[test]
1095    fn step_threshold_triggers_collect() {
1096        let heap = Heap::new();
1097        heap.unpause();
1098        // Allocate enough boxes to exceed the default 64KB threshold.
1099        let mut keeps = Vec::new();
1100        for _ in 0..64 {
1101            // ~1KB per box (Cell0 is small, but allocating many headers
1102            // accumulates). For the threshold test we'd typically allocate
1103            // larger objects; this is a smoke test.
1104            keeps.push(heap.allocate(Cell0 {
1105                next: Cell::new(None),
1106                marker_calls: Cell::new(0),
1107            }));
1108        }
1109        // Build roots that retain all of keeps via individual marks.
1110        struct ManyRoots<'a>(&'a [Gc<Cell0>]);
1111        impl<'a> Trace for ManyRoots<'a> {
1112            fn trace(&self, m: &mut Marker) {
1113                for g in self.0.iter() {
1114                    m.mark(*g);
1115                }
1116            }
1117        }
1118        heap.step(&ManyRoots(&keeps));
1119        // step is a no-op below threshold; all roots retained.
1120        assert!(heap.bytes_used() > 0);
1121    }
1122
1123    #[test]
1124    fn threshold_floored_after_collecting_tiny_heap() {
1125        let heap = Heap::new();
1126        heap.unpause();
1127        struct NoRoots;
1128        impl Trace for NoRoots {
1129            fn trace(&self, _m: &mut Marker) {}
1130        }
1131        for _ in 0..200 {
1132            heap.allocate(Cell0 {
1133                next: Cell::new(None),
1134                marker_calls: Cell::new(0),
1135            });
1136        }
1137        heap.full_collect(&NoRoots);
1138        assert!(
1139            heap.threshold_bytes() >= GC_MIN_THRESHOLD,
1140            "threshold {} collapsed below floor {}; a churning program would full-collect per allocation",
1141            heap.threshold_bytes(),
1142            GC_MIN_THRESHOLD
1143        );
1144    }
1145
1146    fn build_chain(heap: &Heap, len: usize) -> Gc<Cell0> {
1147        let head = heap.allocate(Cell0 {
1148            next: Cell::new(None),
1149            marker_calls: Cell::new(0),
1150        });
1151        let mut cur = head;
1152        for _ in 1..len {
1153            let n = heap.allocate(Cell0 {
1154                next: Cell::new(None),
1155                marker_calls: Cell::new(0),
1156            });
1157            cur.next.set(Some(n));
1158            cur = n;
1159        }
1160        head
1161    }
1162
1163    #[test]
1164    fn budget_zero_does_some_work() {
1165        let heap = Heap::new();
1166        heap.unpause();
1167        let head = build_chain(&heap, 8);
1168        let roots = OneRoot(Some(head));
1169        let budget = StepBudget::from_work(0);
1170        let outcome = heap.incremental_step_with_post_mark(&roots, budget, |_| {});
1171        assert_ne!(outcome, StepOutcome::SkippedStopped);
1172        assert_ne!(heap.gc_state(), GcState::Pause);
1173    }
1174
1175    #[test]
1176    fn larger_budget_drains_more_gray_than_smaller() {
1177        let small_heap = Heap::new();
1178        small_heap.unpause();
1179        let h1 = build_chain(&small_heap, 64);
1180        let r1 = OneRoot(Some(h1));
1181        let mut small_calls = 0;
1182        loop {
1183            small_calls += 1;
1184            let outcome = small_heap.incremental_step_with_post_mark(
1185                &r1,
1186                StepBudget::from_work(2),
1187                |_| {},
1188            );
1189            if outcome == StepOutcome::Paused {
1190                break;
1191            }
1192            assert!(small_calls < 10_000, "did not converge");
1193        }
1194
1195        let big_heap = Heap::new();
1196        big_heap.unpause();
1197        let h2 = build_chain(&big_heap, 64);
1198        let r2 = OneRoot(Some(h2));
1199        let mut big_calls = 0;
1200        loop {
1201            big_calls += 1;
1202            let outcome = big_heap.incremental_step_with_post_mark(
1203                &r2,
1204                StepBudget::from_work(64),
1205                |_| {},
1206            );
1207            if outcome == StepOutcome::Paused {
1208                break;
1209            }
1210            assert!(big_calls < 10_000, "did not converge");
1211        }
1212
1213        assert!(
1214            big_calls < small_calls,
1215            "expected big_calls={} < small_calls={}",
1216            big_calls,
1217            small_calls
1218        );
1219    }
1220
1221    #[test]
1222    fn sweep_can_pause_and_resume() {
1223        let heap = Heap::new();
1224        heap.unpause();
1225        for _ in 0..16 {
1226            let _ = heap.allocate(Cell0 {
1227                next: Cell::new(None),
1228                marker_calls: Cell::new(0),
1229            });
1230        }
1231        let roots = OneRoot(None);
1232        let bytes_before = heap.bytes_used();
1233        assert!(bytes_before > 0);
1234        let mut step_count = 0;
1235        let mut saw_in_progress_during_sweep = false;
1236        loop {
1237            step_count += 1;
1238            let outcome = heap.incremental_step_with_post_mark(
1239                &roots,
1240                StepBudget::from_work(2),
1241                |_| {},
1242            );
1243            if heap.gc_state() == GcState::Sweep && outcome == StepOutcome::InProgress {
1244                saw_in_progress_during_sweep = true;
1245            }
1246            if outcome == StepOutcome::Paused {
1247                break;
1248            }
1249            assert!(step_count < 10_000, "did not converge");
1250        }
1251        assert!(saw_in_progress_during_sweep, "sweep never paused mid-list");
1252        assert_eq!(heap.bytes_used(), 0);
1253    }
1254
1255    #[test]
1256    fn post_mark_runs_once_per_atomic() {
1257        let heap = Heap::new();
1258        heap.unpause();
1259        for _ in 0..32 {
1260            let _ = heap.allocate(Cell0 {
1261                next: Cell::new(None),
1262                marker_calls: Cell::new(0),
1263            });
1264        }
1265        let roots = OneRoot(None);
1266        let call_count = std::cell::Cell::new(0);
1267        let mut step_count = 0;
1268        loop {
1269            step_count += 1;
1270            let outcome = heap.incremental_step_with_post_mark(
1271                &roots,
1272                StepBudget::from_work(2),
1273                |_| {
1274                    call_count.set(call_count.get() + 1);
1275                },
1276            );
1277            if outcome == StepOutcome::Paused {
1278                break;
1279            }
1280            assert!(step_count < 10_000, "did not converge");
1281        }
1282        assert_eq!(call_count.get(), 1, "post_mark must run exactly once per cycle");
1283    }
1284
1285    #[test]
1286    fn full_collect_equivalent_to_incremental_to_pause() {
1287        let h1 = Heap::new();
1288        h1.unpause();
1289        let head1 = h1.allocate(Cell0 {
1290            next: Cell::new(None),
1291            marker_calls: Cell::new(0),
1292        });
1293        let _orphan1 = h1.allocate(Cell0 {
1294            next: Cell::new(None),
1295            marker_calls: Cell::new(0),
1296        });
1297        let _orphan2 = h1.allocate(Cell0 {
1298            next: Cell::new(None),
1299            marker_calls: Cell::new(0),
1300        });
1301        let roots1 = OneRoot(Some(head1));
1302        h1.full_collect(&roots1);
1303        let bytes_full = h1.bytes_used();
1304
1305        let h2 = Heap::new();
1306        h2.unpause();
1307        let head2 = h2.allocate(Cell0 {
1308            next: Cell::new(None),
1309            marker_calls: Cell::new(0),
1310        });
1311        let _orphan3 = h2.allocate(Cell0 {
1312            next: Cell::new(None),
1313            marker_calls: Cell::new(0),
1314        });
1315        let _orphan4 = h2.allocate(Cell0 {
1316            next: Cell::new(None),
1317            marker_calls: Cell::new(0),
1318        });
1319        let roots2 = OneRoot(Some(head2));
1320        loop {
1321            let outcome = h2.incremental_step_with_post_mark(
1322                &roots2,
1323                StepBudget::from_work(1),
1324                |_| {},
1325            );
1326            if outcome == StepOutcome::Paused {
1327                break;
1328            }
1329        }
1330        assert_eq!(h2.bytes_used(), bytes_full);
1331    }
1332}
1333
1334// ──────────────────────────────────────────────────────────────────────────────
1335// PORT STATUS
1336//   source:        production Rust heap/collector substrate
1337//   target_crate:  lua-gc
1338//   confidence:    medium
1339//   todos:         0
1340//   port_notes:    0
1341//   unsafe_blocks: 13
1342//   notes:         Mark-and-sweep collector heap + the Gc<T> raw-pointer substrate. Uses
1343//                  NonNull<GcBox<T>> with linked-list allgc walking; unsafe is
1344//                  required for raw pointer ops and Box::into_raw/from_raw. See
1345//                  unsafe-budgets.toml for the per-crate ceiling.
1346// ──────────────────────────────────────────────────────────────────────────────