Skip to main content

praxis_runtime/
debug.rs

1//! Crash-debugger frame registration (§9.3, ADR-021, ADR-104).
2//!
3//! What the crash debugger reads for `bt`/`locals` is, per live frame, a
4//! function's *static* metadata plus that call's *current* local values. The two
5//! are stored apart (ADR-104), because only one of them varies per call:
6//!
7//! - **Static:** [`FunctionDebugMeta`] — the function's name, source span, and
8//!   the [`DebugLocalMeta`] array. One per function, interned in the JIT
9//!   generation arena at compile time, shared by every call and every recursion
10//!   level. Nothing records it at runtime; it is a compile-time constant.
11//! - **Per call:** one machine word per `Gc` local, claimed from a contiguous
12//!   [`DebugValueStack`] the runtime owns, and one [`DebugFrameEntry`] pairing
13//!   the meta with the base of that run, claimed from a contiguous
14//!   [`DebugFrameStack`]. The word is an `Option<GcRef>` for every local whose
15//!   box survives compilation and a raw scalar payload for one whose box
16//!   ADR-120's forwarding elided; which it is, is [`DebugSlotKind`]'s to say
17//!   and nothing else's.
18//!
19//! Both stacks are [`SlotStack`]s — the mechanism ADR-101 built for the shadow
20//! stack and made generic for exactly this. A prologue claims its slots by
21//! bumping a `top` inline; an epilogue restores the saved base. **No malloc, no
22//! free, no extern call, no `catch_unwind` landing pad.**
23//!
24//! The values are written **once per definition** by the backend (ADR-104),
25//! rather than re-written over the whole live set at every safepoint, and they
26//! are never cleared: a value that has been produced stays renderable, which is
27//! MIR-16's contract and what `locals` in the crash REPL is for.
28//!
29//! ## The value slots are not a *strong* root set, and cannot become one by accident
30//!
31//! [`DebugValueStack`] is `SlotStack<Option<GcRef>>` while the shadow stack is
32//! `SlotStack<*mut GcHeader>`. The two are deliberately *different types*: the
33//! `RootSet` impl lives on `SlotStackHeader<*mut GcHeader>`, so the debug value
34//! stack does not have one and cannot be handed to the collector as something
35//! to trace. That is ADR-044's split made structural — the debug set is
36//! over-approximate and never cleared, and tracing it would re-couple the two
37//! sets and undo MIR-01.
38//!
39//! It also reads better: a debug slot holds a value or nothing, which
40//! `Option<GcRef>` says exactly (its `None` is the all-zero niche, F18, so a
41//! zeroed claim *is* a run of "nothing yet"). A shadow slot is a raw pointer
42//! only because the collector dereferences it and `GcRef` is `NonNull`.
43//!
44//! ## …but the collector does *write* them (ADR-106)
45//!
46//! Not tracing them leaves a hazard: a value whose shadow slot `RootSlots::dead`
47//! nulled, but whose debug slot still names it, is unreachable. A collection in
48//! that window frees it, `poison()` nulls its descriptor, and the block is then
49//! handed back out — after which the debug slot names a live object of an
50//! entirely different type, and `praxis_snapshot_debug_chain` copies that into a
51//! `CrashSnapshot`, which *is* a strong root set.
52//!
53//! So the debug frames are [`RuntimeRoots`](crate::RuntimeRoots)' one **weak**
54//! arm. [`DebugFrameStackHeader::clear_reclaimed`] runs once per collection,
55//! immediately after the sweep, and overwrites every slot naming reclaimed
56//! storage with [`RECLAIMED_WORD`]. The slots retain nothing — a dead local's
57//! object still dies on schedule — and what the debugger renders for it is
58//! `<collected>` rather than freed memory.
59//!
60//! ## …and one slot in three holds no reference at all (ADR-120 part 2)
61//!
62//! ADR-120's block-local forwarding deletes the box a value is put into so the
63//! next instruction can take it straight back out — and with the box goes the
64//! definition that would have written the debugger's slot. Part 2 gives that
65//! slot the *scalar* the box would have held, which means a value slot's word
66//! is not always an `Option<GcRef>`.
67//!
68//! That is a memory-safety statement, not a display one, because of the
69//! paragraph above: this stack is scanned after every sweep and the scan
70//! dereferences what it finds. **The discrimination is
71//! [`DebugLocalMeta::slot_kind`], a type, and [`DebugLocalMeta::read`] is the
72//! only way to turn a word into a value.** A scalar slot decodes to a
73//! [`DebugValue::Scalar`], which contains no `GcRef`, so no consumer — the
74//! scan, the crash snapshot's root set, or the debugger's `p EXPR` bindings —
75//! can reach a header through one. See [`DebugSlotKind`].
76
77use crate::MAX_RECURSION_DEPTH;
78use crate::context::{DebugLocal, RuntimeContext};
79use crate::gc::GcRef;
80use crate::shadow_stack::{
81    MAX_DEBUG_VALUE_SLOTS, MAX_LIVE_SLOTS, MAX_SHADOW_SLOTS, SlotStack, SlotStackHeader,
82};
83
84/// How a local appears in the crash debugger (§9.4 `locals`). Mirrors
85/// [`praxis_mir::ir::LocalDebugKind`], flattened to a `u8` for the FFI
86/// boundary: `0` = a binding, `1` = a compiler temp. "Binding" is ADR-125's
87/// sense — a `var`, a parameter, a `for` variable and a name a pattern
88/// introduces — so that the FFI constant and the compiler agree about what the
89/// byte means (ADR-139). Stored on each [`DebugLocalMeta`] so the debugger can
90/// separate the two in its display and name temps with their materializing
91/// expression.
92pub const LOCAL_KIND_USER: u8 = 0;
93pub const LOCAL_KIND_TEMP: u8 = 1;
94
95/// [`DebugLocalMeta::type_id`] when the MIR local has no static type
96/// (`MirType::Opaque`) — a pipeline accumulator, a fused-loop item.
97///
98/// A `Type` is an index into the compiler's arena, so every small integer is a
99/// valid handle and there is no in-band "none" — `0` would render as whatever
100/// type the arena interned first. `u32::MAX` is outside any arena the debugger
101/// will ever pair this with (`type_str` already omits an out-of-range id), and
102/// the metadata's null descriptor says the same thing in the other field.
103pub const NO_STATIC_TYPE: u32 = u32::MAX;
104
105/// What a debug value slot's word **is** — and the only thing in the process
106/// that can say so (ADR-120 part 2).
107///
108/// The word alone cannot. A slot holding the `Int` payload `4` and a slot
109/// holding a `GcRef` to address `4` are the same sixty-four bits, and there is
110/// no bit left over to tag them apart: [`DebugValueStack`] is one machine word
111/// per local by ADR-104's construction and the shadow stack's, which is what
112/// makes a definition's debug store a single `str`.
113///
114/// So the discrimination lives in the *static* metadata beside the slot, where
115/// it costs nothing per call and cannot be corrupted by a program: this field
116/// is written once per function by `build_function_debug_meta` at compile time,
117/// interned in the JIT generation arena, and never written again.
118///
119/// **This is the type that makes ADR-120 part 2 sound**, and it is a type
120/// rather than a `bool` for a reason. The failure mode the scalar slot creates
121/// is *the collector dereferencing an `f64` bit pattern as a [`GcHeader`]*
122/// (`crate::GcHeader`), and ADR-106 makes the debug frames the collector's one
123/// weak arm — every claimed slot is scanned after every sweep. A `bool` field
124/// would be a condition each scan has to remember to test. An enum whose
125/// non-[`Reference`](DebugSlotKind::Reference) variants are the input to
126/// [`DebugLocalMeta::read`], which answers a
127/// [`DebugValue::Scalar`] that *contains no reference*, is a condition no scan
128/// can fail to test: there is no path from a scalar slot to a `GcRef`.
129///
130/// Every [`praxis_mir::ir::ScalarKind`](https://docs.rs/praxis-mir/latest/praxis_mir/ir/enum.ScalarKind.html)
131/// has a variant here, including `Byte`, which is unwired and which ADR-120's
132/// forwarding therefore cannot reach. The map is total on purpose: a partial map
133/// would have to answer *something* for a kind it did not cover, and the only
134/// available answer is `Reference` — which is precisely the unsound one.
135#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
136pub enum DebugSlotKind {
137    /// The slot holds an `Option<GcRef>`: a reference into the heap, or the
138    /// all-zero `None`. Every local whose box survives compilation has this.
139    Reference,
140    /// `i64` — an `Int` payload whose box ADR-120's forwarding deleted.
141    Int,
142    /// `u8` widened — a `Bool` payload.
143    Bool,
144    /// `f64::to_bits()` — a `Float` payload. The bit pattern the scalar channel
145    /// carries (`ScalarKind::Float`'s doc), not an `f64` register value.
146    Float,
147    /// `u32` widened — a `Char` payload.
148    Char,
149    /// `u8` widened — a `Byte` payload.
150    Byte,
151}
152
153impl DebugSlotKind {
154    /// What generated code adds to a payload before storing it into a debug
155    /// slot, and what [`DebugLocalMeta::read`] subtracts on the way out
156    /// (ADR-121 decision 2).
157    ///
158    /// # The problem this solves
159    ///
160    /// A slot is one word and a claim zeroes its run, so the all-zero word means
161    /// "nothing written here yet". For a [`Reference`](Self::Reference) slot
162    /// that is *exact* — a `GcRef` is `NonNull`. For a scalar slot it cannot be:
163    /// there are 2^64 payloads and 2^64 words, so **some** payload must collide
164    /// with "unwritten", and no encoding avoids it. The only question is which.
165    ///
166    /// Storing the payload raw would make the collision `0` — and therefore
167    /// `false`, and `0.0`. ADR-121 promotes *bindings* into scalar slots, so
168    /// that collision reaches `var i = 0`, which is close to the most common
169    /// line a Praxis program has.
170    ///
171    /// # What each kind gives up instead
172    ///
173    /// The bias is chosen per kind so the collision lands on a value the
174    /// language does not hold, or barely does:
175    ///
176    /// | kind | bias | the one payload that reads `<uninit>` |
177    /// |---|---:|---|
178    /// | [`Reference`](Self::Reference) | 0 | none — `NonNull` has no zero |
179    /// | [`Bool`](Self::Bool) | 1 | **none**: two payloads, biased to 1 and 2 |
180    /// | [`Char`](Self::Char) | 1 | **none**: `0..=0x10FFFF` biased clear of zero |
181    /// | [`Byte`](Self::Byte) | 1 | **none**: `0..=255` biased clear of zero |
182    /// | [`Float`](Self::Float) | 1 | one quiet NaN (`0xFFFF_FFFF_FFFF_FFFF`) |
183    /// | [`Int`](Self::Int) | `i64::MIN` | `i64::MIN` |
184    ///
185    /// Three of the six lose nothing at all, because their payloads do not fill
186    /// the word. `Float` loses one NaN bit pattern out of the 2^52 that are NaN,
187    /// and every one of them prints `NaN` anyway. `Int` genuinely loses a value
188    /// a program could compute — and `i64::MIN` against `0` is the whole trade,
189    /// made in the direction where the losing case is a number nothing reaches
190    /// by accident. `small_int`'s range starts at `-256`, and the sentinel
191    /// idiom that module names is `-1`; neither is anywhere near this.
192    ///
193    /// # Why not a written-marker, which would lose nothing
194    ///
195    /// A parallel byte per slot, zeroed by the claim and set by each store, is
196    /// exact. It is also **a second store per definition** in generated code, on
197    /// the path ADR-120 part 2 already measures at 2.4% of the suite. This is one
198    /// `iadd_imm` against a register that is about to be stored anyway — an ALU
199    /// operation with no memory traffic, which the same measurement cannot see.
200    /// Exactness here is worth an instruction, not a store.
201    #[inline]
202    #[must_use]
203    pub const fn store_bias(self) -> i64 {
204        match self {
205            // Must stay exact: `crash_snapshot` hands this word back as a real
206            // reference, and a biased pointer is not one.
207            DebugSlotKind::Reference => 0,
208            DebugSlotKind::Int => i64::MIN,
209            DebugSlotKind::Bool
210            | DebugSlotKind::Float
211            | DebugSlotKind::Char
212            | DebugSlotKind::Byte => 1,
213        }
214    }
215}
216
217/// One scalar payload read out of a debug slot, decoded under the slot's
218/// [`DebugSlotKind`].
219///
220/// The point of the type is what it does **not** have: no pointer, no `GcRef`,
221/// no descriptor. A consumer holding one of these cannot reach the heap through
222/// it, which is why [`DebugValue`] is safe to hand to the crash-snapshot
223/// walker, the renderer and the collector's post-sweep scan alike.
224#[derive(Clone, Copy, PartialEq, Debug)]
225pub enum ScalarValue {
226    Int(i64),
227    Bool(bool),
228    Float(f64),
229    Char(char),
230    Byte(u8),
231}
232
233/// Render a payload the way the object it came out of would have rendered.
234///
235/// **The text must match**, and that is the requirement rather than a nicety:
236/// ADR-120 part 2 exists so a user cannot tell which temps the optimizer kept a
237/// box for, and a `Float` that printed `3` here and `3.0` through
238/// `crate::scalars::FLOAT`'s `format` would give the answer away. So this lives
239/// beside those callbacks — `write_float` is literally the one `FLOAT.format`
240/// calls (ADR-083's `.0` rule) — rather than in the debugger's renderer, which
241/// is the crate that would have had to guess.
242impl std::fmt::Display for ScalarValue {
243    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244        match *self {
245            ScalarValue::Int(v) => write!(f, "{v}"),
246            ScalarValue::Bool(v) => f.write_str(if v { "true" } else { "false" }),
247            ScalarValue::Float(v) => {
248                crate::scalars::write_float(f, v);
249                Ok(())
250            }
251            ScalarValue::Char(c) => write!(f, "{c}"),
252            ScalarValue::Byte(b) => write!(f, "{b}"),
253        }
254    }
255}
256
257/// What a debug slot holds: a reference the collector and the debugger may
258/// follow, or a raw scalar neither may.
259///
260/// [`DebugLocalMeta::read`] is the only constructor, and it is the only place
261/// in the runtime where a slot word becomes something typed.
262#[derive(Clone, Copy, PartialEq, Debug)]
263pub enum DebugValue {
264    /// A live reference into the heap.
265    Reference(GcRef),
266    /// A payload whose box the compiler elided (ADR-120).
267    Scalar(ScalarValue),
268    /// A reference that **was** here and whose object the collector has since
269    /// reclaimed (ADR-106's weak arm, [`RECLAIMED_WORD`]).
270    ///
271    /// Carries nothing, deliberately: the object is gone, and this variant
272    /// exists to say *that* rather than to say anything about it. It is the
273    /// third thing a slot can be, and it is distinct from "never written": one
274    /// is a line that never ran, the other a line that ran and whose result is
275    /// no longer around to show.
276    ///
277    /// Reaching one is ordinary, not exceptional. ADR-044 decision 2 nulls a
278    /// shadow slot the moment its local dies, so a binding stops being a root at
279    /// its last use rather than at the end of its scope; any collection after
280    /// that point is free to take it, while the debug slot goes on naming it.
281    /// `var b = "asdf"` followed by an allocation big enough to collect is the
282    /// whole recipe.
283    Reclaimed,
284}
285
286impl DebugValue {
287    /// The reference this value names, or `None` if it is a scalar.
288    ///
289    /// The **one** door from a debug value back to the heap, so every consumer
290    /// that roots, traces, poisons-checks or type-recovers a debug value goes
291    /// through a single line that a scalar cannot pass. `crash_snapshot`'s
292    /// `push_roots`, the collector's post-sweep scan and the debugger's
293    /// `p EXPR` bindings are its three callers.
294    #[must_use]
295    pub fn reference(self) -> Option<GcRef> {
296        match self {
297            DebugValue::Reference(r) => Some(r),
298            // A scalar has no reference and a reclaimed slot no longer has one.
299            // The second is what keeps the post-sweep scan idempotent — a slot
300            // it already cleared cannot be poison-checked again, because there
301            // is no pointer left to check — and what keeps `push_roots` from
302            // rooting storage the sweep has handed back.
303            DebugValue::Scalar(_) | DebugValue::Reclaimed => None,
304        }
305    }
306
307    /// Whether this slot still holds something the debugger can render.
308    ///
309    /// [`DebugLocalMeta::read`] answers `Some` for a reclaimed slot — a value
310    /// *was* written — but there is nothing left to show, so a consumer
311    /// branching on that `Option` alone has to say which of the two it means.
312    /// This is the "renderable" half; `fault_span`'s question is the other one,
313    /// and it wants `is_some()`: a temp whose expression finished is not where
314    /// the frame faulted, whatever became of the result.
315    #[must_use]
316    pub fn is_live(self) -> bool {
317        !matches!(self, DebugValue::Reclaimed)
318    }
319
320    /// Read this value as an `Int`, whether it is still boxed or was elided
321    /// into a scalar slot.
322    ///
323    /// # Panics
324    /// If it is neither — the same contract, and the same assertion, as
325    /// [`GcRef::as_int`](crate::GcRef::as_int), which this delegates to for a
326    /// reference. A caller asking a `Vec` for its integer has a bug either way,
327    /// and ADR-120 part 2 does not make that bug quieter.
328    #[must_use]
329    pub fn as_int(&self) -> i64 {
330        match self {
331            DebugValue::Reference(r) => r.as_int(),
332            DebugValue::Scalar(ScalarValue::Int(v)) => *v,
333            DebugValue::Scalar(other) => panic!("not an Int: {other:?}"),
334            DebugValue::Reclaimed => panic!("the object this slot named was reclaimed"),
335        }
336    }
337
338    /// Read this value as a `Vec`'s elements.
339    ///
340    /// # Panics
341    /// If it is a scalar. A `Vec` is never a scalar payload, so this arm is a
342    /// caller bug and not a slot the compiler could have produced: ADR-120
343    /// forwards `Int`, `Bool` and `Float` boxes only.
344    #[must_use]
345    pub fn as_vec(&self) -> &[GcRef] {
346        match self {
347            DebugValue::Reference(r) => r.as_vec(),
348            DebugValue::Scalar(other) => panic!("a scalar is not a Vec: {other:?}"),
349            DebugValue::Reclaimed => panic!("the object this slot named was reclaimed"),
350        }
351    }
352}
353
354/// One local's metadata at frame construction: the source name (ptr + len),
355/// the compiler-assigned symbol id, the local's static type descriptor, the
356/// full static `Type` id, the user-vs-temp classification, the source span, and
357/// what its value slot's word means.
358/// Flattened for FFI.
359#[repr(C)]
360pub struct DebugLocalMeta {
361    pub source_name: *const u8,
362    pub name_len: u32,
363    pub symbol_id: u32,
364    /// The local's static type descriptor (§9.3). The backend embeds the
365    /// `'static TypeDescriptor` resolved from the MIR local's `Type`.
366    pub descriptor: *const crate::TypeDescriptor,
367    /// The full static `Type` id (`praxis_typeck::Type(u32)` handle). Lets the
368    /// debugger reconstruct the exact local type (incl. collection element
369    /// types / record shapes) the runtime `descriptor` alone loses.
370    pub type_id: u32,
371    /// The debugger classification: `LOCAL_KIND_USER` (a binding the programmer
372    /// wrote) or `LOCAL_KIND_TEMP` (a compiler intermediate). The split is
373    /// structural, not a name convention.
374    pub kind: u8,
375    /// The local's source span `[start, end)` (byte offsets into program
376    /// source) for debugger provenance. User locals carry their binding's span;
377    /// temps carry the expression they materialize (rendered as `@ "expr"`).
378    /// `(0, 0)` means "no span" (the return slot, span-less captures).
379    pub span_start: u32,
380    pub span_end: u32,
381    /// The function a **direct** call defines this local from (a `'static`
382    /// embedded string), or null with a zero `callee_name_len` for every local
383    /// that is not one.
384    ///
385    /// It is what places a *caller* frame's line: a frame below the innermost
386    /// is stopped in a call, and the call is the one whose callee is the frame
387    /// above it. Static, because it is a fact about the program — the live
388    /// slots cannot answer it, since a loop leaves an earlier pass's value in
389    /// the call's temp.
390    ///
391    /// Null for a `CallIndirect`: a closure's target is a value, and the name
392    /// this would hold does not exist until the call runs.
393    pub callee_name: *const u8,
394    pub callee_name_len: u32,
395    /// What this local's value slot holds (ADR-120 part 2). [`DebugSlotKind::Reference`]
396    /// for every local whose box the compiler kept; a scalar kind for a temp
397    /// whose box the block-local forwarding deleted and whose payload the
398    /// definition stores raw.
399    ///
400    /// A real `enum`, not the `u8` its neighbours `kind` and `type_id` are,
401    /// because nothing outside Rust ever writes this struct: `#[repr(C)]` is
402    /// here so `crate::crash_snapshot` reads a stable layout, and generated code
403    /// only ever stores the address of the enclosing [`FunctionDebugMeta`]. So
404    /// there is no bit pattern to validate and no "unknown tag" case to decide
405    /// what to do with — which is one fewer place the answer could be
406    /// `Reference` by accident.
407    pub slot_kind: DebugSlotKind,
408}
409
410/// The word [`DebugFrameStackHeader::clear_reclaimed`] writes over a reference
411/// slot whose object the sweep took, and the one word
412/// [`DebugLocalMeta::read`] decodes as [`DebugValue::Reclaimed`].
413///
414/// ### Why a reserved word costs nothing, where the zero word cost a payload
415///
416/// [`DebugSlotKind::store_bias`] documents the trade the *unwritten* state
417/// forces: a scalar slot has 2^64 payloads and 2^64 words, so reserving one word
418/// costs one payload, and the bias only chooses which. Reserving a second word
419/// here looks like the same trade and is not, because this word is only ever
420/// **interpreted** in a [`DebugSlotKind::Reference`] slot — and a reference slot
421/// has far fewer than 2^64 inhabitants. `align_of::<GcHeader>()` is 8 (asserted
422/// in `crate::gc`, and `BLOCK_GRANULE` is that same alignment), so no `GcRef`
423/// can ever be 1. Scalar slots keep every payload they had; `Int` loses only
424/// `i64::MIN`, to the bias, and nothing to this.
425///
426/// `None` — the *other* reserved word — would say "nothing was ever written
427/// here" about a slot whose value the collector had just taken. Those are
428/// different facts about the program: one is a line that never ran, the other a
429/// line that ran and whose result is simply no longer around to show.
430///
431/// ### Not part of the ABI
432///
433/// Generated code only ever **stores** into a debug value slot
434/// (`store_debug_local` is a `str` at a fixed displacement, with no matching
435/// load), so no compiled program can observe this word — a store past the clear
436/// overwrites it, which is exactly the right behaviour if a local is written
437/// again. It is therefore not a `RUNTIME_ABI_VERSION` concern: this word is
438/// written and read by the runtime alone.
439pub const RECLAIMED_WORD: usize = 1;
440
441/// No `GcRef` can collide with [`RECLAIMED_WORD`]: a `GcRef` points at a
442/// `GcHeader`, whose alignment `crate::gc` pins at 8, so its low three bits are
443/// always clear. This is that argument as a compile error.
444const _: () = {
445    assert!(
446        RECLAIMED_WORD != 0,
447        "zero is already `None` — the other state"
448    );
449    assert!(!RECLAIMED_WORD.is_multiple_of(std::mem::align_of::<crate::GcHeader>()));
450};
451
452impl DebugLocalMeta {
453    /// Decode one value slot's word under this local's [`slot_kind`](Self::slot_kind).
454    ///
455    /// **The only place a slot word becomes something typed**, and therefore the
456    /// only place the reference/scalar question is asked. `None` is "nothing has
457    /// been written here yet"; [`DebugValue::Reclaimed`] is the third state, a
458    /// reference slot whose object the collector took after it was written.
459    ///
460    /// ### The zero word, and the one thing a scalar slot cannot say
461    ///
462    /// A claim zeroes its run, so an all-zero word is "no value yet" — which for
463    /// a [`DebugSlotKind::Reference`] slot is *exact*, because a `GcRef` is
464    /// `NonNull` and can never be zero (F18, the niche this module's header
465    /// describes).
466    ///
467    /// For a scalar slot it cannot be exact — 2^64 payloads do not fit in 2^64
468    /// words beside an "unwritten" state — so exactly one payload per kind reads
469    /// back as `<uninit>`. **Which one is [`DebugSlotKind::store_bias`]'s
470    /// choice**: three kinds lose nothing, `Float` loses one NaN, and `Int`
471    /// loses `i64::MIN`. The direction of the error is the safe one — a slot
472    /// under-reports a value it holds and never reports a value it does not.
473    /// `an_int_slot_holding_i64_min_reads_as_uninit_and_zero_does_not` pins both
474    /// halves.
475    ///
476    /// # Safety
477    /// `word` must be the current content of a live value slot belonging to a
478    /// frame whose metadata is this one — that is, `values[i]` paired with
479    /// `locals[i]` of the same [`FunctionDebugMeta`]. Pairing a word with
480    /// another local's metadata is exactly the mistake this function exists to
481    /// make impossible to write by hand, and the two callers
482    /// ([`DebugFrameStackHeader::clear_reclaimed`] and
483    /// `crash_snapshot::copy_stack`) both zip the two arrays.
484    #[must_use]
485    pub unsafe fn read(&self, word: Option<GcRef>) -> Option<DebugValue> {
486        let word = word?;
487        if self.slot_kind == DebugSlotKind::Reference {
488            // The reserved word first, because the whole point of reserving it
489            // is that the reference arm never sees it: a `GcRef` built from
490            // `RECLAIMED_WORD` would be handed to `crash_snapshot`, rooted, and
491            // dereferenced through a descriptor it does not have. The test is
492            // confined to this arm — in a scalar slot the same bits are the
493            // payload `1`, and that slot's object, if it ever had one, is not
494            // this stack's business.
495            if word.as_ptr() as usize == RECLAIMED_WORD {
496                return Some(DebugValue::Reclaimed);
497            }
498            return Some(DebugValue::Reference(word));
499        }
500        // Not a reference: recover the raw bits without ever forming something
501        // dereferenceable from them. `GcRef` is `#[repr(transparent)]` over a
502        // `NonNull`, so this is the address-as-integer read `strict_provenance`
503        // sanctions and not a load through the pointer — then undo the bias
504        // generated code applied on the way in (ADR-121 decision 2). Wrapping,
505        // because the bias is chosen precisely so that one payload wraps to the
506        // all-zero word — and that payload is the one the `word?` above has
507        // already answered `None` for.
508        let bits = (word.as_ptr() as usize as u64).wrapping_sub(self.slot_kind.store_bias() as u64);
509        let scalar = match self.slot_kind {
510            // Unreachable: the branch above returned. Spelled out rather than
511            // `unreachable!()` so this match stays total over the enum and a
512            // new variant is a compile error here.
513            DebugSlotKind::Reference => return Some(DebugValue::Reference(word)),
514            DebugSlotKind::Int => ScalarValue::Int(bits as i64),
515            DebugSlotKind::Bool => ScalarValue::Bool(bits & 1 != 0),
516            DebugSlotKind::Float => ScalarValue::Float(f64::from_bits(bits)),
517            // A `Char` payload is a validated Unicode scalar everywhere the
518            // language can produce one, so `None` here is a compiler bug rather
519            // than a program one — and rendering the slot as `<uninit>` is how
520            // it stays a missing value instead of becoming a wrong character.
521            DebugSlotKind::Char => ScalarValue::Char(char::from_u32(bits as u32)?),
522            DebugSlotKind::Byte => ScalarValue::Byte(bits as u8),
523        };
524        Some(DebugValue::Scalar(scalar))
525    }
526}
527
528/// Everything the crash debugger needs about a function that does **not** vary
529/// per call: its name, its source extent, and the metadata for its `Gc` locals.
530///
531/// One of these exists per lowered function, interned by content in the JIT
532/// generation arena (ADR-043), so a debugger session that recompiles the same
533/// function on every `p EXPR` (DBG-05) pays for it once. A generated prologue
534/// stores its address into a [`DebugFrameEntry`] — one immediate, one store.
535///
536/// `#[repr(C)]` because generated code writes its address and
537/// [`crate::crash_snapshot`] reads its fields across the ABI boundary.
538#[repr(C)]
539pub struct FunctionDebugMeta {
540    /// The function's source name (a `'static` embedded string).
541    pub func_name: *const u8,
542    /// The function name's byte length.
543    pub func_name_len: u32,
544    /// How many `Gc` locals this function has — the length of both `locals` and
545    /// the run of value slots a call of it claims.
546    pub local_count: u32,
547    /// `local_count` entries, in **debug-slot order**: the local's position among
548    /// this function's `Gc` locals, in MIR local order. Entry `i` describes the
549    /// word at displacement `i` of the run a call claims, which is what
550    /// [`crate::crash_snapshot`] and [`DebugFrameStackHeader::clear_reclaimed`]
551    /// rely on when they zip the two.
552    ///
553    /// Debug-slot order is **not** shadow-slot order (ADR-128 decision 3): a
554    /// shadow slot index is a *colour* — `is_prime`'s shadow indices are `{0}`
555    /// while its debug indices are `0..33` — so the two stacks are not
556    /// index-parallel with each other.
557    pub locals: *const DebugLocalMeta,
558    /// The function's source span `[start, end)` as byte offsets into the
559    /// program source (§9.3 "current source span", ADR-035 decision 3). `(0, 0)`
560    /// means "no span recorded" — the `__fnvalue_*` adapter and `__p_expr`,
561    /// whose bodies nobody wrote. A closure's span is its literal's.
562    pub span_start: u32,
563    pub span_end: u32,
564}
565
566/// One live call's debug frame: which function, and where its value slots are.
567///
568/// This is the whole of what a frame is — everything else about the call is
569/// static and lives in [`FunctionDebugMeta`], and the parent frame is the entry
570/// below this one on the stack.
571///
572/// Claimed by bumping the [`DebugFrameStack`]'s `top` in the prologue and
573/// released by restoring the saved base in the epilogue.
574#[repr(C)]
575#[derive(Clone, Copy)]
576pub struct DebugFrameEntry {
577    /// The static metadata for the function this call is executing.
578    pub meta: *const FunctionDebugMeta,
579    /// The base of this call's run of `meta.local_count` value slots inside the
580    /// [`DebugValueStack`].
581    pub values: *mut Option<GcRef>,
582}
583
584impl DebugFrameEntry {
585    /// The byte offsets generated code writes at. Derived from the `#[repr(C)]`
586    /// layout, like every other offset the backend emits (Appendix B), so a
587    /// reorder here is a recompiled constant rather than a silent miscompile.
588    pub const META_OFFSET: i32 = core::mem::offset_of!(Self, meta) as i32;
589    pub const VALUES_OFFSET: i32 = core::mem::offset_of!(Self, values) as i32;
590    /// The stride the frame stack's `top` moves by, per call.
591    pub const SIZE: i64 = core::mem::size_of::<Self>() as i64;
592
593    /// The zero value a fresh reservation holds: no function, no values.
594    ///
595    /// A claimed-but-unwritten entry is not a state generated code can be
596    /// observed in — the prologue's claim and its two stores are straight-line
597    /// with nothing between them — so this exists for [`SlotStack::new`], not as
598    /// a case the snapshot walker handles.
599    #[must_use]
600    pub const fn empty() -> DebugFrameEntry {
601        DebugFrameEntry {
602            meta: std::ptr::null(),
603            values: std::ptr::null_mut(),
604        }
605    }
606}
607
608/// The runtime's one reservation of per-call debug value slots.
609///
610/// `Option<GcRef>` rather than the shadow stack's `*mut GcHeader`, for two
611/// reasons stated in this module's header: the `None` niche means a zeroed claim
612/// *is* a run of "no value yet" (F18), and the distinct type is what keeps
613/// `impl RootSet for SlotStackHeader<*mut GcHeader>` from applying here. The
614/// collector must not **trace** these slots; ADR-044 decision 2 nulls a shadow
615/// slot the moment its local dies, and this stack deliberately does not.
616///
617/// It does scan them, after every sweep, and mark the ones whose object that
618/// sweep reclaimed — see [`DebugFrameStackHeader::clear_reclaimed`] and
619/// ADR-106. That is the difference between keeping a value *alive* and keeping
620/// a slot *valid*, and only the second is this stack's business.
621///
622/// So a slot's word is one of three things, not two: zero (nothing written),
623/// [`RECLAIMED_WORD`] (written, then collected), or a value. `Option<GcRef>`
624/// spells the first and the third; the second is a reserved word inside the
625/// `Some` half, which costs nothing because a reference slot's inhabitants are
626/// aligned pointers.
627pub type DebugValueStack = SlotStack<Option<GcRef>>;
628/// The header generated code bump-allocates value slots against.
629pub type DebugValueStackHeader = SlotStackHeader<Option<GcRef>>;
630/// The runtime's one reservation of per-call frame entries.
631pub type DebugFrameStack = SlotStack<DebugFrameEntry>;
632/// The header generated code bump-allocates frame entries against.
633pub type DebugFrameStackHeader = SlotStackHeader<DebugFrameEntry>;
634
635/// The size of the debug value reservation, in slots.
636///
637/// **Sized by its own headroom term** (ADR-128 decision 3), not written as
638/// `SHADOW_STACK_SLOTS`: root slots are colored by live range and debug value
639/// slots stay dense, one per `Gc` local, so the two index spaces answer
640/// different questions and are bounded by different caps
641/// ([`MAX_SHADOW_SLOTS`](crate::MAX_SHADOW_SLOTS) and
642/// [`MAX_DEBUG_VALUE_SLOTS`]).
643///
644/// Exhaustion is unrepresentable here exactly as it is on the shadow stack, and
645/// for the same reason: every generated prologue rejects
646/// `stack_left < frame_cost(slots)` before it claims anything, and
647/// [`frame_cost`](crate::frame_cost) charges the **dense** count of `Gc` locals
648/// (ADR-128 decision 4) — which is precisely this stack's width. So the claimed
649/// debug value slots of all live frames are bounded by `MAX_LIVE_SLOTS`, the
650/// same `budget / FRAME_BYTES_PER_SLOT + MAX_RECURSION_DEPTH ×
651/// REFERENCE_FRAME_SLOTS` that bounds the shadow stack, and generated code emits
652/// no bounds check because there is nothing left to check.
653///
654/// The headroom term is [`MAX_DEBUG_VALUE_SLOTS`] rather than `MAX_SHADOW_SLOTS`,
655/// for the reason `SHADOW_STACK_SLOTS` keeps one at all: the Rust-side
656/// [`push_frame`] callers spend no budget, so the argument above does not cover
657/// them, and one widest frame of headroom is what covers them instead.
658///
659/// This is a *reservation*, and the weak scan (ADR-106) does not walk it. Its
660/// cost is bounded by `top - base` — the slots live calls have actually claimed
661/// — so raising this number costs address space and not collection time.
662pub const DEBUG_VALUE_STACK_SLOTS: usize = MAX_LIVE_SLOTS + MAX_DEBUG_VALUE_SLOTS;
663
664// The capacity identity for this stack, spelled the same way and for the same
665// reason `SHADOW_STACK_SLOTS`'s is (ADR-128 decision 3: "the assert is not
666// optional"). The hazard is not someone raising the budget — the reservation
667// follows it — it is someone deciding ~5.6 MiB of address space is too much and
668// writing a smaller number. That edit makes debug-value-stack overflow reachable
669// from generated code, silently, because generated code does not check the limit.
670// This fails the *build* instead.
671const _: () = assert!(
672    DEBUG_VALUE_STACK_SLOTS > MAX_LIVE_SLOTS,
673    "the debug value stack must cover every slot the budget can buy, plus one \
674     frame of headroom for Rust-side pushes"
675);
676
677// And the premise that keeps the two stacks' bounds the same arithmetic: a
678// colored root width can never exceed the dense debug width, so the budget
679// charge on the dense count over-covers the shadow stack. If a later change ever
680// made the shadow claim the wider of the two, `SHADOW_STACK_SLOTS` would need its
681// own re-derivation rather than this one.
682const _: () = assert!(
683    MAX_SHADOW_SLOTS <= MAX_DEBUG_VALUE_SLOTS,
684    "a function's root slots are colored from its `Gc` locals, so there cannot \
685     be more of them than there are locals"
686);
687
688// The two reservations differ only in their headroom terms — same budget-derived
689// terms, different headroom (ADR-128 decision 3). A `const` block rather than a
690// test for the reason the capacity identity above is one: this is arithmetic over
691// constants, so a build that disagrees with it should not link.
692const _: () = assert!(
693    DEBUG_VALUE_STACK_SLOTS - crate::SHADOW_STACK_SLOTS == MAX_DEBUG_VALUE_SLOTS - MAX_SHADOW_SLOTS,
694    "the two slot reservations differ by exactly their headroom terms, because \
695     the budget-derived terms are the same arithmetic over the same charge"
696);
697
698/// The size of the debug frame-entry reservation, in slots — one per live call,
699/// bounded by the same depth guard, plus one entry of headroom for the
700/// Rust-side [`push_frame`] callers, who spend no budget and so are not covered
701/// by that guard. This is `SHADOW_STACK_SLOTS`' headroom term counted in
702/// frames, because a frame stack claims one entry per call rather than one per
703/// slot.
704pub const DEBUG_FRAME_STACK_SLOTS: usize = MAX_RECURSION_DEPTH as usize + 1;
705
706// ---------------------------------------------------------------------------
707// The weak arm (ADR-106)
708// ---------------------------------------------------------------------------
709
710impl DebugFrameStackHeader {
711    /// Mark every claimed debug value slot whose object the sweep that just
712    /// finished reclaimed, and answer how many were marked.
713    ///
714    /// This is the entire content of [`RuntimeRoots`](crate::RuntimeRoots)' one
715    /// weak arm. It retains nothing: it runs *after* the mark and the sweep have
716    /// already decided what dies, and its only effect is to replace a reference
717    /// to storage that no longer holds an object with [`RECLAIMED_WORD`], the
718    /// absence that says how it became one.
719    ///
720    /// ### Why `is_poisoned`, and why here
721    ///
722    /// Sweep calls `GcHeader::poison` on each reclaimed block *before* it clears
723    /// that block's `allocated` bit (ADR-039 decision 3), and nothing else in the
724    /// runtime ever nulls a descriptor. So at this instant "poisoned" is exactly
725    /// "reclaimed by this collection or an earlier one", and it is a one-word
726    /// load and a compare against zero.
727    ///
728    /// It is only exactly that *at this instant*. `claim_free_block` hands a
729    /// reclaimed block back to the next allocation, which writes a fresh header
730    /// over the poison — so a slot naming that block stops being distinguishable
731    /// from a slot naming a live object, and the two have different types. That
732    /// is why this cannot be deferred to `praxis_snapshot_debug_chain` or to the
733    /// debugger's render: the window between the sweep and the next allocation is
734    /// the only place the question has an answer. `Heap::collect_inner` calls this
735    /// inside that window.
736    ///
737    /// ### Why the frame entries rather than the value stack's `[base, top)`
738    ///
739    /// A frame entry is what pairs a run of value slots with the `local_count`
740    /// that bounds it, and `crash_snapshot::copy_stack` walks exactly these pairs
741    /// to build a snapshot. Driving the clear from the same walk makes "every
742    /// value a snapshot could copy has been checked" true by construction rather
743    /// than by an argument about the runs partitioning the reservation. The
744    /// `debug_assert` below is that argument, kept as a check: if a prologue ever
745    /// claims value slots without a frame entry to name them, this fires in every
746    /// debug build instead of silently skipping the slots it cannot see.
747    ///
748    /// # Safety
749    /// Every claimed entry's `meta` must point at a live [`FunctionDebugMeta`]
750    /// whose `locals` array has `local_count` entries, and its `values` at that
751    /// many value slots — the same contract `copy_stack` runs under, and the one
752    /// every prologue establishes. `values` must be live for the duration of the
753    /// call: the collector writes through it.
754    ///
755    /// A slot whose [`DebugLocalMeta::slot_kind`] is not
756    /// [`DebugSlotKind::Reference`] is not a reference and is not scanned. The
757    /// exclusion is structural rather than a test this loop performs — see the
758    /// comment at the `continue` — and it is what keeps ADR-120 part 2's scalar
759    /// slots out of the collector's one weak arm entirely.
760    ///
761    /// Reading `r.header()` for a reference into a *reclaimed* block is a read of
762    /// mapped memory, not a use-after-free: a page is unmapped only at teardown,
763    /// after `finalize_all` (`Heap::release_pages`), which is the same premise
764    /// that makes the provenance check in `Heap::mark` a rejection rather than a
765    /// wild read (ADR-103 decision 3).
766    pub(crate) unsafe fn clear_reclaimed(&self, values: &DebugValueStackHeader) -> usize {
767        let mut cleared = 0usize;
768        let mut scanned = 0usize;
769        for entry in self.claimed() {
770            // SAFETY: the caller guarantees a live `meta` on every claimed
771            // entry. `copy_stack` treats null the same way and for the same
772            // reason: a prologue writes both words in straight-line code, so
773            // this is unreachable rather than handled.
774            let Some(meta) = (unsafe { entry.meta.as_ref() }) else {
775                continue;
776            };
777            let count = meta.local_count as usize;
778            scanned += count;
779            for i in 0..count {
780                // SAFETY: the caller guarantees `values` names `local_count`
781                // live slots. This is the same pointer a generated debug store
782                // and `DebugFrameGuard::set` write through, carrying the
783                // reservation's own provenance — not a pointer re-derived from
784                // a shared slice.
785                let slot = unsafe { entry.values.add(i) };
786                // SAFETY: the caller guarantees `locals` names `local_count`
787                // entries, and slot `i` is local `i`'s — the two arrays are
788                // index-parallel by ADR-104's construction.
789                let local = unsafe { &*meta.locals.add(i) };
790                // SAFETY: `local` is slot `i`'s own metadata, which is `read`'s
791                // whole precondition; the slot holds an initialized word (a
792                // claim zeroes its run).
793                let value = unsafe { local.read(*slot) };
794                // **A scalar slot never reaches `header()`**, and it is the type
795                // that says so rather than a test above this line: `read`
796                // answers `DebugValue::Scalar`, which holds no `GcRef`, so
797                // `reference()` is `None` and the poison check is not merely
798                // skipped — it is unreachable. That is the whole of ADR-120
799                // part 2's soundness argument against the failure mode it
800                // creates, which is this scan dereferencing an `f64` bit
801                // pattern as a `GcHeader`.
802                let Some(r) = value.and_then(DebugValue::reference) else {
803                    continue;
804                };
805                if r.header().is_poisoned() {
806                    // SAFETY: as above. Writing this word is correct for a
807                    // reference slot and would be a *lie* in a scalar one — `1`
808                    // is a payload there — which is the second reason the arms
809                    // are separate.
810                    //
811                    // Written as a machine word rather than as an
812                    // `Option<GcRef>`, for `DebugFrameGuard::set_scalar`'s
813                    // reason: no `GcRef` should exist at this address even
814                    // momentarily, and a slot word is dereferenceable only after
815                    // `DebugLocalMeta::read` has said what it is.
816                    unsafe { *slot.cast::<usize>() = RECLAIMED_WORD };
817                    cleared += 1;
818                }
819            }
820        }
821        debug_assert_eq!(
822            scanned,
823            values.len(),
824            "the frame entries' value runs must partition the value stack's \
825             [base, top) — a run of slots no frame entry names is a run this \
826             scan cannot reach, and a stale reference in it would survive the \
827             collection that freed what it points at"
828        );
829        cleared
830    }
831}
832
833impl DebugLocal {
834    /// The source name as a `String`. Allocates; for testing/debugger only.
835    pub fn name(&self) -> String {
836        if self.source_name.is_null() || self.name_len == 0 {
837            return String::new();
838        }
839        // SAFETY: caller (compiler) guarantees valid UTF-8.
840        unsafe {
841            String::from_utf8_lossy(std::slice::from_raw_parts(
842                self.source_name,
843                self.name_len as usize,
844            ))
845            .into_owned()
846        }
847    }
848
849    /// True iff this local is a user-written binding (a `var`/param/
850    /// capture), as opposed to a compiler-generated temporary.
851    pub fn is_user(&self) -> bool {
852        self.kind == LOCAL_KIND_USER
853    }
854
855    /// The local's source span `[start, end)` (byte offsets into program
856    /// source), or `None` if none was threaded. `None` is signalled by the
857    /// `(0, 0)` sentinel (the zero-width span at offset 0 is not a meaningful
858    /// program location for a local that exists).
859    pub fn span(&self) -> Option<(u32, u32)> {
860        let s = (self.span_start, self.span_end);
861        (s != (0, 0)).then_some(s)
862    }
863
864    /// The function a direct call defines this local from, or `None` where the
865    /// local is not a direct call's result.
866    ///
867    /// # Safety
868    /// The pointer is a compiler-embedded `'static` UTF-8 string — the same
869    /// contract [`Self::name`] reads `source_name` under — so the borrow this
870    /// returns outlives any frame it came from.
871    pub unsafe fn callee(&self) -> Option<&'static str> {
872        if self.callee_name.is_null() || self.callee_name_len == 0 {
873            return None;
874        }
875        // SAFETY: the caller guarantees compiler-embedded 'static UTF-8.
876        unsafe {
877            Some(std::str::from_utf8_unchecked(std::slice::from_raw_parts(
878                self.callee_name,
879                self.callee_name_len as usize,
880            )))
881        }
882    }
883}
884
885/// A debug value slot must stay one machine word: generated code stores into
886/// it at a fixed offset with a single `str` — a `GcRef` for a reference slot,
887/// a raw payload for a scalar one (ADR-120 part 2) — and reads the zeroed slot
888/// a fresh claim starts with as "no value yet". `Option<GcRef>` is
889/// niche-optimized to exactly that (F18); this is the compile-time proof, and
890/// the second assertion is also what makes `DebugFrameGuard::set_scalar`'s
891/// `*mut u64` write in-bounds and correctly aligned.
892const _: () = {
893    assert!(std::mem::size_of::<Option<GcRef>>() == std::mem::size_of::<GcRef>());
894    assert!(std::mem::size_of::<Option<GcRef>>() == std::mem::size_of::<u64>());
895    assert!(std::mem::align_of::<Option<GcRef>>() == std::mem::align_of::<u64>());
896};
897
898// ---------------------------------------------------------------------------
899// The Rust-side push. Generated code does this inline; this is for the
900// runtime's own tests and for any host that wants a debug frame the way a
901// prologue makes one.
902// ---------------------------------------------------------------------------
903
904/// A debug frame claimed from Rust, released when dropped.
905///
906/// Mirrors [`crate::shadow_stack::ShadowFrameGuard`], and for the same reason:
907/// the two stacks must be popped together and in the reverse of the order they
908/// were pushed, and an RAII guard is what makes "pop one and not the other"
909/// unrepresentable from Rust.
910pub struct DebugFrameGuard {
911    frames: *mut DebugFrameStackHeader,
912    values: *mut DebugValueStackHeader,
913    /// This frame's entry, and the frame-stack `top` the drop restores.
914    frame_base: *mut DebugFrameEntry,
915    /// This frame's first value slot, and the value-stack `top` the drop
916    /// restores.
917    value_base: *mut Option<GcRef>,
918    count: u32,
919}
920
921impl DebugFrameGuard {
922    /// Record `r` as the current value of local `index`.
923    ///
924    /// # Panics
925    /// If `index` is outside the frame. Writing another frame's slot would make
926    /// the *other* frame render a value it never held, which is not a condition
927    /// the caller could detect afterwards.
928    pub fn set(&mut self, index: usize, r: GcRef) {
929        assert!(
930            index < self.count as usize,
931            "debug slot {index} is outside a {}-local frame",
932            self.count
933        );
934        // SAFETY: `index` is inside the run claimed by `push_frame`, which is
935        // live until this guard drops.
936        unsafe { *self.value_base.add(index) = Some(r) };
937    }
938
939    /// Record the raw word `bits` as the current value of local `index` — the
940    /// Rust-side equivalent of the store a definition of an elided box's scalar
941    /// emits (ADR-120 part 2).
942    ///
943    /// Deliberately *not* typed as a [`ScalarValue`]: generated code writes one
944    /// machine word and knows nothing about what it means, and a test that
945    /// could only write a well-formed payload could not reproduce the state the
946    /// collector has to survive — a slot whose word is an `f64` bit pattern
947    /// that happens to be a plausible heap address.
948    ///
949    /// # Panics
950    /// If `index` is outside the frame, for [`DebugFrameGuard::set`]'s reason.
951    pub fn set_scalar(&mut self, index: usize, bits: u64) {
952        assert!(
953            index < self.count as usize,
954            "debug slot {index} is outside a {}-local frame",
955            self.count
956        );
957        // SAFETY: `index` is inside the run claimed by `push_frame`, which is
958        // live until this guard drops. Written as a machine word through a
959        // `*mut u64` rather than as an `Option<GcRef>`, because that is what a
960        // generated `str` does and because no `GcRef` should exist here even
961        // momentarily: a slot word is dereferenceable only after
962        // `DebugLocalMeta::read` says its `slot_kind` is `Reference`. The two
963        // types have the same size and alignment (the `const _` below), and
964        // every bit pattern is a valid `Option<GcRef>` — `NonNull`'s only
965        // validity invariant is non-nullness — so the write is in-bounds and
966        // leaves the slot initialized either way.
967        unsafe {
968            *self.value_base.add(index).cast::<u64>() = bits;
969        }
970    }
971
972    /// Record `payload` as local `index`'s value **the way generated code does**
973    /// — biased by its slot kind (ADR-121 decision 2).
974    ///
975    /// [`set_scalar`](Self::set_scalar)'s counterpart, and the two exist for the
976    /// two different questions a test can ask. That one writes a machine word
977    /// and is what an adversarial state needs (a payload that *is* a plausible
978    /// heap address). This one writes a *payload* and is what a round-trip needs:
979    /// with the bias in place, a test that stores a raw word and expects
980    /// [`DebugLocalMeta::read`] to answer it back is asserting that the encoding
981    /// does not exist.
982    pub fn set_scalar_payload(&mut self, index: usize, kind: DebugSlotKind, payload: u64) {
983        self.set_scalar(index, payload.wrapping_add(kind.store_bias() as u64));
984    }
985
986    /// This frame's value slots, as the crash snapshot reads them.
987    #[must_use]
988    pub fn values(&self) -> &[Option<GcRef>] {
989        // SAFETY: the run is live until this guard drops.
990        unsafe { std::slice::from_raw_parts(self.value_base, self.count as usize) }
991    }
992}
993
994impl Drop for DebugFrameGuard {
995    fn drop(&mut self) {
996        // SAFETY: both headers were non-null when the guard was made, and
997        // belong to a runtime the caller guaranteed outlives it.
998        unsafe {
999            (*self.frames).restore(self.frame_base);
1000            (*self.values).restore(self.value_base);
1001        }
1002    }
1003}
1004
1005/// Claim a debug frame for `meta` on `ctx`'s debug stacks, the way a generated
1006/// prologue does: one frame entry, and `meta.local_count` value slots that start
1007/// as `None`.
1008///
1009/// # Safety
1010/// `ctx` must point at a live context wired by
1011/// [`Runtime::context`](crate::Runtime::context); `meta` must point at a
1012/// `FunctionDebugMeta` (with a `locals` array of `local_count` entries) valid
1013/// for at least as long as the returned guard; and the runtime that owns the
1014/// stacks must outlive the guard.
1015///
1016/// # Panics
1017/// If `ctx` is null, either header is null, or `meta` is null.
1018#[must_use]
1019pub unsafe fn push_frame(
1020    ctx: *mut RuntimeContext,
1021    meta: *const FunctionDebugMeta,
1022) -> DebugFrameGuard {
1023    assert!(!ctx.is_null(), "push_frame needs a wired context");
1024    assert!(!meta.is_null(), "a debug frame is a function's metadata");
1025    // SAFETY: the caller guarantees `ctx` and `meta` are live.
1026    let (frames, values, count) = unsafe {
1027        (
1028            (*ctx).debug_frames,
1029            (*ctx).debug_values,
1030            (*meta).local_count,
1031        )
1032    };
1033    assert!(
1034        !frames.is_null() && !values.is_null(),
1035        "push_frame needs a context from `Runtime::context`, not a placeholder"
1036    );
1037    // SAFETY: both headers are non-null and owned by live `SlotStack`s, and
1038    // `claim` checks each reservation's limit itself.
1039    unsafe {
1040        let value_base = (*values).claim(count as usize, None);
1041        let frame_base = (*frames).claim(1, DebugFrameEntry::empty());
1042        *frame_base = DebugFrameEntry {
1043            meta,
1044            values: value_base,
1045        };
1046        DebugFrameGuard {
1047            frames,
1048            values,
1049            frame_base,
1050            value_base,
1051            count,
1052        }
1053    }
1054}
1055#[cfg(test)]
1056mod tests {
1057    use super::*;
1058    use crate::context::Runtime;
1059
1060    /// A context wired to a runtime, kept alive alongside it.
1061    struct Fixture {
1062        rt: Runtime,
1063        ctx: Box<RuntimeContext>,
1064    }
1065
1066    impl Fixture {
1067        fn new() -> Fixture {
1068            let mut rt = Runtime::new();
1069            let ctx = Box::new(rt.context());
1070            Fixture { rt, ctx }
1071        }
1072
1073        fn ctx_ptr(&mut self) -> *mut RuntimeContext {
1074            &mut *self.ctx
1075        }
1076    }
1077
1078    /// Two shadowed `a` bindings, as `var a = ...; var a = ...` produces.
1079    fn shadowed_a_metas() -> [DebugLocalMeta; 2] {
1080        let name_a = b"a";
1081        [
1082            DebugLocalMeta {
1083                callee_name: std::ptr::null(),
1084                callee_name_len: 0,
1085                source_name: name_a.as_ptr(),
1086                name_len: 1,
1087                symbol_id: 10,
1088                descriptor: &crate::scalars::INT,
1089                type_id: 1,
1090                kind: LOCAL_KIND_USER,
1091                span_start: 5,
1092                span_end: 6,
1093                slot_kind: crate::debug::DebugSlotKind::Reference,
1094            },
1095            DebugLocalMeta {
1096                callee_name: std::ptr::null(),
1097                callee_name_len: 0,
1098                source_name: name_a.as_ptr(),
1099                name_len: 1,
1100                symbol_id: 20,
1101                descriptor: &crate::scalars::INT,
1102                type_id: 1,
1103                kind: LOCAL_KIND_USER,
1104                span_start: 20,
1105                span_end: 21,
1106                slot_kind: crate::debug::DebugSlotKind::Reference,
1107            },
1108        ]
1109    }
1110
1111    fn meta_for(
1112        name: &'static [u8],
1113        locals: &[DebugLocalMeta],
1114        span: (u32, u32),
1115    ) -> FunctionDebugMeta {
1116        FunctionDebugMeta {
1117            func_name: name.as_ptr(),
1118            func_name_len: name.len() as u32,
1119            local_count: locals.len() as u32,
1120            locals: locals.as_ptr(),
1121            span_start: span.0,
1122            span_end: span.1,
1123        }
1124    }
1125
1126    /// §4.2's guarantee (ADR-021): shadowed locals are distinguishable in
1127    /// debugger frames by source name and symbol ID.
1128    #[test]
1129    fn a_functions_metadata_distinguishes_shadowed_bindings() {
1130        let metas = shadowed_a_metas();
1131        let meta = meta_for(b"f", &metas, (0, 12));
1132        let mut f = Fixture::new();
1133        let ctx = f.ctx_ptr();
1134        // SAFETY: `ctx` is wired to `f.rt`, and `meta`/`metas` outlive the guard.
1135        let guard = unsafe { push_frame(ctx, &meta) };
1136
1137        // SAFETY: the entry the guard just wrote is the one claimed slot.
1138        let entries = unsafe { (*(*ctx).debug_frames).claimed() };
1139        assert_eq!(entries.len(), 1, "one frame is on the stack");
1140        // SAFETY: the entry names the `meta` this test owns.
1141        let seen = unsafe { &*entries[0].meta };
1142        assert_eq!(seen.local_count, 2);
1143        // SAFETY: `locals` is the `metas` array above.
1144        let locals = unsafe { std::slice::from_raw_parts(seen.locals, 2) };
1145        // Both named "a" but distinct symbol ids — the §4.2 guarantee.
1146        assert_eq!(locals[0].symbol_id, 10);
1147        assert_eq!(locals[1].symbol_id, 20);
1148        assert_ne!(locals[0].symbol_id, locals[1].symbol_id);
1149        assert!(std::ptr::eq(locals[0].descriptor, &crate::scalars::INT));
1150        assert_eq!(locals[0].type_id, 1);
1151        assert_eq!(locals[0].kind, LOCAL_KIND_USER);
1152        assert_eq!((locals[1].span_start, locals[1].span_end), (20, 21));
1153        // The span is the function's, and it is static: nothing writes it at
1154        // runtime.
1155        assert_eq!((seen.span_start, seen.span_end), (0, 12));
1156        drop(guard);
1157    }
1158
1159    #[test]
1160    fn a_frames_values_start_empty_and_hold_what_is_written() {
1161        // The claim is zeroed, and a zeroed `Option<GcRef>` *is* `None` (F18) —
1162        // which is what lets a local that has not been assigned yet render as
1163        // `<uninit>` without a sentinel to compare against.
1164        let metas = shadowed_a_metas();
1165        let meta = meta_for(b"f", &metas, (0, 0));
1166        let mut f = Fixture::new();
1167        let value =
1168            f.rt.heap()
1169                .alloc_unpaced(crate::scalars::INT_PAYLOAD, 42_i64);
1170        let ctx = f.ctx_ptr();
1171        // SAFETY: `ctx` is wired to `f.rt`, and `meta`/`metas` outlive the guard.
1172        let mut guard = unsafe { push_frame(ctx, &meta) };
1173        assert_eq!(guard.values(), &[None, None]);
1174        guard.set(1, value);
1175        assert_eq!(guard.values()[0], None);
1176        assert_eq!(guard.values()[1].map(|r| r.as_int()), Some(42));
1177        drop(guard);
1178    }
1179
1180    #[test]
1181    fn pushing_and_popping_restores_both_tops() {
1182        // The balance property: pushes and pops match on both stacks.
1183        // `m10ws2_debug_frame_pushpop_balanced_across_recursion` is its
1184        // end-to-end form, and `Runtime::clear_for_rerun` asserts on it.
1185        let metas = shadowed_a_metas();
1186        let outer_meta = meta_for(b"outer", &metas, (0, 0));
1187        let inner_meta = meta_for(b"inner", &metas[..1], (0, 0));
1188        let mut f = Fixture::new();
1189        let ctx = f.ctx_ptr();
1190        // SAFETY: `ctx` is wired to `f.rt`; both metas outlive both guards.
1191        unsafe {
1192            let outer = push_frame(ctx, &outer_meta);
1193            assert_eq!(f.rt.debug_frame_stack().len(), 1);
1194            assert_eq!(f.rt.debug_value_stack().len(), 2);
1195            {
1196                let inner = push_frame(ctx, &inner_meta);
1197                assert_eq!(f.rt.debug_frame_stack().len(), 2);
1198                assert_eq!(
1199                    f.rt.debug_value_stack().len(),
1200                    3,
1201                    "the inner frame's one local sits above the outer frame's two"
1202                );
1203                drop(inner);
1204            }
1205            assert_eq!(f.rt.debug_frame_stack().len(), 1);
1206            assert_eq!(f.rt.debug_value_stack().len(), 2);
1207            drop(outer);
1208        }
1209        assert!(f.rt.debug_frame_stack().is_empty());
1210        assert!(f.rt.debug_value_stack().is_empty());
1211    }
1212
1213    /// ADR-106's invariant. A value the shadow stack has stopped naming — which
1214    /// is every `Gc` local after its last use, by ADR-044 decision 2 — is
1215    /// unreachable while the debugger still names it. The collection that
1216    /// reclaims it must leave the debug slot as an absence, not as a reference
1217    /// into storage the allocator is now free to hand out.
1218    ///
1219    /// The `9_999` is past the interned small-`Int` range on purpose: an
1220    /// interned `Int` is an immortal that no sweep touches, so a value inside
1221    /// that range would pass this test by never dying.
1222    #[test]
1223    fn a_weak_slot_whose_object_died_becomes_an_absence() {
1224        let metas = shadowed_a_metas();
1225        let meta = meta_for(b"f", &metas, (0, 0));
1226        let mut f = Fixture::new();
1227        let value =
1228            f.rt.heap()
1229                .alloc_unpaced(crate::scalars::INT_PAYLOAD, 9_999_i64);
1230        let before = f.rt.heap().stats().live_count;
1231        let ctx = f.ctx_ptr();
1232        // SAFETY: `ctx` is wired to `f.rt`, and `meta`/`metas` outlive the guard.
1233        let mut guard = unsafe { push_frame(ctx, &meta) };
1234        // Slot 1 names the value; nothing roots it. This is the state
1235        // `RootSlots::dead` produces at a local's last use.
1236        guard.set(1, value);
1237        assert_eq!(guard.values()[1].map(|r| r.as_int()), Some(9_999));
1238
1239        f.rt.collect_now();
1240
1241        assert_eq!(
1242            f.rt.heap().stats().live_count,
1243            before - 1,
1244            "the weak arm must not have retained it — that is the merge ADR-044 \
1245             refuses, and it would make this test pass for the wrong reason"
1246        );
1247        // SAFETY: slot `i` decoded under local `i`'s own metadata, which is
1248        // `read`'s whole precondition.
1249        let (written, unwritten) = unsafe {
1250            (
1251                metas[1].read(guard.values()[1]),
1252                metas[0].read(guard.values()[0]),
1253            )
1254        };
1255        assert_eq!(
1256            written,
1257            Some(DebugValue::Reclaimed),
1258            "the debug slot still names swept storage; the next allocation \
1259             reissues that block and the slot then names an object of another \
1260             type"
1261        );
1262        assert_eq!(
1263            written.and_then(DebugValue::reference),
1264            None,
1265            "and nothing can follow it back into the heap"
1266        );
1267        // The contrast this test exists to draw: slot 1 was written and
1268        // collected, slot 0 was never written. A debugger reading them apart is
1269        // the difference between `<collected>` and `<uninit>` on a locals line.
1270        assert_eq!(unwritten, None, "slot 0 was never written");
1271        assert_ne!(written, unwritten, "the two absences are not one absence");
1272        drop(guard);
1273    }
1274
1275    /// The other half: the scan is a clear, not a sweep of its own. A value some
1276    /// *strong* arm still roots survives the collection, so its debug slot is
1277    /// untouched and reads back.
1278    ///
1279    /// Without this, nulling every claimed slot unconditionally would pass the
1280    /// test above and destroy the debugger.
1281    #[test]
1282    fn a_weak_slot_whose_object_is_still_rooted_is_untouched() {
1283        let metas = shadowed_a_metas();
1284        let meta = meta_for(b"f", &metas, (0, 0));
1285        let mut f = Fixture::new();
1286        let value =
1287            f.rt.heap()
1288                .alloc_unpaced(crate::scalars::INT_PAYLOAD, 9_999_i64);
1289        let ctx = f.ctx_ptr();
1290        // SAFETY: `ctx` is wired to `f.rt`; the shadow frame and the debug frame
1291        // are both released before the runtime drops.
1292        let (mut shadow, mut guard) = unsafe {
1293            (
1294                crate::shadow_stack::push_frame(ctx, crate::SlotCount::new(1).unwrap()),
1295                push_frame(ctx, &meta),
1296            )
1297        };
1298        shadow.set(0, value);
1299        guard.set(1, value);
1300
1301        f.rt.collect_now();
1302
1303        assert_eq!(
1304            guard.values()[1].map(|r| r.as_int()),
1305            Some(9_999),
1306            "the weak scan nulled a slot whose object the shadow stack roots"
1307        );
1308        drop(guard);
1309        drop(shadow);
1310    }
1311
1312    /// A pair of metadata whose **second** slot is a scalar (ADR-120 part 2),
1313    /// so a test can put one of each side by side in one frame and check that
1314    /// the collector treats them differently.
1315    fn one_reference_and_one_scalar(kind: DebugSlotKind) -> [DebugLocalMeta; 2] {
1316        let mut metas = shadowed_a_metas();
1317        metas[1].slot_kind = kind;
1318        metas
1319    }
1320
1321    /// **The whole of ADR-120 part 2's soundness argument, as a test.** A scalar
1322    /// slot must not enter the collector's post-sweep scan.
1323    ///
1324    /// The word written is the *address of an object this collection reclaims*,
1325    /// which is the adversarial case rather than a plausible one: had the slot
1326    /// been marked `Reference` the scan would have found its header poisoned
1327    /// and nulled it, so "the word is unchanged" is a deterministic statement
1328    /// about the discrimination working and not about a bit pattern happening
1329    /// not to look like a pointer. A payload that *is* a heap address is
1330    /// exactly what an `f64` or a large `Int` can be.
1331    ///
1332    /// `a_weak_slot_whose_object_died_becomes_an_absence` is the same program
1333    /// with the same word in a `Reference` slot, and it asserts the opposite —
1334    /// so the two together say the `slot_kind` is what decides, and nothing
1335    /// else is.
1336    #[test]
1337    fn a_scalar_slot_is_not_scanned_even_when_its_word_names_reclaimed_storage() {
1338        let metas = one_reference_and_one_scalar(DebugSlotKind::Int);
1339        let meta = meta_for(b"f", &metas, (0, 0));
1340        let mut f = Fixture::new();
1341        // Past ADR-100's intern range, so the sweep really does reclaim it.
1342        let doomed =
1343            f.rt.heap()
1344                .alloc_unpaced(crate::scalars::INT_PAYLOAD, 9_999_i64);
1345        let bits = doomed.as_ptr() as usize as u64;
1346        let ctx = f.ctx_ptr();
1347        // SAFETY: `ctx` is wired to `f.rt`, and `meta`/`metas` outlive the guard.
1348        let mut guard = unsafe { push_frame(ctx, &meta) };
1349        guard.set_scalar(1, bits);
1350
1351        f.rt.collect_now();
1352
1353        // **The claim is about the word**, asserted on the word itself rather
1354        // than through `read`: the encoding is biased (ADR-121 decision 2), so a
1355        // decode here would compare `bits` against `bits - i64::MIN` and the
1356        // test would be about the bias rather than about the scan.
1357        assert_eq!(
1358            guard.values()[1].map(|v| v.as_ptr() as usize as u64),
1359            Some(bits),
1360            "the scan nulled a scalar slot, which means it read the word as a \
1361             reference and dereferenced its header"
1362        );
1363        // SAFETY: slot 1 is local 1's, which is the pairing `read` requires.
1364        let seen = unsafe { metas[1].read(guard.values()[1]) };
1365        assert_eq!(
1366            seen.and_then(DebugValue::reference),
1367            None,
1368            "and a scalar can never be handed back as something to follow"
1369        );
1370        drop(guard);
1371    }
1372
1373    /// [`RECLAIMED_WORD`] is reserved in a `Reference` slot and **only** there:
1374    /// the same word in a scalar slot is that slot's payload, and reads back as
1375    /// one.
1376    ///
1377    /// This is the property that makes the reserved word free where the
1378    /// `store_bias` trade is not. The adversarial case is the cheapest one to
1379    /// reach: `Byte`'s bias is 1, so the payload `0` — the most ordinary byte
1380    /// there is — stores *exactly* this word. A decode that tested for the
1381    /// sentinel before consulting the `slot_kind` would answer `Reclaimed` for
1382    /// `0u8`, and would do it for `false` and for `'\0'` too, all three biased
1383    /// the same way.
1384    #[test]
1385    fn the_reclaimed_word_is_a_payload_in_a_scalar_slot() {
1386        let metas = one_reference_and_one_scalar(DebugSlotKind::Byte);
1387        let meta = meta_for(b"f", &metas, (0, 0));
1388        let mut f = Fixture::new();
1389        let ctx = f.ctx_ptr();
1390        // SAFETY: `ctx` is wired to `f.rt`, and `meta`/`metas` outlive the guard.
1391        let mut guard = unsafe { push_frame(ctx, &meta) };
1392        guard.set_scalar_payload(1, DebugSlotKind::Byte, 0);
1393        assert_eq!(
1394            guard.values()[1].map(|v| v.as_ptr() as usize),
1395            Some(RECLAIMED_WORD),
1396            "the premise: this payload really does store the reserved word"
1397        );
1398
1399        // SAFETY: slot 1 is local 1's, which is the pairing `read` requires.
1400        let seen = unsafe { metas[1].read(guard.values()[1]) };
1401        assert_eq!(
1402            seen,
1403            Some(DebugValue::Scalar(ScalarValue::Byte(0))),
1404            "a scalar slot has no reserved words — its `slot_kind` decides \
1405             before the word does"
1406        );
1407
1408        // And a collection does not change that: the scan never reaches a
1409        // scalar slot, so nothing here can turn into the other reading.
1410        f.rt.collect_now();
1411        // SAFETY: as above.
1412        assert_eq!(
1413            unsafe { metas[1].read(guard.values()[1]) },
1414            Some(DebugValue::Scalar(ScalarValue::Byte(0))),
1415        );
1416        drop(guard);
1417    }
1418
1419    /// The control: the *same* word in a `Reference` slot is cleared. Without
1420    /// it, a scan that had quietly stopped clearing anything at all would pass
1421    /// the test above.
1422    #[test]
1423    fn the_same_word_in_a_reference_slot_is_still_cleared_by_the_scan() {
1424        let metas = shadowed_a_metas();
1425        let meta = meta_for(b"f", &metas, (0, 0));
1426        let mut f = Fixture::new();
1427        let doomed =
1428            f.rt.heap()
1429                .alloc_unpaced(crate::scalars::INT_PAYLOAD, 9_999_i64);
1430        let bits = doomed.as_ptr() as usize as u64;
1431        let ctx = f.ctx_ptr();
1432        // SAFETY: as above.
1433        let mut guard = unsafe { push_frame(ctx, &meta) };
1434        guard.set_scalar(1, bits);
1435
1436        f.rt.collect_now();
1437
1438        // SAFETY: slot 1 decoded under local 1's own metadata.
1439        assert_eq!(
1440            unsafe { metas[1].read(guard.values()[1]) },
1441            Some(DebugValue::Reclaimed),
1442            "a reference slot is scanned"
1443        );
1444        drop(guard);
1445    }
1446
1447    /// Every scalar kind round-trips through the slot's one machine word, and
1448    /// `Float` is the one that matters: the scalar channel carries
1449    /// `f64::to_bits()` (`ScalarKind::Float`'s own doc), so a decode that
1450    /// forgot `from_bits` would render `4614256656552045848` for `3.14`.
1451    #[test]
1452    fn each_scalar_kind_decodes_the_word_its_channel_carries() {
1453        let cases: [(DebugSlotKind, u64, ScalarValue); 5] = [
1454            (DebugSlotKind::Int, -7_i64 as u64, ScalarValue::Int(-7)),
1455            (DebugSlotKind::Bool, 1, ScalarValue::Bool(true)),
1456            (
1457                DebugSlotKind::Float,
1458                std::f64::consts::PI.to_bits(),
1459                ScalarValue::Float(std::f64::consts::PI),
1460            ),
1461            (
1462                DebugSlotKind::Char,
1463                u32::from('q') as u64,
1464                ScalarValue::Char('q'),
1465            ),
1466            (DebugSlotKind::Byte, 200, ScalarValue::Byte(200)),
1467        ];
1468        for (kind, bits, expected) in cases {
1469            let metas = one_reference_and_one_scalar(kind);
1470            let meta = meta_for(b"f", &metas, (0, 0));
1471            let mut f = Fixture::new();
1472            let ctx = f.ctx_ptr();
1473            // SAFETY: `ctx` is wired to `f.rt`, and `meta`/`metas` outlive the guard.
1474            let mut guard = unsafe { push_frame(ctx, &meta) };
1475            guard.set_scalar_payload(1, kind, bits);
1476            // SAFETY: slot 1 is local 1's.
1477            let seen = unsafe { metas[1].read(guard.values()[1]) };
1478            assert_eq!(seen, Some(DebugValue::Scalar(expected)), "{kind:?}");
1479            drop(guard);
1480        }
1481    }
1482
1483    /// **The one thing an `Int` slot cannot say**, pinned so a later change
1484    /// moves it deliberately rather than discovers it.
1485    ///
1486    /// A claim zeroes its run and zero means "nothing written here yet", which
1487    /// is exact for a `Reference` slot (a `GcRef` is `NonNull`) and cannot be
1488    /// exact for a scalar one: 2^64 payloads do not fit in 2^64 words beside an
1489    /// "unwritten" state. Exactly one payload per kind is therefore lost, and
1490    /// the bias chooses which — for `Int`, `i64::MIN` (ADR-121 decision 2).
1491    ///
1492    /// Both halves are asserted here, because the test is worth nothing without
1493    /// the second: a bias that lost *both* would pass the first line.
1494    #[test]
1495    fn an_int_slot_holding_i64_min_reads_as_uninit_and_zero_does_not() {
1496        let metas = one_reference_and_one_scalar(DebugSlotKind::Int);
1497        let meta = meta_for(b"f", &metas, (0, 0));
1498        let mut f = Fixture::new();
1499        let ctx = f.ctx_ptr();
1500        // SAFETY: `ctx` is wired to `f.rt`, and `meta`/`metas` outlive the guard.
1501        let mut guard = unsafe { push_frame(ctx, &meta) };
1502
1503        guard.set_scalar_payload(1, DebugSlotKind::Int, i64::MIN as u64);
1504        // SAFETY: slot 1 is local 1's.
1505        assert_eq!(
1506            unsafe { metas[1].read(guard.values()[1]) },
1507            None,
1508            "`i64::MIN` is the payload the bias spends: it and an unwritten \
1509             slot are the same word, and the honest answer for the pair is the \
1510             absence"
1511        );
1512
1513        guard.set_scalar_payload(1, DebugSlotKind::Int, 0);
1514        // SAFETY: slot 1 is local 1's.
1515        assert_eq!(
1516            unsafe { metas[1].read(guard.values()[1]) },
1517            Some(DebugValue::Scalar(ScalarValue::Int(0))),
1518            "and `0` — the payload `var i = 0` holds — round-trips, which is \
1519             the whole of what decision 2 bought"
1520        );
1521        drop(guard);
1522    }
1523
1524    /// The three kinds whose payloads do not fill the word lose **nothing**, and
1525    /// that is a stronger claim than "the collision moved".
1526    ///
1527    /// `Bool` has two payloads, `Byte` 256 and `Char` rather more; biased by one
1528    /// none of them can reach the all-zero word, so every value of all three
1529    /// round-trips. Without this the table in [`DebugSlotKind::store_bias`] is a
1530    /// claim nothing checks — and the case that matters is `false`, which is as
1531    /// common in a crash snapshot as `0` is.
1532    #[test]
1533    fn the_bounded_scalar_kinds_lose_no_payload_at_all() {
1534        let cases: [(DebugSlotKind, u64, ScalarValue); 3] = [
1535            (DebugSlotKind::Bool, 0, ScalarValue::Bool(false)),
1536            (DebugSlotKind::Byte, 0, ScalarValue::Byte(0)),
1537            (DebugSlotKind::Char, 0, ScalarValue::Char('\0')),
1538        ];
1539        for (kind, payload, expected) in cases {
1540            let metas = one_reference_and_one_scalar(kind);
1541            let meta = meta_for(b"f", &metas, (0, 0));
1542            let mut f = Fixture::new();
1543            let ctx = f.ctx_ptr();
1544            // SAFETY: `ctx` is wired to `f.rt`, and `meta`/`metas` outlive the guard.
1545            let mut guard = unsafe { push_frame(ctx, &meta) };
1546            guard.set_scalar_payload(1, kind, payload);
1547            // SAFETY: slot 1 is local 1's.
1548            assert_eq!(
1549                unsafe { metas[1].read(guard.values()[1]) },
1550                Some(DebugValue::Scalar(expected)),
1551                "{kind:?}'s zero payload is not the unwritten word"
1552            );
1553            drop(guard);
1554        }
1555    }
1556
1557    /// `0.0` round-trips and the NaN the bias spends does not.
1558    ///
1559    /// `Float` is the kind that *does* fill the word, so it loses one pattern
1560    /// like `Int` — but the pattern it loses is a quiet NaN, and every NaN
1561    /// renders `NaN` anyway. The pair is asserted for
1562    /// `an_int_slot_holding_i64_min_reads_as_uninit_and_zero_does_not`'s reason.
1563    #[test]
1564    fn a_float_slot_keeps_zero_and_spends_one_nan() {
1565        let metas = one_reference_and_one_scalar(DebugSlotKind::Float);
1566        let meta = meta_for(b"f", &metas, (0, 0));
1567        let mut f = Fixture::new();
1568        let ctx = f.ctx_ptr();
1569        // SAFETY: `ctx` is wired to `f.rt`, and `meta`/`metas` outlive the guard.
1570        let mut guard = unsafe { push_frame(ctx, &meta) };
1571
1572        guard.set_scalar_payload(1, DebugSlotKind::Float, 0.0_f64.to_bits());
1573        // SAFETY: slot 1 is local 1's.
1574        assert_eq!(
1575            unsafe { metas[1].read(guard.values()[1]) },
1576            Some(DebugValue::Scalar(ScalarValue::Float(0.0))),
1577            "`0.0` round-trips"
1578        );
1579
1580        let spent = u64::MAX;
1581        assert!(f64::from_bits(spent).is_nan(), "and what it costs is a NaN");
1582        guard.set_scalar_payload(1, DebugSlotKind::Float, spent);
1583        // SAFETY: slot 1 is local 1's.
1584        assert_eq!(
1585            unsafe { metas[1].read(guard.values()[1]) },
1586            None,
1587            "which is the one pattern the bias spends"
1588        );
1589        drop(guard);
1590    }
1591
1592    /// A scalar renders as the *object would have*, which is the requirement
1593    /// that keeps ADR-120's elision invisible: a user must not be able to tell
1594    /// which temps kept a box by looking at the value column.
1595    #[test]
1596    fn a_scalar_renders_the_text_its_descriptor_would_have_written() {
1597        let mut out = String::new();
1598        // SAFETY: the payload is a real `FloatPayload` on this stack frame.
1599        let f = 3.0_f64;
1600        unsafe {
1601            (crate::scalars::FLOAT.format)(
1602                std::ptr::addr_of!(f) as *const u8,
1603                &mut crate::FormatSink::display(&mut out),
1604            );
1605        }
1606        assert_eq!(out, ScalarValue::Float(3.0).to_string(), "ADR-083's `.0`");
1607        let mut out = String::new();
1608        let b: crate::scalars::BoolPayload = 1;
1609        // SAFETY: as above, for a `BoolPayload`.
1610        unsafe {
1611            (crate::scalars::BOOL.format)(
1612                std::ptr::addr_of!(b).cast::<u8>(),
1613                &mut crate::FormatSink::display(&mut out),
1614            );
1615        }
1616        assert_eq!(out, ScalarValue::Bool(true).to_string());
1617    }
1618
1619    /// The scan is driven from the frame entries, so a frame that claims no
1620    /// value slots must contribute nothing to it — and, more to the point, must
1621    /// not make the partition check in `clear_reclaimed` disagree with the value
1622    /// stack's own extent.
1623    #[test]
1624    fn a_zero_local_frame_neither_holds_nor_clears_anything() {
1625        let metas = shadowed_a_metas();
1626        let outer = meta_for(b"outer", &metas, (0, 0));
1627        let empty = meta_for(b"nothing", &[], (0, 0));
1628        let mut f = Fixture::new();
1629        let value =
1630            f.rt.heap()
1631                .alloc_unpaced(crate::scalars::INT_PAYLOAD, 9_999_i64);
1632        let ctx = f.ctx_ptr();
1633        // SAFETY: `ctx` is wired to `f.rt`, and both metas outlive both guards.
1634        let (mut outer_guard, inner_guard) =
1635            unsafe { (push_frame(ctx, &outer), push_frame(ctx, &empty)) };
1636        outer_guard.set(0, value);
1637
1638        f.rt.collect_now();
1639
1640        assert!(inner_guard.values().is_empty());
1641        // SAFETY: slot 0 decoded under local 0's own metadata.
1642        assert_eq!(
1643            unsafe { metas[0].read(outer_guard.values()[0]) },
1644            Some(DebugValue::Reclaimed),
1645            "the outer frame's slot was still reached by a scan the empty \
1646             frame took part in"
1647        );
1648        drop(inner_guard);
1649        drop(outer_guard);
1650    }
1651
1652    #[test]
1653    fn a_zero_local_function_claims_no_value_slots() {
1654        // The counterexample that keeps `MAX_RECURSION_DEPTH` in charge of the
1655        // depth bound rather than the value stack's capacity, restated for this
1656        // stack: a function with no `Gc` locals still needs a frame entry (its
1657        // name must appear in a `bt`) but claims no value slots at all.
1658        let meta = meta_for(b"nothing", &[], (0, 0));
1659        let mut f = Fixture::new();
1660        let ctx = f.ctx_ptr();
1661        // SAFETY: `ctx` is wired to `f.rt`, and `meta` outlives the guard.
1662        let guard = unsafe { push_frame(ctx, &meta) };
1663        assert_eq!(f.rt.debug_frame_stack().len(), 1);
1664        assert!(f.rt.debug_value_stack().is_empty());
1665        assert!(guard.values().is_empty());
1666        drop(guard);
1667    }
1668
1669    // -----------------------------------------------------------------------
1670    // ADR-128 decision 3: this stack is bounded on its own terms.
1671    //
1672    // The shadow stack's three sizing tests, restated here because the two
1673    // stacks are sized independently and `shadow_stack.rs`'s tests cover only
1674    // the other one.
1675    // -----------------------------------------------------------------------
1676
1677    /// The sibling of `rejects_an_oversized_frame`: a function with more `Gc`
1678    /// locals than the debug value stack can index is unconstructible, not
1679    /// rejected at run time.
1680    #[test]
1681    fn rejects_a_function_with_more_gc_locals_than_the_stack_can_index() {
1682        use crate::DebugSlotCount;
1683        assert!(DebugSlotCount::new(MAX_DEBUG_VALUE_SLOTS as u32).is_some());
1684        assert!(DebugSlotCount::new(MAX_DEBUG_VALUE_SLOTS as u32 + 1).is_none());
1685    }
1686
1687    /// The sibling of `the_reservation_covers_every_slot_the_budget_can_buy`.
1688    ///
1689    /// The arithmetic is the shadow stack's, and it closes here for the reason
1690    /// ADR-128 decision 4 gives: `frame_cost` charges the **dense** count of `Gc`
1691    /// locals, which is exactly this stack's width. Charging the colored root
1692    /// count instead would leave this reservation unbounded — which is one of the
1693    /// two reasons decision 4 refuses the obvious temptation.
1694    #[test]
1695    fn the_value_reservation_covers_every_slot_the_budget_can_buy() {
1696        for width in [0u32, 1, 11, 192, 1024, MAX_DEBUG_VALUE_SLOTS as u32] {
1697            let per_frame = crate::frame_cost(width);
1698            let frames = crate::STACK_BUDGET_BYTES / per_frame;
1699            let slots = frames as usize * width as usize;
1700            assert!(
1701                slots <= MAX_LIVE_SLOTS,
1702                "a stack of {frames} frames {width} `Gc` locals wide claims \
1703                 {slots} value slots, past the {MAX_LIVE_SLOTS}-slot bound"
1704            );
1705        }
1706        let stack = DebugValueStack::new(DEBUG_VALUE_STACK_SLOTS, None);
1707        assert_eq!(stack.capacity(), DEBUG_VALUE_STACK_SLOTS);
1708    }
1709
1710    /// The sibling of `a_wide_frame_spends_more_budget_than_a_narrow_one`, and
1711    /// what pins ADR-128 decision 4.
1712    ///
1713    /// A function's *debug* width is what the guard charges for, so a function
1714    /// with many `Gc` locals recurses less deeply than one with few — whatever
1715    /// its co-live root set colors down to. If some later change moved the charge
1716    /// onto the colored count, `MAX_DEBUG_VALUE_SLOTS` locals would cost the same
1717    /// as `REFERENCE_FRAME_SLOTS` and this fails.
1718    #[test]
1719    fn a_function_with_more_gc_locals_spends_more_budget() {
1720        let reference = crate::STACK_BUDGET_BYTES / crate::frame_cost(crate::REFERENCE_FRAME_SLOTS);
1721        let widest = crate::STACK_BUDGET_BYTES / crate::frame_cost(MAX_DEBUG_VALUE_SLOTS as u32);
1722        assert_eq!(
1723            reference, MAX_RECURSION_DEPTH,
1724            "a reference-width frame still reaches exactly the depth the old \
1725             call count allowed"
1726        );
1727        assert!(
1728            widest * 50 < reference,
1729            "and the widest function there is must be far dearer than the \
1730             reference one — 153 against 8000 as this is written: {widest} vs \
1731             {reference}"
1732        );
1733    }
1734}