Skip to content

feat: a real call stack, shared by the editor and the CLI - #15

Open
RaoulSchaffranek wants to merge 1 commit into
mainfrom
feat/call-stack
Open

feat: a real call stack, shared by the editor and the CLI#15
RaoulSchaffranek wants to merge 1 commit into
mainfrom
feat/call-stack

Conversation

@RaoulSchaffranek

Copy link
Copy Markdown
Member

The Callstack view showed one frame, named after a wasm instruction and carrying the replay position in its label. It now shows every frame that led to the current line, and soroban-trace reports the same stack per stop as frames — both from one derivation, debugAdapter/callStack.ts, so a script and a debug session can never disagree about who called whom.

The stack is assembled from three sources, ordered by how much each can be trusted, and every frame states which one placed it:

The trace's own wasm activations are the STRUCTURE. computeDepths becomes computeFrames, which returns the linked activation stack (fn, depth, callSite, caller) the depths were always a projection of; depth is now that projection, so the frame count and step-over/ step-out are derived from the same walk and cannot drift apart. An outer frame stands at the call it is suspended in, which is also the record its own locals were last observed at.

DWARF adds the Rust frames inlining erased. ScopeIndex.inlineScopesAt walks the DW_TAG_inlined_subroutine instances covering a pc, resolving names through abstract_origin/specification/linkage_name and call sites through call_file/call_line; LineTable keeps each unit's file table so a call-site file INDEX resolves to a path. An instance whose range this parser cannot read (a v5 .debug_rnglists list) is skipped, never guessed at: a missing frame degrades the view, an invented one misreports the program. The trace's contract-call boundaries close the stack below as labels.

Names come off a precision ladder: the DWARF subprogram name qualified by its enclosing namespaces and types, else the module's name-section symbol demangled (new wasm/names.ts — rustc leaves some method DIEs anonymous, so this rung matters even in a DWARF build), else the function index, else the raw offset. A frame is never nameless, and a release build with no debug info still gets control::Control::while_call+0x1a.

Frames are inspectable per frame, not just at the top: a variables reference now encodes the frame together with the scope kind, so Locals, Value Stack and Variables answer for the SELECTED frame, read from that frame's own record. Linear memory stays at the cursor — a callee may have written through a reference the caller still holds. stackTrace honors the client's paging window, gives each frame its own id and instruction pointer reference (so Disassembly follows the selection), and reports a non-workspace or sourceless frame as subtle rather than dropping it: an optimized build can put eight SDK conversion frames between the user's code and the pc, and a stack that quietly hid them would be a lie about how the program got here.

The replay position moves out of the frame name into the thread label (soroban-vm [29/40]). A frame name says what the program is doing; where the cursor sits is a property of the recorded thread.

The rules are specified as C1-C8 in the new docs/callstack.md, pinned by test/callStack.test.ts and test/dapFrames.test.ts across the fixture spread that matters: opt-0, optimized, stripped of DWARF, without a name section, and with no wasm at all.

The Callstack view showed one frame, named after a wasm instruction and
carrying the replay position in its label. It now shows every frame that
led to the current line, and `soroban-trace` reports the same stack per
stop as `frames` — both from one derivation, `debugAdapter/callStack.ts`,
so a script and a debug session can never disagree about who called whom.

The stack is assembled from three sources, ordered by how much each can
be trusted, and every frame states which one placed it:

The trace's own wasm activations are the STRUCTURE. `computeDepths`
becomes `computeFrames`, which returns the linked activation stack
(`fn`, `depth`, `callSite`, `caller`) the depths were always a projection
of; depth is now that projection, so the frame count and step-over/
step-out are derived from the same walk and cannot drift apart. An outer
frame stands at the call it is suspended in, which is also the record its
own locals were last observed at.

DWARF adds the Rust frames inlining erased. `ScopeIndex.inlineScopesAt`
walks the DW_TAG_inlined_subroutine instances covering a pc, resolving
names through abstract_origin/specification/linkage_name and call sites
through call_file/call_line; LineTable keeps each unit's file table so a
call-site file INDEX resolves to a path. An instance whose range this
parser cannot read (a v5 .debug_rnglists list) is skipped, never guessed
at: a missing frame degrades the view, an invented one misreports the
program. The trace's contract-call boundaries close the stack below as
labels.

Names come off a precision ladder: the DWARF subprogram name qualified by
its enclosing namespaces and types, else the module's `name`-section
symbol demangled (new wasm/names.ts — rustc leaves some method DIEs
anonymous, so this rung matters even in a DWARF build), else the function
index, else the raw offset. A frame is never nameless, and a release
build with no debug info still gets `control::Control::while_call+0x1a`.

Frames are inspectable per frame, not just at the top: a variables
reference now encodes the frame together with the scope kind, so Locals,
Value Stack and Variables answer for the SELECTED frame, read from that
frame's own record. Linear memory stays at the cursor — a callee may have
written through a reference the caller still holds. `stackTrace` honors
the client's paging window, gives each frame its own id and instruction
pointer reference (so Disassembly follows the selection), and reports a
non-workspace or sourceless frame as `subtle` rather than dropping it: an
optimized build can put eight SDK conversion frames between the user's
code and the pc, and a stack that quietly hid them would be a lie about
how the program got here.

The replay position moves out of the frame name into the thread label
(`soroban-vm [29/40]`). A frame name says what the program is doing;
where the cursor sits is a property of the recorded thread.

The rules are specified as C1-C8 in the new docs/callstack.md, pinned by
test/callStack.test.ts and test/dapFrames.test.ts across the fixture
spread that matters: opt-0, optimized, stripped of DWARF, without a name
section, and with no wasm at all.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant