Expand description
Crash-debugger frame registration (§9.3, ADR-021, ADR-104).
What the crash debugger reads for bt/locals is, per live frame, a
function’s static metadata plus that call’s current local values. The two
are stored apart (ADR-104), because only one of them varies per call:
- Static:
FunctionDebugMeta— the function’s name, source span, and theDebugLocalMetaarray. One per function, interned in the JIT generation arena at compile time, shared by every call and every recursion level. Nothing records it at runtime; it is a compile-time constant. - Per call: one machine word per
Gclocal, claimed from a contiguousDebugValueStackthe runtime owns, and oneDebugFrameEntrypairing the meta with the base of that run, claimed from a contiguousDebugFrameStack. The word is anOption<GcRef>for every local whose box survives compilation and a raw scalar payload for one whose box ADR-120’s forwarding elided; which it is, isDebugSlotKind’s to say and nothing else’s.
Both stacks are SlotStacks — the mechanism ADR-101 built for the shadow
stack and made generic for exactly this. A prologue claims its slots by
bumping a top inline; an epilogue restores the saved base. No malloc, no
free, no extern call, no catch_unwind landing pad.
The values are written once per definition by the backend (ADR-104),
rather than re-written over the whole live set at every safepoint, and they
are never cleared: a value that has been produced stays renderable, which is
MIR-16’s contract and what locals in the crash REPL is for.
§The value slots are not a strong root set, and cannot become one by accident
DebugValueStack is SlotStack<Option<GcRef>> while the shadow stack is
SlotStack<*mut GcHeader>. The two are deliberately different types: the
RootSet impl lives on SlotStackHeader<*mut GcHeader>, so the debug value
stack does not have one and cannot be handed to the collector as something
to trace. That is ADR-044’s split made structural — the debug set is
over-approximate and never cleared, and tracing it would re-couple the two
sets and undo MIR-01.
It also reads better: a debug slot holds a value or nothing, which
Option<GcRef> says exactly (its None is the all-zero niche, F18, so a
zeroed claim is a run of “nothing yet”). A shadow slot is a raw pointer
only because the collector dereferences it and GcRef is NonNull.
§…but the collector does write them (ADR-106)
Not tracing them leaves a hazard: a value whose shadow slot RootSlots::dead
nulled, but whose debug slot still names it, is unreachable. A collection in
that window frees it, poison() nulls its descriptor, and the block is then
handed back out — after which the debug slot names a live object of an
entirely different type, and praxis_snapshot_debug_chain copies that into a
CrashSnapshot, which is a strong root set.
So the debug frames are RuntimeRoots’ one weak
arm. DebugFrameStackHeader::clear_reclaimed runs once per collection,
immediately after the sweep, and overwrites every slot naming reclaimed
storage with RECLAIMED_WORD. The slots retain nothing — a dead local’s
object still dies on schedule — and what the debugger renders for it is
<collected> rather than freed memory.
§…and one slot in three holds no reference at all (ADR-120 part 2)
ADR-120’s block-local forwarding deletes the box a value is put into so the
next instruction can take it straight back out — and with the box goes the
definition that would have written the debugger’s slot. Part 2 gives that
slot the scalar the box would have held, which means a value slot’s word
is not always an Option<GcRef>.
That is a memory-safety statement, not a display one, because of the
paragraph above: this stack is scanned after every sweep and the scan
dereferences what it finds. The discrimination is
DebugLocalMeta::slot_kind, a type, and DebugLocalMeta::read is the
only way to turn a word into a value. A scalar slot decodes to a
DebugValue::Scalar, which contains no GcRef, so no consumer — the
scan, the crash snapshot’s root set, or the debugger’s p EXPR bindings —
can reach a header through one. See DebugSlotKind.
Structs§
- Debug
Frame Entry - One live call’s debug frame: which function, and where its value slots are.
- Debug
Frame Guard - A debug frame claimed from Rust, released when dropped.
- Debug
Local Meta - One local’s metadata at frame construction: the source name (ptr + len),
the compiler-assigned symbol id, the local’s static type descriptor, the
full static
Typeid, the user-vs-temp classification, the source span, and what its value slot’s word means. Flattened for FFI. - Function
Debug Meta - Everything the crash debugger needs about a function that does not vary
per call: its name, its source extent, and the metadata for its
Gclocals.
Enums§
- Debug
Slot Kind - What a debug value slot’s word is — and the only thing in the process that can say so (ADR-120 part 2).
- Debug
Value - What a debug slot holds: a reference the collector and the debugger may follow, or a raw scalar neither may.
- Scalar
Value - One scalar payload read out of a debug slot, decoded under the slot’s
DebugSlotKind.
Constants§
- DEBUG_
FRAME_ STACK_ SLOTS - The size of the debug frame-entry reservation, in slots — one per live call,
bounded by the same depth guard, plus one entry of headroom for the
Rust-side
push_framecallers, who spend no budget and so are not covered by that guard. This isSHADOW_STACK_SLOTS’ headroom term counted in frames, because a frame stack claims one entry per call rather than one per slot. - DEBUG_
VALUE_ STACK_ SLOTS - The size of the debug value reservation, in slots.
- LOCAL_
KIND_ TEMP - LOCAL_
KIND_ USER - How a local appears in the crash debugger (§9.4
locals). Mirrors [praxis_mir::ir::LocalDebugKind], flattened to au8for the FFI boundary:0= a binding,1= a compiler temp. “Binding” is ADR-125’s sense — avar, a parameter, aforvariable and a name a pattern introduces — so that the FFI constant and the compiler agree about what the byte means (ADR-139). Stored on eachDebugLocalMetaso the debugger can separate the two in its display and name temps with their materializing expression. - NO_
STATIC_ TYPE DebugLocalMeta::type_idwhen the MIR local has no static type (MirType::Opaque) — a pipeline accumulator, a fused-loop item.- RECLAIMED_
WORD - The word
DebugFrameStackHeader::clear_reclaimedwrites over a reference slot whose object the sweep took, and the one wordDebugLocalMeta::readdecodes asDebugValue::Reclaimed.
Functions§
- push_
frame ⚠ - Claim a debug frame for
metaonctx’s debug stacks, the way a generated prologue does: one frame entry, andmeta.local_countvalue slots that start asNone.
Type Aliases§
- Debug
Frame Stack - The runtime’s one reservation of per-call frame entries.
- Debug
Frame Stack Header - The header generated code bump-allocates frame entries against.
- Debug
Value Stack - The runtime’s one reservation of per-call debug value slots.
- Debug
Value Stack Header - The header generated code bump-allocates value slots against.