docs(debugger): correct interp push-call model and span.start term (A9.2)

The interp's .trace_frame op only yields the packed value; the separate
sx_trace_push call op is executed by the interp as a foreign call via
host_ffi/dlsym, so the prior 'no sx_trace_push call runs' / 'never calls
sx_trace_push' phrasing was wrong. The packed low word is the op's
span.start (a source byte offset), not an IR instruction offset; renamed
every ir_offset/offset reference to span.start.
This commit is contained in:
agra
2026-06-03 14:49:23 +03:00
parent 0e5b79ddab
commit e5d9d1fec1

View File

@@ -40,7 +40,7 @@ to satisfy all three. "JIT" and "comptime" are **not** the same thing.
|---|---|---| |---|---|---|
| **AOT** (`sx build`) | native machine code in an on-disk binary | pointer to an interned `Frame` | | **AOT** (`sx build`) | native machine code in an on-disk binary | pointer to an interned `Frame` |
| **JIT** (`sx run`) | ORC-JIT'd machine code in anonymous memory | pointer to an interned `Frame` | | **JIT** (`sx run`) | ORC-JIT'd machine code in anonymous memory | pointer to an interned `Frame` |
| **Comptime** (`#run`) | the IR interpreter (`interp.zig`) — no machine code | packed `(func_id, ir_offset)` | | **Comptime** (`#run`) | the IR interpreter (`interp.zig`) — no machine code | packed `(func_id, span.start)` |
The crucial constraint: **the same lowered IR runs in the compiled The crucial constraint: **the same lowered IR runs in the compiled
backend *and* the interpreter.** So a value the IR produces (like a trace backend *and* the interpreter.** So a value the IR produces (like a trace
@@ -92,11 +92,12 @@ so the location — *and the offending source line itself* (`line_text`, for the
formatter reads it directly. No PC capture, no DWARF, no symbolizer, no runtime formatter reads it directly. No PC capture, no DWARF, no symbolizer, no runtime
file read. file read.
A comptime frame is instead a packed `(func_id: u32, ir_offset: u32)`, A comptime frame is instead a packed `(func_id: u32, span.start: u32)`
resolved through the interpreter's in-memory IR/source tables. The where `span.start` is the op's source byte offset — resolved through the
interpreter **never dereferences the compiled `Frame` pointer** — it uses interpreter's in-memory IR/source tables. The interpreter **never
its own representation — so the compiled and interpreted memory models dereferences the compiled `Frame` pointer** — it uses its own
never collide. representation — so the compiled and interpreted memory models never
collide.
### The niladic trace-push op ### The niladic trace-push op
@@ -113,9 +114,12 @@ context:
(the same mechanism, in the same file, as the tag-name table), and yields (the same mechanism, in the same file, as the tag-name table), and yields
its address as the op's value. The lowerer feeds that value to a separate its address as the op's value. The lowerer feeds that value to a separate
`sx_trace_push` call emitted through the normal call lowering. `sx_trace_push` call emitted through the normal call lowering.
- **`interp`:** yields the packed `(func_id, ir_offset)` from its own - **`interp`:** yields the packed `(func_id, span.start)` from its own
execution context as the op's value; no `sx_trace_push` call runs — the execution context as the op's value. The separate `sx_trace_push` call
packed value is recovered later by the comptime `.trace_resolve` resolver. op consuming it is executed by the interp as a foreign call (via
`host_ffi`/dlsym, the same path as any extern), storing the packed value
in the buffer; the comptime `.trace_resolve` resolver later recovers
`file:line:col` from it.
The op stays niladic by design: it carries no operand and no `GlobalId`, The op stays niladic by design: it carries no operand and no `GlobalId`,
so no IR-level `Frame` global is ever visible to the interpreter. The so no IR-level `Frame` global is ever visible to the interpreter. The
@@ -253,7 +257,7 @@ both the trace path and the DWARF path. Items marked ✅ exist today;
| [`src/ir/emit_llvm.zig`](../src/ir/emit_llvm.zig) | IR→LLVM orchestrator. Owns `LLVMEmitter` + the source map (`setDebugContext`); dispatches the `.trace_frame` op and the DWARF passes to the helpers below | | [`src/ir/emit_llvm.zig`](../src/ir/emit_llvm.zig) | IR→LLVM orchestrator. Owns `LLVMEmitter` + the source map (`setDebugContext`); dispatches the `.trace_frame` op and the DWARF passes to the helpers below |
| [`src/backend/llvm/reflection.zig`](../src/backend/llvm/reflection.zig) | `Reflection`: builds the interned `Frame` table + the tag-name / type-name tables; yields the `.trace_frame` op's value (the `Frame` global's address) — the `sx_trace_push` call itself is emitted by `lower.zig` | | [`src/backend/llvm/reflection.zig`](../src/backend/llvm/reflection.zig) | `Reflection`: builds the interned `Frame` table + the tag-name / type-name tables; yields the `.trace_frame` op's value (the `Frame` global's address) — the `sx_trace_push` call itself is emitted by `lower.zig` |
| [`src/backend/llvm/debug.zig`](../src/backend/llvm/debug.zig) | `DebugInfo`: builds all DWARF metadata (compile unit, per-function subprograms, per-instruction `DILocation`) | | [`src/backend/llvm/debug.zig`](../src/backend/llvm/debug.zig) | `DebugInfo`: builds all DWARF metadata (compile unit, per-function subprograms, per-instruction `DILocation`) |
| [`src/ir/interp.zig`](../src/ir/interp.zig) | Comptime IR interpreter. Lowers the `.trace_frame` op to a packed `(func_id, offset)`; resolves comptime frames | | [`src/ir/interp.zig`](../src/ir/interp.zig) | Comptime IR interpreter. The `.trace_frame` op yields a packed `(func_id, span.start)`; the separate `sx_trace_push` call op runs as a foreign call (dlsym); `.trace_resolve` recovers comptime frames |
| [`src/errors.zig`](../src/errors.zig) | `SourceLoc.compute(source, offset) → {line, col}`; the `import_sources` map type | | [`src/errors.zig`](../src/errors.zig) | `SourceLoc.compute(source, offset) → {line, col}`; the `import_sources` map type |
| [`src/ir/inst.zig`](../src/ir/inst.zig) | `Inst.span`, `Function.source_file`, the `Op` union (home of the `.trace_frame` op) | | [`src/ir/inst.zig`](../src/ir/inst.zig) | `Inst.span`, `Function.source_file`, the `Op` union (home of the `.trace_frame` op) |
| [`library/vendors/sx_trace_runtime/sx_trace.c`](../library/vendors/sx_trace_runtime/sx_trace.c) | the thread-local ring buffer + `sx_trace_report_unhandled` | | [`library/vendors/sx_trace_runtime/sx_trace.c`](../library/vendors/sx_trace_runtime/sx_trace.c) | the thread-local ring buffer + `sx_trace_report_unhandled` |
@@ -296,9 +300,11 @@ traces and DWARF can never disagree:
`sx_trace_push` call (step 1) consumes. The `sx_trace_push` extern is `sx_trace_push` call (step 1) consumes. The `sx_trace_push` extern is
declared lazily by `getTraceFids()` (which sets `needs_trace_runtime`). declared lazily by `getTraceFids()` (which sets `needs_trace_runtime`).
3. **Interpreter** (`interp.zig`, same op): pack `(current_func_id, 3. **Interpreter** (`interp.zig`, same op): pack `(current_func_id,
ir_offset)` into a `u64` and return it as the op's value. The interp span.start)` into a `u64` and return it as the op's value. The separate
never calls `sx_trace_push`; the packed value is recovered later by the `sx_trace_push` call op is then executed by the interp as a foreign call
comptime `.trace_resolve` resolver, which turns it back into (`callForeign` → `host_ffi.lookupSymbol`/dlsym, the same path as any
extern), storing the packed value in the buffer. The comptime
`.trace_resolve` resolver later turns each packed value back into
`file:line:col` via the IR/source tables. `file:line:col` via the IR/source tables.
**Buffer (run time) ✅** — `sx_trace.c` stores the `u64`s. Linked into the **Buffer (run time) ✅** — `sx_trace.c` stores the `u64`s. Linked into the
@@ -310,7 +316,7 @@ compiler so the JIT resolves `sx_trace_*` via `dlsym`; auto-injected as a
a **read-side context-split primitive** (the mirror of the `.trace_frame` op): a **read-side context-split primitive** (the mirror of the `.trace_frame` op):
- compiled: cast the `u64` → `*Frame`, load the fields. - compiled: cast the `u64` → `*Frame`, load the fields.
- comptime: unpack `(func_id, offset)`, resolve via the interpreter's - comptime: unpack `(func_id, span.start)`, resolve via the interpreter's
IR/source tables → a `Frame`. IR/source tables → a `Frame`.
The same `trace.sx` source works in both because it runs in the matching The same `trace.sx` source works in both because it runs in the matching
@@ -474,7 +480,7 @@ a Mach-O debug map, never register JIT DWARF.
| IR instructions carry source spans | ✅ done — E3.0 slice 1 (`b44a5d0`) | | IR instructions carry source spans | ✅ done — E3.0 slice 1 (`b44a5d0`) |
| DWARF emission (compile unit / subprogram / line table) | ✅ done — E3.0 slice 2 (`c32d694`) | | DWARF emission (compile unit / subprogram / line table) | ✅ done — E3.0 slice 2 (`c32d694`) |
| Niladic trace-push op + interned `Frame` table (runtime) | ✅ done — E3.3 slice 3a (`1b6cbc1`) | | Niladic trace-push op + interned `Frame` table (runtime) | ✅ done — E3.3 slice 3a (`1b6cbc1`) |
| Comptime resolver (`func_id, ir_offset` → location) | ✅ done — slice 3b | | Comptime resolver (`func_id, span.start` → location) | ✅ done — slice 3b |
| Source snippet + `^` caret | ✅ done — slice 3c (line embedded in `Frame`) | | Source snippet + `^` caret | ✅ done — slice 3c (line embedded in `Frame`) |
| `--emit-obj` artifact plumbing | ✅ done — slice 3d | | `--emit-obj` artifact plumbing | ✅ done — slice 3d |
| Stepping verification: macOS lldb | ✅ done — 3e rung 1 (`tests/debug_stepping_smoke.sh`) | | Stepping verification: macOS lldb | ✅ done — 3e rung 1 (`tests/debug_stepping_smoke.sh`) |