From badf2af2986749442d9016165ca5a3634b265527 Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 3 Jun 2026 13:52:38 +0300 Subject: [PATCH 1/7] docs(debugger): point DWARF/Frame wiring at backend/llvm helpers (A9.2) Refresh the debugging architecture reference for the A7.2 relocation: DWARF emission lives in src/backend/llvm/debug.zig (DebugInfo) and the interned Frame / tag-name tables in src/backend/llvm/reflection.zig (Reflection); emit_llvm.zig is the orchestrator that owns LLVMEmitter and dispatches to them. Behavior is unchanged; only the file-and-function map, the 'what's emitted' home, and the debugEnabled() owner are corrected. --- docs/debugger.md | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/docs/debugger.md b/docs/debugger.md index 6c66467..fd3b56b 100644 --- a/docs/debugger.md +++ b/docs/debugger.md @@ -105,10 +105,12 @@ niladic, span-stamped IR op** — the same pattern as `is_comptime` / `interp_print_frames`. It carries **no operands and no global reference**; each backend derives the frame from its own context: -- **`emit_llvm`:** resolves the op's `span` + current function → - `{file, line, col, func}` (reusing the source map wired in for DWARF), - **interns and builds the `Frame` global in `emit_llvm`** (the same - mechanism as the tag-name table), then emits `call sx_trace_push(ptr)`. +- **`emit_llvm` (the `.trace_frame` arm):** resolves the op's `span` + + current function → `{file, line, col, func}` (reusing the source map + wired in for DWARF), **interns and builds the `Frame` global** in + [`src/backend/llvm/reflection.zig`](../src/backend/llvm/reflection.zig) + (the same mechanism, in the same file, as the tag-name table), then emits + `call sx_trace_push(ptr)`. - **`interp`:** pushes the packed `(func_id, ir_offset)` from its own execution context. @@ -118,8 +120,9 @@ alternative — an op carrying a `GlobalId` to an IR-level `Frame` global — would make the global visible to the interpreter (forcing comptime onto the pointer-deref path) and fatten the lowerer; **do not do this.** -`Frame` is defined **once** in sx (`trace.sx`/std); `emit_llvm` builds the -interned global off that `TypeId` through the normal struct-emission path, +`Frame` is defined **once** in sx (`trace.sx`/std); the reflection builder +(`src/backend/llvm/reflection.zig`) builds the interned global off that +`TypeId` through the normal struct-emission path, never a bespoke byte layout (which would risk the "8-bytes-assumed" clobber class of bug). `file`/`func` strings are interned into a shared pool so a path shared by N push sites is stored once — the table stays @@ -193,8 +196,9 @@ stripped without affecting traces. ### What's emitted -In [`src/ir/emit_llvm.zig`](../src/ir/emit_llvm.zig), gated on the same -debug opt levels + a wired source map (`setDebugContext`): +In [`src/backend/llvm/debug.zig`](../src/backend/llvm/debug.zig) (the +`DebugInfo` helper, driven from `emit_llvm`'s `emit()` pipeline), gated on +the same debug opt levels + a wired source map (`setDebugContext`): - one `DICompileUnit` + `DIFile` on the main file, - a `DISubprogram` per emitted function (`LLVMSetSubprogram`), @@ -237,7 +241,9 @@ both the trace path and the DWARF path. Items marked ✅ exist today; |---|---| | [`src/core.zig`](../src/core.zig) | `Compilation`: owns `import_sources` (file→source map), constructs the emitter, calls `setDebugContext` + `emit`; re-enters the interpreter for `#run`/post-link | | [`src/ir/lower.zig`](../src/ir/lower.zig) | AST→IR. Stamps `Inst.span`; emits push/clear at failure/absorb sites; `tracesEnabled` gate; declares the `sx_trace_*` externs | -| [`src/ir/emit_llvm.zig`](../src/ir/emit_llvm.zig) | IR→LLVM. Builds the interned `Frame` table; lowers the push op to a pointer push; emits all DWARF metadata | +| [`src/ir/emit_llvm.zig`](../src/ir/emit_llvm.zig) | IR→LLVM orchestrator. Owns `LLVMEmitter` + the source map (`setDebugContext`); dispatches the push 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; lowers the push op to a pointer push | +| [`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 push op to a packed `(func_id, offset)`; resolves comptime frames | | [`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-push op) | @@ -330,8 +336,8 @@ the failable-`main` wrapper, whose `ret` path in `emit_llvm` ### The gate: one switch, two consumers -`Lowering.tracesEnabled()` (lower.zig) and `LLVMEmitter.debugEnabled()` -(emit_llvm) both reduce to `opt_level == .none or .less`. The `Frame` +`Lowering.tracesEnabled()` (lower.zig) and `DebugInfo.debugEnabled()` +(backend/llvm/debug.zig) both reduce to `opt_level == .none or .less`. The `Frame` table + push/clear ride `tracesEnabled`; DWARF rides `debugEnabled`. Release (`-O2`/`-O3`) emits neither. `sx run` defaults to `-O0` (both on); `sx ir`/`sx asm` default to `-O2` (both off) — which is why the `.ir` From 5cb16912654bded44d45ffbd8333e91c445df05b Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 3 Jun 2026 14:03:44 +0300 Subject: [PATCH 2/7] docs(debugger): correct trace-frame op name and sx_trace_push attribution (A9.2) Name the niladic op `.trace_frame` (no `.trace_frame_push` op exists) in the trace-path roadmap, matching the rest of the doc and src/ir/inst.zig. Describe the `.trace_frame` arm as building/interning the Frame global and yielding its address as the op's value; the separate sx_trace_push call is emitted by the lowerer via normal call lowering, not by the arm itself. --- docs/debugger.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/debugger.md b/docs/debugger.md index fd3b56b..08e5540 100644 --- a/docs/debugger.md +++ b/docs/debugger.md @@ -109,8 +109,9 @@ each backend derives the frame from its own context: current function → `{file, line, col, func}` (reusing the source map wired in for DWARF), **interns and builds the `Frame` global** in [`src/backend/llvm/reflection.zig`](../src/backend/llvm/reflection.zig) - (the same mechanism, in the same file, as the tag-name table), then emits - `call sx_trace_push(ptr)`. + (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 + `sx_trace_push` call emitted through the normal call lowering. - **`interp`:** pushes the packed `(func_id, ir_offset)` from its own execution context. @@ -276,13 +277,15 @@ traces and DWARF can never disagree: 1. `lower.zig` reaches a failure site — `lowerRaise`, `lowerTry`'s propagation branch, `lowerFailableOr`, or `lowerDestructureDecl` — and - (when `tracesEnabled()`) emits the niladic `.trace_frame_push` op, - replacing today's `emitTracePush(placeholderTraceFrame())`. Absorbing - sites emit `emitTraceClear()` → `call sx_trace_clear()`. -2. **Compiled backend** (`emit_llvm.emitInst`, `.trace_frame_push` arm): + (when `tracesEnabled()`) emits the niladic `.trace_frame` op via + `placeholderTraceFrame()`, whose result feeds a separate `sx_trace_push` + call via `emitTracePush()`. Absorbing sites emit `emitTraceClear()` → + `call sx_trace_clear()`. +2. **Compiled backend** (`emit_llvm.emitInst`, `.trace_frame` arm): resolve the op's `span` + current function → `{file,line,col,func}`, intern into the `Frame` table (built alongside `tag_name_array`), and - emit `call sx_trace_push(ptr_to_Frame)`. The `sx_trace_push` extern is + yield the `Frame` global's address as the op's value, which the separate + `sx_trace_push` call (step 1) consumes. The `sx_trace_push` extern is declared lazily by `getTraceFids()` (which sets `needs_trace_runtime`). 3. **Interpreter** (`interp.zig`, same op): pack `(current_func_id, ir_offset)` into a `u64` and call the foreign `sx_trace_push` (resolved From e6c51359fe9c158c9d4c3a5aa76916a7feeef4f4 Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 3 Jun 2026 14:17:24 +0300 Subject: [PATCH 3/7] docs(debugger): align trace-push mechanism to one ground-truth model (A9.2) The .trace_frame op is niladic: it carries no operand and no GlobalId. The compiled backend yields the interned Frame global's address as the op's value (reflection.emitTraceFrame); the interpreter yields a packed (func_id, ir_offset) as the op's value and never calls sx_trace_push (recovered later by .trace_resolve). The sx_trace_push call is a separate call op emitted by lower.zig at each push site, consuming the op's value. Reword every passage that stated the old/wrong model: the niladic invariant is about the op (not the push site emitting only one instruction); reflection yields the op's value rather than lowering a push; the interp returns the packed value rather than calling the foreign sx_trace_push via host_ffi dlsym. --- docs/debugger.md | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/docs/debugger.md b/docs/debugger.md index 08e5540..c1487d9 100644 --- a/docs/debugger.md +++ b/docs/debugger.md @@ -100,10 +100,11 @@ never collide. ### The niladic trace-push op -Because the same IR runs in both machines, the push is a **dedicated, -niladic, span-stamped IR op** — the same pattern as `is_comptime` / -`interp_print_frames`. It carries **no operands and no global reference**; -each backend derives the frame from its own context: +Because the same IR runs in both machines, the frame value comes from a +**dedicated, niladic, span-stamped IR op** (`.trace_frame`) — the same +pattern as `is_comptime` / `interp_print_frames`. It carries **no operands +and no global reference**; each backend derives the frame from its own +context: - **`emit_llvm` (the `.trace_frame` arm):** resolves the op's `span` + current function → `{file, line, col, func}` (reusing the source map @@ -112,14 +113,16 @@ each backend derives the frame from its own context: (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 `sx_trace_push` call emitted through the normal call lowering. -- **`interp`:** pushes the packed `(func_id, ir_offset)` from its own - execution context. +- **`interp`:** yields the packed `(func_id, ir_offset)` from its own + execution context as the op's value; no `sx_trace_push` call runs — the + packed value is recovered later by the comptime `.trace_resolve` resolver. -This keeps the lowerer thin: at each push site it emits the op and nothing -else — no operand wiring, no global construction. The rejected -alternative — an op carrying a `GlobalId` to an IR-level `Frame` global — -would make the global visible to the interpreter (forcing comptime onto -the pointer-deref path) and fatten the lowerer; **do not do this.** +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 +rejected alternative — an op carrying a `GlobalId` to an IR-level `Frame` +global — would make the global visible to the interpreter (forcing +comptime onto the pointer-deref path) and fatten the lowerer; **do not do +this.** `Frame` is defined **once** in sx (`trace.sx`/std); the reflection builder (`src/backend/llvm/reflection.zig`) builds the interned global off that @@ -242,12 +245,12 @@ both the trace path and the DWARF path. Items marked ✅ exist today; |---|---| | [`src/core.zig`](../src/core.zig) | `Compilation`: owns `import_sources` (file→source map), constructs the emitter, calls `setDebugContext` + `emit`; re-enters the interpreter for `#run`/post-link | | [`src/ir/lower.zig`](../src/ir/lower.zig) | AST→IR. Stamps `Inst.span`; emits push/clear at failure/absorb sites; `tracesEnabled` gate; declares the `sx_trace_*` externs | -| [`src/ir/emit_llvm.zig`](../src/ir/emit_llvm.zig) | IR→LLVM orchestrator. Owns `LLVMEmitter` + the source map (`setDebugContext`); dispatches the push 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; lowers the push op to a pointer push | +| [`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/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 push op to a packed `(func_id, offset)`; resolves comptime frames | +| [`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/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-push 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/modules/trace.sx`](../library/modules/trace.sx) | the formatter (`to_string` / `print_current`) | | [`src/llvm_api.zig`](../src/llvm_api.zig) | binds `llvm-c/Core.h` + `llvm-c/DebugInfo.h` | @@ -288,8 +291,10 @@ traces and DWARF can never disagree: `sx_trace_push` call (step 1) consumes. The `sx_trace_push` extern is declared lazily by `getTraceFids()` (which sets `needs_trace_runtime`). 3. **Interpreter** (`interp.zig`, same op): pack `(current_func_id, - ir_offset)` into a `u64` and call the foreign `sx_trace_push` (resolved - via `host_ffi` `dlsym` against the linked `sx_trace.c`). + ir_offset)` into a `u64` and return it as the op's value. The interp + never calls `sx_trace_push`; the packed value is recovered later by the + comptime `.trace_resolve` resolver, which turns it back into + `file:line:col` via the IR/source tables. **Buffer (run time) ✅** — `sx_trace.c` stores the `u64`s. Linked into the compiler so the JIT resolves `sx_trace_*` via `dlsym`; auto-injected as a @@ -297,7 +302,7 @@ compiler so the JIT resolves `sx_trace_*` via `dlsym`; auto-injected as a **Formatter (run time) ✅ (compiled 3a, comptime 3b)** — `trace.sx` `to_string()` loops `sx_trace_len()` / `sx_trace_frame_at(i)` and resolves each `u64` through -a **read-side context-split primitive** (the mirror of the push op): +a **read-side context-split primitive** (the mirror of the `.trace_frame` op): - compiled: cast the `u64` → `*Frame`, load the fields. - comptime: unpack `(func_id, offset)`, resolve via the interpreter's From e907fc9e01ce8fe1e557a3889956e959ef3796f0 Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 3 Jun 2026 14:28:28 +0300 Subject: [PATCH 4/7] docs(debugger): describe Frame global build as LLVMConstNamedStruct over getFrameStructType (A9.2) The compiled backend builds each trace Frame global as an LLVM named-struct constant over the cached getFrameStructType() layout (file, line, col, func, line_text) via LLVMConstNamedStruct -- a type-safe LLVM struct, not the sx Frame TypeId / normal struct-emission path. Also correct the file field to the source basename (full paths live in DWARF). --- docs/debugger.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/docs/debugger.md b/docs/debugger.md index c1487d9..0d2968d 100644 --- a/docs/debugger.md +++ b/docs/debugger.md @@ -124,14 +124,19 @@ global — would make the global visible to the interpreter (forcing comptime onto the pointer-deref path) and fatten the lowerer; **do not do this.** -`Frame` is defined **once** in sx (`trace.sx`/std); the reflection builder -(`src/backend/llvm/reflection.zig`) builds the interned global off that -`TypeId` through the normal struct-emission path, -never a bespoke byte layout (which would risk the "8-bytes-assumed" -clobber class of bug). `file`/`func` strings are interned into a shared -pool so a path shared by N push sites is stored once — the table stays -tiny. File paths are normalized to a stable relative form so trace output -is machine-independent and snapshot-testable. +`Frame` is defined **once** in sx (`trace.sx`/std), and its runtime layout — +`{ string file, i32 line, i32 col, string func, string line_text }` — is +mirrored by the cached LLVM named-struct type `getFrameStructType()` +(`src/ir/emit_llvm.zig`). The reflection builder +(`src/backend/llvm/reflection.zig`) assembles each push site's global as an +LLVM **named-struct constant** over that cached type via +`LLVMConstNamedStruct` — a type-safe LLVM struct, not hand-packed bytes +(which would risk the "8-bytes-assumed" clobber class of bug). It does +**not** derive the layout from the sx `Frame` `TypeId`, nor route through +the normal struct-emission path. `file`/`func`/`line_text` strings are +interned into a shared pool so a path shared by N push sites is stored once +— the table stays tiny. The `file` field is the source basename (full paths +live in DWARF), so trace output is machine-independent and snapshot-testable. ### Push and clear sites From 0e5b79ddabf92b3ec4564ae13b707d6dee5d6acb Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 3 Jun 2026 14:36:08 +0300 Subject: [PATCH 5/7] docs(debugger): call getFrameStructType a literal (anonymous) struct type (A9.2) --- docs/debugger.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/debugger.md b/docs/debugger.md index 0d2968d..8a4e624 100644 --- a/docs/debugger.md +++ b/docs/debugger.md @@ -126,7 +126,7 @@ this.** `Frame` is defined **once** in sx (`trace.sx`/std), and its runtime layout — `{ string file, i32 line, i32 col, string func, string line_text }` — is -mirrored by the cached LLVM named-struct type `getFrameStructType()` +mirrored by the cached LLVM **literal (anonymous) struct type** `getFrameStructType()` (`src/ir/emit_llvm.zig`). The reflection builder (`src/backend/llvm/reflection.zig`) assembles each push site's global as an LLVM **named-struct constant** over that cached type via From e5d9d1fec187ecfe3e73ed1f39a9527fc9969c15 Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 3 Jun 2026 14:49:23 +0300 Subject: [PATCH 6/7] 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. --- docs/debugger.md | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/docs/debugger.md b/docs/debugger.md index 8a4e624..85aa934 100644 --- a/docs/debugger.md +++ b/docs/debugger.md @@ -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` | | **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 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 file read. -A comptime frame is instead a packed `(func_id: u32, ir_offset: u32)`, -resolved through the interpreter's in-memory IR/source tables. The -interpreter **never dereferences the compiled `Frame` pointer** — it uses -its own representation — so the compiled and interpreted memory models -never collide. +A comptime frame is instead a packed `(func_id: u32, span.start: u32)` — +where `span.start` is the op's source byte offset — resolved through the +interpreter's in-memory IR/source tables. The interpreter **never +dereferences the compiled `Frame` pointer** — it uses its own +representation — so the compiled and interpreted memory models never +collide. ### The niladic trace-push op @@ -113,9 +114,12 @@ context: (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 `sx_trace_push` call emitted through the normal call lowering. -- **`interp`:** yields the packed `(func_id, ir_offset)` from its own - execution context as the op's value; no `sx_trace_push` call runs — the - packed value is recovered later by the comptime `.trace_resolve` resolver. +- **`interp`:** yields the packed `(func_id, span.start)` from its own + execution context as the op's value. The separate `sx_trace_push` call + 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`, 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/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/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/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` | @@ -296,9 +300,11 @@ traces and DWARF can never disagree: `sx_trace_push` call (step 1) consumes. The `sx_trace_push` extern is declared lazily by `getTraceFids()` (which sets `needs_trace_runtime`). 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 - never calls `sx_trace_push`; the packed value is recovered later by the - comptime `.trace_resolve` resolver, which turns it back into + span.start)` into a `u64` and return it as the op's value. The separate + `sx_trace_push` call op is then executed by the interp as a foreign call + (`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. **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): - 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`. 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`) | | 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`) | -| 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`) | | `--emit-obj` artifact plumbing | ✅ done — slice 3d | | Stepping verification: macOS lldb | ✅ done — 3e rung 1 (`tests/debug_stepping_smoke.sh`) | From a7ddbeb85b23f4386f6055fa742fa9ea411f3ffe Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 3 Jun 2026 15:02:09 +0300 Subject: [PATCH 7/7] docs(error-handling): trace locations come from embedded Frame metadata, not DWARF (A9.2) --- docs/error-handling.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/error-handling.md b/docs/error-handling.md index 251eb82..c746801 100644 --- a/docs/error-handling.md +++ b/docs/error-handling.md @@ -326,7 +326,9 @@ error trace: Traces are on by default in debug builds and compiled out in release (re-enable with `--release-traces`). They cost nothing on the success -path. Frame locations resolve through the binary's debug info, so +path. Each frame's location comes from `Frame` metadata +(file/line/col/func) baked in at the trace point — the trace resolves +itself with no debug info. Separately, sx emits standard DWARF, so `lldb` / `gdb` work on sx binaries too. Interpolating a tag with `{}` prints its **name**, not a number — in