Skip to main content

praxis_runtime/
roots.rs

1//! Explicit root frames (§12.3, ADR-012) and the composite runtime root set.
2//!
3//! §12.3 offers "compiler-managed shadow-stack frames **or** explicit root
4//! frames"; this runtime takes explicit root frames (ADR-012): a [`RootSet`] is
5//! anything that can enumerate the `GcRef`s it keeps alive, and a RAII
6//! [`RootScope`] holds a `Vec<GcRef>` and chains to an optional parent.
7//!
8//! [`RuntimeRoots`] closes the collector's root set: it is the only thing
9//! `Heap::collect` accepts, it is constructible only from a `*mut
10//! RuntimeContext`, and it is exhaustive over its six arms — five **strong**,
11//! enumerated by its [`RootSet`] impl, and one **weak**, cleared by its
12//! [`WeakSet`] impl. `ctx.shadow`, `input_source`, a parse failure's partial
13//! value, a runtime-owned crash snapshot and everything native code holds in a
14//! Rust local are all arms of it, so "collect against a partial root set" has
15//! no representation.
16//!
17//! [`NativeScope`] is the fifth strong arm. Native code that builds a value
18//! across an allocation — the grid helpers assembling a `Vec` of points, the
19//! parser interpreter assembling a record — holds it in a `Rooted`, which is
20//! the only input the `&mut Payload` accessors take. Holding a payload
21//! reference across a safepoint without rooting its owner does not type-check.
22//! The references themselves live in **one** contiguous [`NativeRootStore`] the
23//! runtime owns (ADR-114); a scope is the run of entries above the watermark it
24//! found, not an object.
25//!
26//! The sixth arm is the crash debugger's frames, and it is weak (ADR-106): the
27//! collector never traces it — tracing it would re-merge the two sets ADR-044
28//! split — but it does *scan* it, once per collection, immediately after the
29//! sweep, marking every debug slot whose object that sweep reclaimed. A debug
30//! value is therefore always a live object or a stated absence, never a
31//! dangling reference — and the absence says *which* absence it is
32//! ([`crate::debug::RECLAIMED_WORD`]): a slot the collector emptied is not a
33//! slot nothing was ever written into.
34
35use std::cell::RefCell;
36use std::marker::PhantomData;
37
38use crate::GcRef;
39use crate::context::RuntimeContext;
40
41/// Anything that can enumerate the GC references it keeps alive (§12.3).
42///
43/// The collector treats every yielded `GcRef` (plus everything transitively
44/// reachable through it) as a root.
45pub trait RootSet {
46    /// Push every root held by this set into `out`, in any order.
47    fn push_roots(&self, out: &mut Vec<GcRef>);
48}
49
50/// A no-roots impl so the top-level scope can be rooted on `()`.
51impl RootSet for () {
52    fn push_roots(&self, _out: &mut Vec<GcRef>) {}
53}
54
55/// A set the collector keeps **valid** without keeping **alive** (ADR-106).
56///
57/// A [`RootSet`] answers "what must survive". This answers the other question:
58/// "what names storage, but has no say in whether that storage survives". Such
59/// a set is never traced, so it retains nothing; instead it is scanned once per
60/// collection, immediately after the sweep, and every entry naming reclaimed
61/// storage is turned into an absence rather than left as a dangling reference.
62///
63/// The one implementor that matters is [`RuntimeRoots`], whose weak arm is the
64/// crash debugger's per-call value slots. Those deliberately outlive the shadow
65/// slots that root them — ADR-044 decision 2 nulls a shadow slot the moment its
66/// local dies, while the debugger must keep rendering the value — so between
67/// the death and the fault the debugger names something the collector is free
68/// to reclaim, and, since swept storage is reusable, free to *reissue as an
69/// object of another type*.
70///
71/// **Weak, not strong, is the whole point.** Rooting those slots strongly is a
72/// two-line change and it is the set-merge ADR-044 exists to refuse: it makes
73/// the GC root set the over-approximate one again and fails
74/// `a_dead_local_stops_being_reachable_from_its_frame` by construction.
75///
76/// The timing is as load-bearing as the weakness; see
77/// [`Heap::collect`](crate::Heap::collect) and ADR-106 decision 2. "Reclaimed"
78/// is only observable in the window between the sweep that reclaimed a block
79/// and the next allocation that reissues it, so the clear has to happen inside
80/// the collection. A filter applied later — at the snapshot, at the render —
81/// cannot distinguish a block that died from one that died and came back.
82pub trait WeakSet {
83    /// Mark every entry of this set whose object the just-finished sweep
84    /// reclaimed, and answer how many were marked.
85    ///
86    /// The count is for tests and for the measurement ADR-106 records; nothing
87    /// on the collection path reads it.
88    fn clear_reclaimed(&self) -> usize;
89}
90
91/// A no-weak-set impl, so in-crate tests can collect against a bare
92/// [`RootScope`]. Mirrors `impl RootSet for ()`.
93impl WeakSet for () {
94    fn clear_reclaimed(&self) -> usize {
95        0
96    }
97}
98
99/// A RAII frame that roots a set of `GcRef`s and optionally chains to a parent
100/// [`RootSet`].
101///
102/// Roots are added via [`RootScope::root`] and dropped automatically when the
103/// scope ends. A scope keeps its own roots live; the collector also walks the
104/// parent chain, so a nested scope's roots supplement (never replace) its
105/// ancestors'.
106pub struct RootScope<'a> {
107    parent: Option<&'a dyn RootSet>,
108    roots: Vec<GcRef>,
109}
110
111impl<'a> RootScope<'a> {
112    /// A fresh top-level scope with no parent.
113    pub fn new() -> Self {
114        RootScope {
115            parent: None,
116            roots: Vec::new(),
117        }
118    }
119
120    /// A scope that chains onto `parent`; its roots are added to `parent`'s.
121    pub fn child(parent: &'a dyn RootSet) -> Self {
122        RootScope {
123            parent: Some(parent),
124            roots: Vec::new(),
125        }
126    }
127
128    /// Register `gcref` as a root for the lifetime of this scope and return a
129    /// copy of it. The returned `GcRef` is kept alive until the scope drops.
130    ///
131    /// Called for its rooting side-effect; the returned copy is a convenience
132    /// for chaining.
133    pub fn root(&mut self, gcref: GcRef) -> GcRef {
134        self.roots.push(gcref);
135        gcref
136    }
137
138    /// Number of roots held directly by this scope (excluding the parent chain).
139    pub fn root_count(&self) -> usize {
140        self.roots.len()
141    }
142}
143
144impl Default for RootScope<'_> {
145    fn default() -> Self {
146        Self::new()
147    }
148}
149
150impl RootSet for RootScope<'_> {
151    fn push_roots(&self, out: &mut Vec<GcRef>) {
152        if let Some(parent) = self.parent {
153            parent.push_roots(out);
154        }
155        out.extend_from_slice(&self.roots);
156    }
157}
158
159// ---------------------------------------------------------------------------
160// The native root store (ADR-114)
161// ---------------------------------------------------------------------------
162
163/// How many roots the store reserves at [`Runtime::new`](crate::Runtime::new).
164///
165/// **This is a reservation and not a bound**, which is the whole difference
166/// between this and [`SHADOW_STACK_SLOTS`], where exhaustion is unrepresentable
167/// rather than handled. ADR-101 could close that arithmetic because both of its
168/// factors are constants: depth is capped by ADR-105's byte budget and width by
169/// [`SlotCount`]. **Here only the depth half holds.** Every nested scope sits
170/// under a Praxis call or a parser-plan node, so the budget bounds it — measured
171/// depth is 1 on every benchmark, 2 for `read lines(int)`, 3 for a template
172/// inside `lines`. A single scope's *width* is bounded by nothing at all:
173/// `walk_lines` (`parser.rs`) opens one scope for a whole `lines(…)` walk and
174/// roots one reference per input line, and `praxis_bfs` opens one for an entire
175/// search. Measured: **200,001 roots** for a 200,000-line `read lines(int)`, and
176/// 119,997 for a 40,000-node `bfs`. No constant covers that, and an `assert!`
177/// that tried would turn a large puzzle input into a process abort — the exact
178/// failure ADR-105's guard exists to prevent.
179///
180/// So the store reallocs, and the two populations it serves are 1 and *the
181/// input*, with nothing in between: **the reservation is not sized to demand,
182/// because there is no demand curve to size to.** 1024 roots is 8 KiB, three
183/// orders of magnitude above every bounded program measured. It buys the
184/// bounded population "one `malloc` per `Runtime`, ever", which is the property
185/// this store is for.
186///
187/// **Nothing turns on the number**, and that is worth saying rather than
188/// implying: for the unbounded population the growth is `Vec`'s doubling, so the
189/// total bytes copied is within a factor of two of the final size whatever the
190/// starting capacity, and even `Vec::new()` would cost the bounded population
191/// exactly one allocation, at the first `root()` of the program's life. Raising
192/// this is a memory decision, not a speed one.
193///
194/// [`SHADOW_STACK_SLOTS`]: crate::SHADOW_STACK_SLOTS
195/// [`SlotCount`]: crate::SlotCount
196pub const NATIVE_ROOT_RESERVATION: usize = 1024;
197
198/// Every `GcRef` the runtime's own Rust code is holding across an allocation,
199/// in one contiguous array (ADR-114).
200///
201/// The runtime's own wrappers build values in Rust locals — a result `Vec` that
202/// is filled by repeatedly allocating points, a record assembled field by
203/// field. Those locals are invisible to the shadow stack, which only generated
204/// code writes. There is exactly one store per [`Runtime`](crate::Runtime), it
205/// is reachable through [`RuntimeContext::native_roots`], and it is the fifth
206/// strong arm of [`RuntimeRoots`].
207///
208/// **A frame is not an object.** A [`NativeScope`] is the run of entries above
209/// the watermark it found on entry, exactly as ADR-101 made a shadow frame the
210/// run of slots above the `top` a prologue found, so opening one allocates
211/// nothing — which matters because it sits on the path of `praxis_vec_push`,
212/// `praxis_map_insert` and every other mutating collection primitive in the
213/// language.
214///
215/// Roots are held behind a `RefCell` so [`NativeScope::root`] can take `&self`
216/// and several `Rooted` values can be live at once — the common shape, since a
217/// helper usually roots its result and then roots each intermediate it builds.
218/// Nothing can collect inside the `borrow_mut`: the only thing a push can call
219/// is the *system* allocator, on a growth, and that is not a safepoint.
220#[derive(Debug)]
221pub struct NativeRootStore {
222    roots: RefCell<Vec<GcRef>>,
223}
224
225impl NativeRootStore {
226    /// A store with [`NATIVE_ROOT_RESERVATION`] roots' worth of capacity.
227    #[must_use]
228    pub fn new() -> NativeRootStore {
229        NativeRootStore {
230            roots: RefCell::new(Vec::with_capacity(NATIVE_ROOT_RESERVATION)),
231        }
232    }
233
234    /// How many roots are currently held, across every live scope. Zero between
235    /// runs, if every scope was balanced by its `Drop`.
236    #[must_use]
237    pub fn len(&self) -> usize {
238        self.roots.borrow().len()
239    }
240
241    /// True iff no scope holds anything.
242    #[must_use]
243    pub fn is_empty(&self) -> bool {
244        self.len() == 0
245    }
246
247    /// The reservation's current capacity, in roots.
248    ///
249    /// Exposed because it is the observable form of "this program forced the
250    /// store to grow": the capacity only ever rises, so a value above
251    /// [`NATIVE_ROOT_RESERVATION`] is a realloc that happened, and the growth
252    /// path is the one a pointer-shaped watermark would have died on.
253    #[must_use]
254    pub fn capacity(&self) -> usize {
255        self.roots.borrow().capacity()
256    }
257
258    /// Append one root. The whole of [`NativeScope::root`]'s work.
259    #[inline]
260    fn push(&self, r: GcRef) {
261        self.roots.borrow_mut().push(r);
262    }
263
264    /// Drop everything above `watermark`.
265    ///
266    /// An absolute, not a subtraction, for ADR-101's reason — and `truncate`'s
267    /// own no-op-when-already-shorter rule is what makes it *self-healing*: a
268    /// scope dropped out of order restores a watermark that is already below the
269    /// current length, and the store simply stays where it is. A pop that could
270    /// *raise* the length would resurrect entries a live scope had released,
271    /// handing the collector references to storage a sweep may already have
272    /// reclaimed. There is no spelling for that here.
273    #[inline]
274    fn truncate(&self, watermark: usize) {
275        self.roots.borrow_mut().truncate(watermark);
276    }
277
278    /// Drop every root. Only correct between runs, when no scope is live.
279    pub(crate) fn reset(&mut self) {
280        self.roots.get_mut().clear();
281    }
282}
283
284impl Default for NativeRootStore {
285    fn default() -> Self {
286        Self::new()
287    }
288}
289
290impl RootSet for NativeRootStore {
291    /// One `extend_from_slice` over every live scope's roots at once.
292    ///
293    /// One copy yields *every* live scope's roots, for ADR-101's reason applied
294    /// to this chain: scopes nest with the Rust stack, each occupies exactly the
295    /// run between its own watermark and the next one's, and the runs partition
296    /// `[0, len)`. There is no per-frame walk, which matters because the parser
297    /// interpreter's own recursion can stack dozens of scopes in front of a
298    /// collection taken inside a parse.
299    fn push_roots(&self, out: &mut Vec<GcRef>) {
300        out.extend_from_slice(&self.roots.borrow());
301    }
302}
303
304/// A `GcRef` proven rooted for `'s` — the only input to a `&mut Payload`
305/// accessor.
306///
307/// A `Rooted<'s>` cannot outlive the [`NativeScope`] that produced it, and the
308/// accessors' results cannot outlive the `Rooted`, so the whole chain is bounded
309/// by a scope that is itself in the collector's root set. An accessor taking a
310/// bare `GcRef` and handing back a `&'static mut Payload` would say the payload
311/// outlives the program, and let a helper keep writing through one across an
312/// allocation that reclaimed its owner.
313///
314/// **It carries the reference by value, and that is what makes it survive the
315/// store's growth.** A `Rooted` that held a `*mut GcRef` into
316/// [`NativeRootStore`] would be the natural shape — `Drop` could then clear its
317/// own slot — and it would be wrong: the store reallocs, so a later `root()` in
318/// *any* live scope can move the array out from under every `Rooted` handed out
319/// before it. Holding the value instead means growth is invisible here. What the
320/// store owes a `Rooted` is not an address but a promise — that the reference is
321/// somewhere in `[0, len)` for as long as the scope lives — and moving the array
322/// does not break that promise.
323#[derive(Clone, Copy, Debug)]
324pub struct Rooted<'s> {
325    r: GcRef,
326    _scope: PhantomData<&'s ()>,
327}
328
329impl Rooted<'_> {
330    /// The underlying reference. Copying it out drops the proof, so this is for
331    /// passing the value on (as a call argument, as a return value), not for
332    /// re-deriving a payload reference.
333    #[inline]
334    #[must_use]
335    pub fn get(self) -> GcRef {
336        self.r
337    }
338}
339
340/// A RAII claim on the tail of [`NativeRootStore`]: records the store's length
341/// on construction and truncates back to it on `Drop`.
342///
343/// Create one in any runtime wrapper that holds a `GcRef` across something that
344/// may allocate, and root every such reference through it; `abi.rs`, the parser
345/// interpreter and the debugger's `p EXPR` all do. The type is a pointer, a
346/// `usize` and a `PhantomData`, constructed by an `unsafe fn new(ctx)` returning
347/// by value; `root` takes `&self` so several `Rooted` values can be live at
348/// once, and `Rooted<'s>` borrows from the scope.
349///
350/// **The watermark is a `usize` index and it must stay one.** The store grows —
351/// see [`NATIVE_ROOT_RESERVATION`] for why it has to — so a `*mut GcRef`
352/// watermark saved by an outer scope would dangle the moment an inner scope's
353/// `root()` reallocs, and `Drop` would then publish that dangling pointer as the
354/// store's new end. Only a scope that roots past the reservation while another
355/// is live exercises this: `a_scope_survives_the_growth_its_own_roots_force`
356/// and its sibling.
357pub struct NativeScope<'c> {
358    /// The store this scope claims from, or null when the context was null or
359    /// a [`placeholder`](RuntimeContext::placeholder). Shared, never `&mut`:
360    /// every mutation goes through the `RefCell`, which is what lets `root`
361    /// take `&self`.
362    store: *const NativeRootStore,
363    /// The store's length when this scope opened — everything above it belongs
364    /// to this scope and to the scopes it nests.
365    watermark: usize,
366    _ctx: PhantomData<&'c mut RuntimeContext>,
367}
368
369impl<'c> NativeScope<'c> {
370    /// Open a scope on `ctx`'s native root store.
371    ///
372    /// A null or unwired context is accepted: the scope has no store to claim
373    /// from, so `root` records nothing, but it still hands back a `Rooted`, so
374    /// the proof keeps its meaning on the defensive null-context paths. The
375    /// references are unreachable from a collection that cannot happen, because
376    /// a context with no store has no heap either.
377    ///
378    /// # Safety
379    /// `ctx` must be null, or point at a live `RuntimeContext` that outlives
380    /// this scope.
381    #[must_use]
382    pub unsafe fn new(ctx: *mut RuntimeContext) -> NativeScope<'c> {
383        let store: *const NativeRootStore = if ctx.is_null() {
384            std::ptr::null()
385        } else {
386            // SAFETY: caller guarantees `ctx` is live.
387            unsafe { (*ctx).native_roots }
388        };
389        // SAFETY: a non-null `native_roots` is the store owned by the `Runtime`
390        // this context views, which the caller guarantees outlives the scope.
391        let watermark = match unsafe { store.as_ref() } {
392            Some(store) => store.len(),
393            None => 0,
394        };
395        NativeScope {
396            store,
397            watermark,
398            _ctx: PhantomData,
399        }
400    }
401
402    /// Root `r` for the rest of this scope and return the proof.
403    ///
404    /// One bounds-checked store and one increment past the null test.
405    #[inline]
406    pub fn root(&self, r: GcRef) -> Rooted<'_> {
407        // SAFETY: as `new`'s — `store` is either null or the live store of the
408        // context this scope borrows.
409        if let Some(store) = unsafe { self.store.as_ref() } {
410            store.push(r);
411        }
412        Rooted {
413            r,
414            _scope: PhantomData,
415        }
416    }
417
418    /// The number of references this scope and everything nested inside it
419    /// currently root.
420    #[must_use]
421    pub fn root_count(&self) -> usize {
422        // SAFETY: as `new`'s.
423        match unsafe { self.store.as_ref() } {
424            Some(store) => store.len() - self.watermark,
425            None => 0,
426        }
427    }
428}
429
430impl Drop for NativeScope<'_> {
431    fn drop(&mut self) {
432        // SAFETY: `store` was the live store of the context when the scope was
433        // created, and the caller of `new` guaranteed that context outlives the
434        // scope.
435        if let Some(store) = unsafe { self.store.as_ref() } {
436            store.truncate(self.watermark);
437        }
438    }
439}
440
441// ---------------------------------------------------------------------------
442// The composite runtime root set
443// ---------------------------------------------------------------------------
444
445/// Everything the runtime owns that names a `GcRef` — five arms that keep one
446/// alive, and one that only has to keep one *valid*.
447///
448/// Sealed: the only constructor is [`RuntimeRoots::from_context`], so a
449/// collection cannot be run against a hand-picked subset. The six arms are
450/// every documented owner of a reference:
451///
452/// | arm | strength | owner |
453/// |---|---|---|
454/// | `shadow` | strong | `ctx.shadow` — the generated shadow stack, scanned `[base, top)` (ADR-019, ADR-101) |
455/// | `input` | strong | `ctx.input_source` — the read-in buffer |
456/// | `parse_partial` | strong | `ParseDetail.fail.partial` — the best partial parse |
457/// | `snapshot` | strong | the runtime-owned `CrashSnapshot`'s copied locals |
458/// | `native` | strong | [`NativeRootStore`] — what Rust helpers hold, scanned `[0, len)` (ADR-114) |
459/// | `debug` | **weak** | `ctx.debug_frames` + `ctx.debug_values` — the crash debugger's live frames and the value slots they name (ADR-104, ADR-106) |
460///
461/// `abi::maybe_collect` builds one of these and passes it whole, so a
462/// host-driven allocation and one taken inside the parser interpreter collect
463/// against the same arms generated code does.
464///
465/// ## Why the sixth arm is weak
466///
467/// The debug slots are the *over-approximate* set: ADR-044 split them from the
468/// root set precisely so that making the root set exact would not make the
469/// debugger render `<uninit>` for a local the user can still see in their
470/// source. `RootSlots::dead` nulls a shadow slot at its local's last use; the
471/// debug slot keeps the value, because a value that has been produced stays
472/// renderable.
473///
474/// Pushing `debug` in [`RootSet::push_roots`] would undo exactly that split. It
475/// is one line, it makes every dead local reachable again, and
476/// `a_dead_local_stops_being_reachable_from_its_frame` is the end-to-end gate
477/// that fails when someone writes it — deliberately, and it must keep failing.
478/// The arm's whole content is therefore in [`WeakSet::clear_reclaimed`]: the
479/// collector decides what dies without consulting the debugger, and then tells
480/// the debugger what died.
481pub struct RuntimeRoots<'a> {
482    shadow: Option<&'a crate::ShadowStackHeader>,
483    input: Option<GcRef>,
484    parse_partial: Option<GcRef>,
485    snapshot: Option<&'a crate::CrashSnapshot>,
486    native: Option<&'a NativeRootStore>,
487    /// The weak arm (ADR-106). `None` on a placeholder context, exactly as the
488    /// strong arms are.
489    debug: Option<DebugArm<'a>>,
490}
491
492/// The weak arm's two halves: a frame is two claims on two stacks (ADR-104
493/// decision 3), and the clear needs both.
494///
495/// The scan is driven from the *frames*, because a frame entry is what pairs a
496/// run of value slots with the `local_count` that bounds it — the same pair
497/// `crash_snapshot::copy_stack` walks, so the set the collector clears and the
498/// set a snapshot copies are the same set by construction rather than by
499/// argument. The value stack comes along so
500/// [`DebugFrameStackHeader::clear_reclaimed`] can `debug_assert` that those runs
501/// really do partition `[base, top)`, which is the premise that makes "driven
502/// from the frames" and "every claimed slot" the same statement.
503///
504/// Shared references like every other arm. The collector *reads* both headers
505/// and writes through the `*mut Option<GcRef>` each frame entry carries — the
506/// same pointer `DebugFrameGuard::set` and every generated prologue write
507/// through, and one that carries the reservation's own provenance rather than
508/// being re-derived from a shared slice.
509#[derive(Clone, Copy)]
510struct DebugArm<'a> {
511    frames: &'a crate::DebugFrameStackHeader,
512    values: &'a crate::DebugValueStackHeader,
513}
514
515impl<'a> RuntimeRoots<'a> {
516    /// Read every root arm out of `ctx`.
517    ///
518    /// # Safety
519    /// `ctx` must be null, or point at a live `RuntimeContext` whose non-null
520    /// `shadow` / `parse_detail` / `crash_snapshot` / `native_roots` /
521    /// `debug_frames` / `debug_values` pointers reference live values for `'a`.
522    /// A non-null context's `input_source` must be a valid `GcRef`
523    /// (`RuntimeContext::placeholder` documents the same requirement).
524    #[must_use]
525    pub unsafe fn from_context(ctx: *mut RuntimeContext) -> RuntimeRoots<'a> {
526        if ctx.is_null() {
527            return RuntimeRoots {
528                shadow: None,
529                input: None,
530                parse_partial: None,
531                snapshot: None,
532                native: None,
533                debug: None,
534            };
535        }
536        // SAFETY: caller guarantees `ctx` is live for `'a`.
537        let c = unsafe { &*ctx };
538        RuntimeRoots {
539            // SAFETY: a non-null `shadow` is the header of the runtime-owned
540            // shadow stack, which is live for as long as the context is.
541            shadow: unsafe { c.shadow.as_ref() },
542            input: Some(c.input_source),
543            // SAFETY: a non-null `parse_detail` points at the runtime's slot.
544            parse_partial: unsafe { c.parse_detail.as_ref() }
545                .and_then(|d| d.fail.as_ref())
546                .and_then(|f| f.partial),
547            // SAFETY: a non-null `crash_snapshot` points at the runtime's slot.
548            snapshot: unsafe { c.crash_snapshot.as_ref() }.and_then(|s| s.get()),
549            // SAFETY: a non-null `native_roots` is the one store owned by the
550            // `Runtime` this context views, live for as long as the context is.
551            native: unsafe { c.native_roots.as_ref() },
552            // SAFETY: a non-null `debug_frames` / `debug_values` are the headers
553            // of the runtime-owned debug stacks, live for as long as the
554            // context. `Runtime::context` wires the two together or not at all,
555            // and `zip` is what says the arm needs both to mean anything.
556            debug: unsafe { c.debug_frames.as_ref() }
557                .zip(unsafe { c.debug_values.as_ref() })
558                .map(|(frames, values)| DebugArm { frames, values }),
559        }
560    }
561}
562
563impl RootSet for RuntimeRoots<'_> {
564    fn push_roots(&self, out: &mut Vec<GcRef>) {
565        // Exhaustive over all six arms. Destructured rather than field-accessed
566        // so adding an owner to `RuntimeContext` without deciding its strength
567        // fails to compile here.
568        let RuntimeRoots {
569            shadow,
570            input,
571            parse_partial,
572            snapshot,
573            native,
574            debug,
575        } = self;
576        if let Some(shadow) = shadow {
577            shadow.push_roots(out);
578        }
579        out.extend(input.iter().copied());
580        out.extend(parse_partial.iter().copied());
581        if let Some(snapshot) = snapshot {
582            snapshot.push_roots(out);
583        }
584        if let Some(native) = native {
585            native.push_roots(out);
586        }
587        // `debug` is bound and deliberately not pushed (ADR-106). This is the
588        // one arm that is named here only so the destructure stays exhaustive:
589        // the debug slots are the over-approximate set, so rooting them makes
590        // the collector's set over-approximate too, which is the merge ADR-044
591        // exists to refuse and which
592        // `a_dead_local_stops_being_reachable_from_its_frame` fails on. What the
593        // collector does with this arm instead is `WeakSet::clear_reclaimed`
594        // below; `the_debug_arm_contributes_no_strong_roots` pins the absence.
595        let _ = debug;
596    }
597}
598
599impl WeakSet for RuntimeRoots<'_> {
600    /// Null every debug value slot whose object the sweep just reclaimed.
601    ///
602    /// The whole of the weak arm. Called by `Heap::collect_inner` between the
603    /// sweep and the return to the allocator — see
604    /// [`WeakSet`] for why nowhere else will do.
605    fn clear_reclaimed(&self) -> usize {
606        let Some(arm) = self.debug else {
607            return 0;
608        };
609        // SAFETY: `from_context`'s contract puts the two stacks' liveness on its
610        // caller, and every claimed entry was written by a prologue (or by
611        // `debug::push_frame`) with a `'static` meta and the base of its own run
612        // of value slots. A collection can only be entered from a safepoint, and
613        // a prologue's claim and its two stores are straight-line with no
614        // safepoint between them, so a half-written entry is not a state this
615        // can observe.
616        unsafe { arm.frames.clear_reclaimed(arm.values) }
617    }
618}
619
620#[cfg(test)]
621mod tests {
622    use super::*;
623    use std::ptr::NonNull;
624
625    fn dummy_ref(n: usize) -> GcRef {
626        // A `GcRef` whose header is a leaked `GcHeader` — never dereferenced by
627        // these root-set tests; only the pointer identity is observed, and each
628        // call leaks its own header, so two refs are two addresses. `n` only
629        // labels the call site.
630        let header = Box::leak(Box::new(crate::GcHeader::detached()));
631        let nn = NonNull::from(header);
632        // SAFETY: `nn` points at a leaked, aligned, live header.
633        let r = unsafe { GcRef::from_non_null(nn) };
634        let _ = n;
635        r
636    }
637
638    #[test]
639    fn empty_scope_has_no_roots() {
640        let scope = RootScope::new();
641        let mut out = Vec::new();
642        scope.push_roots(&mut out);
643        assert!(out.is_empty());
644    }
645
646    #[test]
647    fn scope_yields_its_roots() {
648        let mut scope = RootScope::new();
649        let a = dummy_ref(1);
650        let b = dummy_ref(2);
651        scope.root(a);
652        scope.root(b);
653        let mut out = Vec::new();
654        scope.push_roots(&mut out);
655        assert_eq!(out.len(), 2);
656        assert!(out.contains(&a));
657        assert!(out.contains(&b));
658    }
659
660    #[test]
661    fn child_scope_chains_to_parent() {
662        let mut parent = RootScope::new();
663        let a = dummy_ref(1);
664        parent.root(a);
665        let mut child = RootScope::child(&parent);
666        let b = dummy_ref(2);
667        child.root(b);
668
669        let mut out = Vec::new();
670        child.push_roots(&mut out);
671        assert_eq!(out.len(), 2);
672        assert!(out.contains(&a));
673        assert!(out.contains(&b));
674    }
675
676    // -----------------------------------------------------------------------
677    // The native root store (ADR-114)
678    // -----------------------------------------------------------------------
679
680    /// A runtime plus a context wired to it, which is the only shape a
681    /// `NativeScope` can be opened against.
682    ///
683    /// The runtime is boxed because the fixture moves it into this struct after
684    /// minting the context, and `native_roots` — like `heap`, `pending_fault`
685    /// and `fault_message` — is a pointer *into* the `Runtime` rather than into
686    /// a separately boxed header the way `shadow` and the two debug stacks are.
687    /// That distinction is deliberate (only generated code needs a header that
688    /// survives a move; nothing outside this crate ever learns this address),
689    /// and it is the kind of thing a fixture discovers as a SIGBUS.
690    struct Native {
691        rt: Box<crate::Runtime>,
692        ctx: Box<RuntimeContext>,
693    }
694
695    impl Native {
696        fn new() -> Native {
697            let mut rt = Box::new(crate::Runtime::new());
698            let ctx = Box::new(rt.context());
699            Native { rt, ctx }
700        }
701
702        fn ctx_ptr(&mut self) -> *mut RuntimeContext {
703            &mut *self.ctx
704        }
705
706        fn store(&self) -> &NativeRootStore {
707            self.rt.native_root_store()
708        }
709
710        /// What the collector would see through the fifth arm.
711        fn native_roots(&mut self) -> Vec<GcRef> {
712            let ctx = self.ctx_ptr();
713            // SAFETY: `ctx` is a live view of `self.rt`, which outlives the
714            // borrow.
715            let roots = unsafe { RuntimeRoots::from_context(ctx) };
716            let mut out = Vec::new();
717            roots.push_roots(&mut out);
718            out
719        }
720    }
721
722    /// A `GcRef` from the real heap, so the collector can be run against it.
723    fn heap_ref(rt: &crate::Runtime, value: i64) -> GcRef {
724        rt.heap().alloc_unpaced(crate::scalars::INT_PAYLOAD, value)
725    }
726
727    #[test]
728    fn a_scope_claims_the_tail_and_drops_exactly_what_it_claimed() {
729        let mut f = Native::new();
730        let a = heap_ref(&f.rt, 1);
731        let b = heap_ref(&f.rt, 2);
732        assert!(
733            f.store().is_empty(),
734            "a fresh runtime holds no native roots"
735        );
736        {
737            let ctx = f.ctx_ptr();
738            // SAFETY: `ctx` is wired to `f.rt`, which outlives the scope.
739            let scope = unsafe { NativeScope::new(ctx) };
740            scope.root(a);
741            scope.root(b);
742            assert_eq!(scope.root_count(), 2);
743            assert_eq!(f.store().len(), 2);
744            let found = f.native_roots();
745            assert!(found.contains(&a) && found.contains(&b));
746        }
747        assert!(f.store().is_empty(), "the scope released its whole run");
748        assert!(f.native_roots().iter().all(|r| *r != a && *r != b));
749    }
750
751    #[test]
752    fn nested_scopes_partition_one_contiguous_run() {
753        // ADR-114's form of `nested_frames_are_one_contiguous_scan`: nested
754        // scopes are runs of one array, so the collector reads `[0, len)` once
755        // rather than walking a chain.
756        let mut f = Native::new();
757        let a = heap_ref(&f.rt, 1);
758        let b = heap_ref(&f.rt, 2);
759        let ctx = f.ctx_ptr();
760        // SAFETY: `ctx` is wired to `f.rt`, which outlives both scopes.
761        unsafe {
762            let outer = NativeScope::new(ctx);
763            outer.root(a);
764            {
765                let inner = NativeScope::new(ctx);
766                inner.root(b);
767                assert_eq!(inner.root_count(), 1);
768                assert_eq!(f.store().len(), 2, "one run holds both scopes");
769                let found = f.native_roots();
770                assert!(found.contains(&a) && found.contains(&b));
771            }
772            assert_eq!(
773                f.store().len(),
774                1,
775                "the inner pop restores the outer scope's extent"
776            );
777            assert!(!f.native_roots().contains(&b));
778            drop(outer);
779        }
780        assert!(f.store().is_empty());
781    }
782
783    #[test]
784    fn a_scope_survives_the_growth_its_own_roots_force() {
785        // The store reallocs — it must, because one scope's root count is the
786        // program's input (`praxis_bfs` roots ~2 per edge) — and a `*mut GcRef`
787        // watermark would be left pointing into the freed array. A `usize` index
788        // cannot be.
789        let mut f = Native::new();
790        let refs: Vec<GcRef> = (0..(NATIVE_ROOT_RESERVATION as i64 + 64))
791            .map(|n| heap_ref(&f.rt, n))
792            .collect();
793        let ctx = f.ctx_ptr();
794        // SAFETY: `ctx` is wired to `f.rt`, which outlives the scope.
795        let scope = unsafe { NativeScope::new(ctx) };
796        assert_eq!(f.store().capacity(), NATIVE_ROOT_RESERVATION);
797        for r in &refs {
798            scope.root(*r);
799        }
800        assert!(
801            f.store().capacity() > NATIVE_ROOT_RESERVATION,
802            "the reservation was not actually exceeded, so this test proves \
803             nothing: capacity is still {}",
804            f.store().capacity()
805        );
806        assert_eq!(scope.root_count(), refs.len());
807
808        // Every root is still found, still in order, and still the object it
809        // was — a moved array that was re-read correctly, rather than a stale
810        // pointer that happened not to crash.
811        let found = f.native_roots();
812        let native: Vec<GcRef> = found[found.len() - refs.len()..].to_vec();
813        assert_eq!(native, refs);
814        drop(scope);
815        assert!(f.store().is_empty());
816    }
817
818    #[test]
819    fn an_inner_scopes_growth_leaves_the_outer_scopes_watermark_valid() {
820        // The sharper half: the growth is forced by an *inner* scope, so the
821        // outer scope's saved watermark was taken before the array moved. Under
822        // a pointer watermark the outer `Drop` publishes an address inside the
823        // freed allocation as the store's new end, and the next collection reads
824        // it. Under an index it is arithmetic on a number.
825        let mut f = Native::new();
826        let outer_refs: Vec<GcRef> = (0..3).map(|n| heap_ref(&f.rt, n)).collect();
827        let inner_refs: Vec<GcRef> = (0..(NATIVE_ROOT_RESERVATION as i64 + 8))
828            .map(|n| heap_ref(&f.rt, 1_000 + n))
829            .collect();
830        let ctx = f.ctx_ptr();
831        // SAFETY: `ctx` is wired to `f.rt`, which outlives both scopes.
832        unsafe {
833            let outer = NativeScope::new(ctx);
834            for r in &outer_refs {
835                outer.root(*r);
836            }
837            {
838                let inner = NativeScope::new(ctx);
839                for r in &inner_refs {
840                    inner.root(*r);
841                }
842                assert!(f.store().capacity() > NATIVE_ROOT_RESERVATION);
843            }
844            assert_eq!(
845                f.store().len(),
846                outer_refs.len(),
847                "the inner scope released exactly its own run across a growth"
848            );
849            let found = f.native_roots();
850            for r in &outer_refs {
851                assert!(found.contains(r), "the outer scope lost a root");
852            }
853            for r in &inner_refs {
854                assert!(!found.contains(r), "a released root is still scanned");
855            }
856            drop(outer);
857        }
858        assert!(f.store().is_empty());
859    }
860
861    #[test]
862    fn a_rooted_handed_out_before_a_growth_still_names_its_object() {
863        // Why `Rooted` carries the reference by value. If it held a slot address
864        // instead — the shape that would let `Drop` clear its own entry — this
865        // is where it would dangle, and it would dangle silently: the read would
866        // land in freed-then-reused storage and answer *a* `GcRef`.
867        let mut f = Native::new();
868        let first = heap_ref(&f.rt, 7);
869        let filler: Vec<GcRef> = (0..(NATIVE_ROOT_RESERVATION as i64))
870            .map(|n| heap_ref(&f.rt, n))
871            .collect();
872        let ctx = f.ctx_ptr();
873        // SAFETY: `ctx` is wired to `f.rt`, which outlives the scope.
874        let scope = unsafe { NativeScope::new(ctx) };
875        let rooted = scope.root(first);
876        for r in &filler {
877            scope.root(*r);
878        }
879        assert!(f.store().capacity() > NATIVE_ROOT_RESERVATION);
880        assert_eq!(rooted.get(), first, "the proof still names its object");
881        assert!(f.native_roots().contains(&first));
882    }
883
884    #[test]
885    fn a_scope_dropped_out_of_order_cannot_raise_the_watermark() {
886        // Scopes nest with the Rust stack, so this is not a state the runtime
887        // reaches — but the release is `truncate`, whose no-op-when-shorter rule
888        // makes the bad order *unrepresentable* rather than merely unreached. A
889        // pop that could raise the length would republish entries a live scope
890        // had already released, and the collector would trace storage a sweep
891        // may have reclaimed.
892        let mut f = Native::new();
893        let a = heap_ref(&f.rt, 1);
894        let b = heap_ref(&f.rt, 2);
895        let ctx = f.ctx_ptr();
896        // SAFETY: `ctx` is wired to `f.rt`, which outlives both scopes.
897        let (outer, inner) = unsafe {
898            let outer = NativeScope::new(ctx);
899            outer.root(a);
900            let inner = NativeScope::new(ctx);
901            inner.root(b);
902            (outer, inner)
903        };
904        drop(outer);
905        assert_eq!(f.store().len(), 0, "the outer release took both runs");
906        drop(inner);
907        assert_eq!(
908            f.store().len(),
909            0,
910            "the late inner release restored a watermark above the length and \
911             the store stayed where it was"
912        );
913        assert!(f.native_roots().iter().all(|r| *r != a && *r != b));
914    }
915
916    #[test]
917    fn a_scope_on_a_null_context_roots_nothing_and_drops_cleanly() {
918        // The defensive path `NativeScope::new`'s contract allows. There is no
919        // store to claim from, so the proof is all the caller gets.
920        let mut rt = crate::Runtime::new();
921        let a = heap_ref(&rt, 1);
922        // SAFETY: a null context is explicitly accepted.
923        let scope = unsafe { NativeScope::new(std::ptr::null_mut()) };
924        assert_eq!(scope.root(a).get(), a);
925        assert_eq!(scope.root_count(), 0);
926        drop(scope);
927        assert!(rt.native_root_store().is_empty());
928        assert!(
929            !rt.context().native_roots.is_null(),
930            "a wired context is the case that does have a store"
931        );
932    }
933
934    #[test]
935    fn every_context_this_runtime_mints_sees_the_same_store() {
936        // The store is the runtime's, not the context's, so a context taken
937        // *while* a scope is open — which is what `Runtime::collect_now` and the
938        // debugger's `p EXPR` both do — sees it. A collection driven from the
939        // host is therefore not blind to what native code is holding.
940        let mut f = Native::new();
941        let a = heap_ref(&f.rt, 42);
942        let ctx = f.ctx_ptr();
943        // SAFETY: `ctx` is wired to `f.rt`, which outlives the scope.
944        let scope = unsafe { NativeScope::new(ctx) };
945        scope.root(a);
946
947        let mut fresh = f.rt.context();
948        // SAFETY: `fresh` is a second live view of the same runtime.
949        let roots = unsafe { RuntimeRoots::from_context(&mut fresh) };
950        let mut out = Vec::new();
951        roots.push_roots(&mut out);
952        assert!(
953            out.contains(&a),
954            "a freshly minted context could not see the open scope"
955        );
956    }
957
958    #[test]
959    fn a_native_root_survives_the_collection_that_reclaims_its_neighbour() {
960        // The end-to-end statement of the fifth arm, against the real sweep: two
961        // objects, one rooted in a scope and one held only in a Rust local, and
962        // the collection has to tell them apart.
963        let mut f = Native::new();
964        let kept = heap_ref(&f.rt, 111);
965        let dropped = heap_ref(&f.rt, 222);
966        let before = f.rt.heap().stats().live_count;
967        assert!(before >= 2);
968        let ctx = f.ctx_ptr();
969        // SAFETY: `ctx` is wired to `f.rt`, which outlives the scope.
970        let scope = unsafe { NativeScope::new(ctx) };
971        let rooted = scope.root(kept);
972        f.rt.collect_now();
973        assert!(
974            f.rt.heap().stats().live_count < before,
975            "nothing was reclaimed, so this test cannot distinguish the arms"
976        );
977        assert!(
978            f.native_roots().contains(&kept),
979            "the scope's root did not survive its own collection"
980        );
981        assert_eq!(rooted.get(), kept);
982        let _ = dropped;
983        drop(scope);
984    }
985
986    #[test]
987    fn the_reservation_is_a_reservation_and_not_a_bound() {
988        // ADR-114's whole decision, as an assertion: the store starts at
989        // `NATIVE_ROOT_RESERVATION` and goes past it rather than refusing. A
990        // hard cap here is a process abort on a graph one edge too large, which
991        // is the failure ADR-105's budget exists to prevent.
992        let store = NativeRootStore::new();
993        assert_eq!(store.capacity(), NATIVE_ROOT_RESERVATION);
994        assert!(store.is_empty());
995    }
996
997    /// The structural statement that the sixth arm is weak (ADR-106), one layer
998    /// below any heap behaviour: a value that *only* a debug slot names is not
999    /// in the set the collector traces.
1000    ///
1001    /// `a_dead_local_stops_being_reachable_from_its_frame` is the end-to-end
1002    /// form of the same property and is the gate that a future change did not
1003    /// quietly promote this arm to a strong one. This is the local form, and it
1004    /// fails on the line that would do it rather than on a heap size three
1005    /// layers away.
1006    #[test]
1007    fn the_debug_arm_contributes_no_strong_roots() {
1008        let mut rt = crate::Runtime::new();
1009        let value = rt.heap().alloc_unpaced(crate::scalars::INT_PAYLOAD, 9_999);
1010        let mut ctx = Box::new(rt.context());
1011
1012        let name = b"x";
1013        let locals = [crate::DebugLocalMeta {
1014            callee_name: std::ptr::null(),
1015            callee_name_len: 0,
1016            source_name: name.as_ptr(),
1017            name_len: 1,
1018            symbol_id: 1,
1019            descriptor: &crate::scalars::INT,
1020            type_id: 1,
1021            kind: crate::LOCAL_KIND_USER,
1022            span_start: 0,
1023            span_end: 0,
1024            slot_kind: crate::debug::DebugSlotKind::Reference,
1025        }];
1026        let meta = crate::FunctionDebugMeta {
1027            func_name: b"f".as_ptr(),
1028            func_name_len: 1,
1029            local_count: 1,
1030            locals: locals.as_ptr(),
1031            span_start: 0,
1032            span_end: 0,
1033        };
1034        // SAFETY: `ctx` is wired to `rt`, and `meta`/`locals` outlive the guard.
1035        let mut guard = unsafe { crate::debug::push_frame(&mut *ctx, &meta) };
1036        guard.set(0, value);
1037        assert_eq!(guard.values()[0], Some(value), "the debugger names it");
1038
1039        // SAFETY: `ctx` is a live view of `rt`, which outlives `roots`.
1040        let roots = unsafe { RuntimeRoots::from_context(&mut *ctx) };
1041        let mut out = Vec::new();
1042        roots.push_roots(&mut out);
1043        assert!(
1044            !out.contains(&value),
1045            "the debug slot put a value in the collector's strong set — that is \
1046             the ADR-044 set-merge, arriving as one line in `push_roots`"
1047        );
1048        drop(guard);
1049    }
1050}