Skip to content

programs-react, web: apply instruction contexts as postconditions in the tracer - #289

Merged
gnidan merged 2 commits into
mainfrom
ui-context-offbyone
Sep 3, 2026
Merged

programs-react, web: apply instruction contexts as postconditions in the tracer#289
gnidan merged 2 commits into
mainfrom
ui-context-offbyone

Conversation

@gnidan

@gnidan gnidan commented Aug 6, 2026

Copy link
Copy Markdown
Member

Instruction contexts are postconditions: instruction i's context — its semantic facts and its pointers — describes the machine state after i executes. A trace step observes the machine state before its instruction runs, so a debugger paused at the step about to execute instruction i is observing the postcondition of instruction i-1.

The tracer was instead applying instruction i's context against instruction i's own pre-execution state. Consequences at O0 with memory-homed locals emitted:

  • Every variable read stale at its producing instruction — the pointer for the just-assigned value resolved against the state before the write. Freshly-allocated frame memory reads zero, so this surfaced as 0x000…0 ("not yet assigned") rather than obvious garbage, which is why it went unnoticed.
  • The value assigned by the last executed instruction was never shown at all: its postcondition can only surface on a following step, and that step (e.g. STOP) carries no context of its own under the old scheme.

Fix

Add effectiveContextForStep(program, pcToInstruction, trace, i): prepend the program-level context and index the resulting sequence by trace position — instruction i-1's context at step i, with the program-level context as the base case for the first step. Pointer resolution still runs against the state observed at step i; only the context selection shifts.

The helper is context-source-agnostic (it takes a contextAtPc accessor), so both tracer surfaces share it:

  • programs-react TraceContext — the variables and call-info panels.
  • web TraceDrawer — the same two panels; the two surfaces previously duplicated the resolver and now agree.

currentInstruction (opcode/pc display, source highlighting, transform tags) still tracks the instruction about to execute — only the state-resolved semantic facts move.

Call stack

Call-stack reconstruction moves to the same timing. buildCallStack now walks the effective-context sequence via effectiveContextForStep (program-level context as the base case, then instruction i-1's context at step i), so a frame is pushed on the step the call-info banner first names its invoke and the two can never disagree. The frame discipline itself is untouched — close-after, the caller-JUMP/callee-JUMPDEST invoke dedupe, TCO back-edge frame reuse, and inline virtual frames all behave as before, one step later. A frame's stepIndex is now the step whose observed state its argument pointers describe (the callee entry's postcondition), which is where both surfaces resolve them; since JUMPDEST is a no-op the resolved argument values are unchanged. A step with no context contributes no events and no inline membership, so the defensive membership guard still tears down a stale virtual frame there.

Tests

  • effectiveContextForStep unit tests: program-level base case, the previous-instruction selection, and the missing-context edges.
  • buildCallStack: new tests pin the frame/banner agreement at every step of a real call (including the frame's root step and argument pointers) and the program-level base case; the existing suite is re-timed with its discipline assertions intact.
  • TraceProvider integration tests (@testing-library/react): stepping shows instruction i-1's variables, the program-level context at the first step, the invoke surfacing only after its instruction executes, the frame and banner appearing together, and an argument resolving to the expected value through the shifted frame.

Full suite green; docusaurus build (web in-context typecheck) passes.

https://claude.ai/code/session_01JeiUMsA1rcjAbvraYpvg1F

…struction's postcondition

Instruction contexts are postconditions: instruction i's context —
its semantic facts and its pointers — describes the machine state
after i executes. A trace step observes the state before its
instruction runs, so the step about to execute instruction i is
observing the postcondition of instruction i-1. The tracer was
applying instruction i's context against instruction i's
pre-execution state, so every variable read stale at its producing
instruction (memory-homed locals masked this by reading zero) and
the value assigned by the last executed instruction was never shown.

Add effectiveContextForStep(program, pcToInstruction, trace, i):
prepend the program-level context and index the resulting sequence
by trace position — instruction i-1's context at step i, with the
program-level context as the base case for the first step. Pointer
resolution still runs against the state observed at step i; only the
context selection shifts.

Wire the shared helper into the two tracer surfaces (programs-react
TraceContext and the web TraceDrawer) for the variables and
call-info panels, so both stop resolving current-instruction facts a
step early and stop diverging from one another.

Call-stack reconstruction (buildCallStack) and its argument
resolution carry the same off-by-one but are left to a follow-up: the
frame-timing shift is entangled with the close-after / invoke-
inclusive / TCO / inline tuning, and the argument values are already
correct because their pointers resolve on a no-op JUMPDEST.
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-09-03 05:21 UTC

buildCallStack scanned each step's own instruction, so a frame was
pushed while parked on the caller JUMP — one step before the
call-info banner (already on postcondition timing) named the invoke.

Walk the effective-context sequence instead: at step i the frame
list reflects the contexts of the instructions executed at steps
0..i-1, with the program-level context as the base case, selected
per step via effectiveContextForStep so the frame list and the
banner can never disagree. A frame's stepIndex becomes the step
whose observed state its argument pointers describe — the callee
entry's postcondition — which is where both surfaces already
resolve them. JUMPDEST is a no-op, so the resolved argument values
are unchanged.

The frame discipline is untouched: close-after, the caller-JUMP /
callee-JUMPDEST invoke dedup, TCO back-edge frame reuse, and inline
virtual frames all survive with their step indices shifted by one.
A step with no context contributes no events and no inline
membership, so the defensive guard still tears down a stale virtual
frame there (previously only for instructions present in the map).

Tests: pin the frame/banner agreement at every step of a real call
and the program-level base case; re-time the existing buildCallStack
suite; add a TraceProvider integration test that resolves an
argument through the shifted frame.

Claude-Session: https://claude.ai/code/session_01JeiUMsA1rcjAbvraYpvg1F
@gnidan
gnidan merged commit 95c725f into main Sep 3, 2026
4 checks passed
@gnidan
gnidan deleted the ui-context-offbyone branch September 3, 2026 05:17
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