Merge branch 'flow/sx-plan-arch/A9.2' into arch-refactor
This commit is contained in:
115
docs/debugger.md
115
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,39 +92,55 @@ 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
|
||||
|
||||
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`:** 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)`.
|
||||
- **`interp`:** pushes the packed `(func_id, ir_offset)` from its own
|
||||
execution context.
|
||||
- **`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), 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, 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.
|
||||
|
||||
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); `emit_llvm` 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 **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
|
||||
`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
|
||||
|
||||
@@ -193,8 +209,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,10 +254,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. Builds the interned `Frame` table; lowers the push op to a pointer push; emits all DWARF metadata |
|
||||
| [`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/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. 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-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` |
|
||||
@@ -270,17 +289,23 @@ 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
|
||||
via `host_ffi` `dlsym` against the linked `sx_trace.c`).
|
||||
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
|
||||
compiler so the JIT resolves `sx_trace_*` via `dlsym`; auto-injected as a
|
||||
@@ -288,10 +313,10 @@ 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
|
||||
- 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
|
||||
@@ -330,8 +355,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`
|
||||
@@ -455,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`) |
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user