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}