Skip to main content

Module debug

Module debug 

Source
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 the DebugLocalMeta array. 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 Gc local, claimed from a contiguous DebugValueStack the runtime owns, and one DebugFrameEntry pairing the meta with the base of that run, claimed from a contiguous DebugFrameStack. The word is an Option<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, is DebugSlotKind’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§

DebugFrameEntry
One live call’s debug frame: which function, and where its value slots are.
DebugFrameGuard
A debug frame claimed from Rust, released when dropped.
DebugLocalMeta
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 Type id, the user-vs-temp classification, the source span, and what its value slot’s word means. Flattened for FFI.
FunctionDebugMeta
Everything the crash debugger needs about a function that does not vary per call: its name, its source extent, and the metadata for its Gc locals.

Enums§

DebugSlotKind
What a debug value slot’s word is — and the only thing in the process that can say so (ADR-120 part 2).
DebugValue
What a debug slot holds: a reference the collector and the debugger may follow, or a raw scalar neither may.
ScalarValue
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_frame callers, who spend no budget and so are not covered by that guard. This is SHADOW_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 a u8 for the FFI boundary: 0 = a binding, 1 = a compiler temp. “Binding” is ADR-125’s sense — a var, a parameter, a for variable and a name a pattern introduces — so that the FFI constant and the compiler agree about what the byte means (ADR-139). Stored on each DebugLocalMeta so the debugger can separate the two in its display and name temps with their materializing expression.
NO_STATIC_TYPE
DebugLocalMeta::type_id when the MIR local has no static type (MirType::Opaque) — a pipeline accumulator, a fused-loop item.
RECLAIMED_WORD
The word DebugFrameStackHeader::clear_reclaimed writes over a reference slot whose object the sweep took, and the one word DebugLocalMeta::read decodes as DebugValue::Reclaimed.

Functions§

push_frame
Claim a debug frame for meta on ctx’s debug stacks, the way a generated prologue does: one frame entry, and meta.local_count value slots that start as None.

Type Aliases§

DebugFrameStack
The runtime’s one reservation of per-call frame entries.
DebugFrameStackHeader
The header generated code bump-allocates frame entries against.
DebugValueStack
The runtime’s one reservation of per-call debug value slots.
DebugValueStackHeader
The header generated code bump-allocates value slots against.