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}