Skip to content

feat(trace): tape format v2 — scope-qualified locals via S records (#539 v2)#549

Merged
InauguralPhysicist merged 1 commit into
mainfrom
feat/539-scope-qualified-tape
Jul 11, 2026
Merged

feat(trace): tape format v2 — scope-qualified locals via S records (#539 v2)#549
InauguralPhysicist merged 1 commit into
mainfrom
feat/539-scope-qualified-tape

Conversation

@InauguralPhysicist

Copy link
Copy Markdown
Collaborator

The v2 half of #539 (v3 — the src/eigsdap.c DAP server with stepBack/reverseContinue in VS Code — remains open on that issue, and now has the scope-qualified substrate it was blocked on).

The problem

A-records carry the name only, so same-named bindings from different scopes merged into one stream: --step (and any tape consumer) could not tell a function-local i from the top-level one, and trajectory labels fed on the interleaved garbage.

The format change (TRACE_FORMAT_VERSION 1→2)

A new scope-transition record qualifies the A records that follow:

S <fn> <depth> <serial>

<serial> is a per-thread monotonically increasing frame-instance id (CallFrame.call_serial, stamped at all four frame-push sites) — so two invocations of the same function never merge either. Emission mirrors the L-record dedup discipline: an S is written only when the frame owning the next assignment differs from the last S, so the byte cost lands at call boundaries that actually assign.

  • Measured (call-heavy worst case, 20k calls, n=5): +43% tape bytes, ~8% traced runtime. A straight-line module tape carries exactly one S; untraced execution is untouched (the hook stays off). Stripping S records reproduces the v1 tape byte-for-byte — the change is purely additive.
  • Replay skips S like A — byte-exact replay determinism unchanged (verified same-seed).
  • v1 tapes refuse on both replay and --step per the Design: trace-tape format versioning — the tape is becoming a persisted artifact #411 version-and-reject rule (no compat promise, ever).

The stepper

--step folds S records into a reconstructed call chain (returning into a live serial pops to it; a new serial pops same-or-deeper frames and pushes with parent linkage):

  • p shows the live chain's bindings innermost-first with {in fn} notes and shadowing — dead frames' locals no longer appear, and a shadowed outer i stays hidden.
  • p name / t name resolve innermost-first, so trajectory labels are per-invocation ((i in work, frame #3)).

Pins & docs

  • tests/test_step.sh grows six scope checks (22 total): S records present, distinct serials per invocation, module stream unpolluted, frame-local shadowing, per-frame trajectory resolution, shadowed-outer hidden.
  • docs/TRACE.md: the S record row, the A record's new semantics, and the format version history.

Validation

Release suite 2737/2737; ASan+UBSan detect_leaks=1 2737/2737, zero LeakSanitizer reports (the run caught a t->scopes teardown leak in the stepper during development — fixed at all three teardown sites). Rebased on b5db2d6 (#547).

🤖 Generated with Claude Code

https://claude.ai/code/session_01GdDAVfZHgoQ2UNP63FYUXm

 v2)

Same-named bindings from different scopes merged into one A-record
stream, so --step (and any tape consumer) could not tell a function-
local i from the top-level one, and trajectory labels fed on the
interleaved garbage. v2 adds a scope-transition record

    S <fn> <depth> <serial>

qualifying the A records that follow. <serial> is a per-thread
monotonically increasing frame-instance id (CallFrame.call_serial,
stamped at all four frame-push sites), so two invocations of the same
function never merge either. Emission mirrors the L-record dedup: an S
is written only when the frame owning the next assignment differs from
the last S — cost lands at call boundaries that actually assign.

Measured (call-heavy worst case, 20k calls, n=5): +43% tape bytes,
~8% traced runtime; a straight-line module tape carries exactly one S;
untraced execution is untouched (the hook stays off). Stripping S
records reproduces the v1 tape byte-for-byte.

TRACE_FORMAT_VERSION 1→2: v1 tapes refuse on replay and --step per the
#411 version-and-reject rule. Replay skips S like A — byte-exact replay
determinism is unchanged (verified same-seed).

--step folds the S records into a reconstructed call chain (returning
into a live serial pops to it; a new serial pops same-or-deeper frames
and pushes with parent linkage). `p` shows the live chain innermost-
first with {in fn} notes and shadowing — dead frames' locals no longer
appear; `p name`/`t name` resolve innermost-first, so trajectory labels
are per-invocation. tests/test_step.sh grows six scope checks (22
total); docs/TRACE.md documents the record kind and version history.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdDAVfZHgoQ2UNP63FYUXm
@InauguralPhysicist InauguralPhysicist merged commit 83020c9 into main Jul 11, 2026
17 checks passed
@InauguralPhysicist InauguralPhysicist deleted the feat/539-scope-qualified-tape branch July 11, 2026 01:28
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