diff --git a/docs/debugger.md b/docs/debugger.md index ca0a69d..57ee006 100644 --- a/docs/debugger.md +++ b/docs/debugger.md @@ -284,7 +284,7 @@ traces and DWARF can never disagree: compiler so the JIT resolves `sx_trace_*` via `dlsym`; auto-injected as a `#source` for AOT when `needs_trace_runtime` is set. -**Formatter (run time) — compiled ✅ (3a), comptime ⏳ (3b)** — `trace.sx` `to_string()` loops +**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): @@ -434,7 +434,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) | ⏳ planned — slice 3b | +| Comptime resolver (`func_id, ir_offset` → location) | ✅ done — slice 3b | | Source snippet + `^` caret | ⏳ planned — slice 3c | | `--emit-obj` / `--debug` artifact plumbing | ⏳ planned — slice 3d | | Stepping verification ladder (macOS → sim → device) | ⏳ planned — slice 3e (capstone) | diff --git a/examples/253-comptime-trace.sx b/examples/253-comptime-trace.sx new file mode 100644 index 0000000..16ae40c --- /dev/null +++ b/examples/253-comptime-trace.sx @@ -0,0 +1,33 @@ +// Comptime return-trace resolution (ERR E3.0 slice 3b). A `#run` block that +// raises, propagates via `try`, and catches the error, then formats the trace +// with `trace.print_current()`. At comptime a frame is a packed +// `(func_id, span.start)` (not a `*Frame`); the interpreter's `.trace_resolve` +// unpacks it and resolves `file:line:col` via the module + source map — so the +// comptime trace prints the same `func at file:line:col` form as a runtime one. +// Expected exit: 0 (the error is caught; the trace is printed during the build). + +#import "modules/std.sx"; +trace :: #import "modules/trace.sx"; + +TErr :: error { Bad }; + +leaf :: () -> !TErr { + raise error.Bad; +} + +mid :: () -> !TErr { + try leaf(); +} + +probe :: () { + mid() catch e { + print("comptime caught {}\n", e); + trace.print_current(); + }; +} + +#run probe(); + +main :: () -> s32 { + return 0; +} diff --git a/library/modules/trace.sx b/library/modules/trace.sx index 8880576..13b8f61 100644 --- a/library/modules/trace.sx +++ b/library/modules/trace.sx @@ -31,10 +31,12 @@ Frame :: struct { // The error-trace buffer C API (library/vendors/sx_trace_runtime/sx_trace.c), // linked in for the JIT and auto-injected for AOT when traces are used. -// `frame_at` hands back the stamped `*Frame` (the compiler stored its address). +// `frame_at` returns the raw stored `u64`; `__trace_resolve_frame` turns it +// into a `Frame` — by reinterpreting the stamped `*Frame` in compiled code, or +// by resolving the packed `(func_id, span.start)` in the comptime interpreter. sx_trace_len :: () -> u32 #foreign; sx_trace_truncated :: () -> u32 #foreign; -sx_trace_frame_at :: (i: u32) -> *Frame #foreign; +sx_trace_frame_at :: (i: u32) -> u64 #foreign; write :: (fd: s32, buf: [*]u8, count: usize) -> isize #foreign libc; @@ -51,7 +53,7 @@ to_string :: () -> string { i : u32 = 0; while i < n { - f := sx_trace_frame_at(i); + f := __trace_resolve_frame(sx_trace_frame_at(i)); line := format(" {} at {}:{}:{}\n", f.func, f.file, f.line, f.col); result = concat(result, line); i = i + 1; diff --git a/src/core.zig b/src/core.zig index 83d4641..782295c 100644 --- a/src/core.zig +++ b/src/core.zig @@ -180,6 +180,7 @@ pub const Compilation = struct { const mod = self.ir_module orelse return error.NoIRModule; var interp = ir.Interpreter.init(mod, self.allocator); defer interp.deinit(); + interp.setSourceMap(&self.import_sources); if (self.ir_emitter) |*e| interp.build_config = &e.build_config; ir.Interpreter.last_bail_op = null; ir.Interpreter.last_bail_builtin = null; diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 9547178..77c74b0 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -1347,6 +1347,7 @@ pub const LLVMEmitter = struct { const func_id = ir_inst.FuncId.fromIndex(@intCast(i)); var interp_inst = Interpreter.init(self.ir_mod, self.alloc); interp_inst.build_config = &self.build_config; + if (self.import_sources) |sm| interp_inst.setSourceMap(sm); _ = interp_inst.call(func_id, &.{}) catch {}; // Route #run `print` output to fd 1 so it joins the // JIT-executed runtime's stream. Same call site shape as @@ -1385,6 +1386,7 @@ pub const LLVMEmitter = struct { if (global.comptime_func) |func_id| { var interp_inst = Interpreter.init(self.ir_mod, self.alloc); interp_inst.build_config = &self.build_config; + if (self.import_sources) |sm| interp_inst.setSourceMap(sm); Interpreter.last_bail_op = null; Interpreter.last_bail_builtin = null; Interpreter.last_bail_detail = null; @@ -1896,6 +1898,14 @@ pub const LLVMEmitter = struct { .trace_frame => { self.mapRef(self.emitTraceFrame(instruction)); }, + .trace_resolve => |u| { + // The operand is a `Frame*` stamped in by `.trace_frame` (as + // i64); reinterpret and load it. + const raw = self.resolveRef(u.operand); + const frame_ty = self.getFrameStructType(); + const ptr = c.LLVMBuildIntToPtr(self.builder, raw, self.cached_ptr, "frame.ptr"); + self.mapRef(c.LLVMBuildLoad2(self.builder, frame_ty, ptr, "frame.val")); + }, .const_string => |str_id| { const str = self.ir_mod.types.getString(str_id); const llvm_val = self.emitStringConstant(str); @@ -2517,6 +2527,7 @@ pub const LLVMEmitter = struct { if (callee_func.is_comptime and call_op.args.len == 0) { var interp_inst = Interpreter.init(self.ir_mod, self.alloc); interp_inst.build_config = &self.build_config; + if (self.import_sources) |sm| interp_inst.setSourceMap(sm); defer interp_inst.deinit(); if (interp_inst.call(call_op.callee, &.{})) |result| { if (result.asInt()) |v| { diff --git a/src/ir/inst.zig b/src/ir/inst.zig index bcd8536..3910c5c 100644 --- a/src/ir/inst.zig +++ b/src/ir/inst.zig @@ -104,6 +104,13 @@ pub const Op = union(enum) { /// `(func_id << 32 | span.start)` for the comptime resolver (slice 3b). The /// result feeds the existing `sx_trace_push(u64)` call. trace_frame, + /// ERR E3.0 slice 3b — the read-side resolver: a raw trace-buffer `u64` → + /// a `Frame` value. The mirror of `trace_frame`'s context split. + /// `emit_llvm` reinterprets the operand as `*Frame` and loads it (the value + /// `trace_frame` stamped in). `interp` unpacks `(func_id, span.start)` and + /// resolves it via the module's functions + the source map into a `Frame` + /// aggregate. Result type is the `Frame` `TypeId`. + trace_resolve: UnaryOp, /// Comptime-only Type value. Carried as a `Value.type_tag(TypeId)` /// in the interpreter. NEVER emitted to LLVM — types are erased /// after lowering. `emit_llvm` bails loudly if it sees one, diff --git a/src/ir/interp.zig b/src/ir/interp.zig index 797ea6a..85839d5 100644 --- a/src/ir/interp.zig +++ b/src/ir/interp.zig @@ -3,6 +3,7 @@ const Allocator = std.mem.Allocator; const types = @import("types.zig"); const inst_mod = @import("inst.zig"); const mod_mod = @import("module.zig"); +const errors = @import("../errors.zig"); const TypeId = types.TypeId; const TypeTable = types.TypeTable; @@ -148,6 +149,12 @@ pub const Interpreter = struct { /// tracked — foreign calls return before the frame is pushed. call_chain: std.ArrayList(FuncId) = .empty, + /// File → source text (the diagnostics' import_sources). Set by the host + /// where available so `.trace_resolve` can turn a `(func_id, span.start)` + /// frame into `file:line:col` at comptime (ERR E3.0 slice 3b). Null → the + /// resolver degrades to line/col 1:1. + source_map: ?*const std.StringHashMap([:0]const u8) = null, + // Heap: dynamically allocated memory blocks heap: std.ArrayList([]u8), @@ -203,6 +210,12 @@ pub const Interpreter = struct { }; } + /// Provide the file→source map so `.trace_resolve` can compute file:line:col + /// for comptime trace frames. Optional — absent in unit tests. + pub fn setSourceMap(self: *Interpreter, sm: *const std.StringHashMap([:0]const u8)) void { + self.source_map = sm; + } + pub fn deinit(self: *Interpreter) void { // Free all heap allocations for (self.heap.items) |block| { @@ -652,6 +665,32 @@ pub const Interpreter = struct { const packed_frame: u64 = (fid << 32) | @as(u64, instruction.span.start); return .{ .value = .{ .int = @bitCast(packed_frame) } }; }, + .trace_resolve => |u| { + // Unpack the comptime frame `(func_id << 32 | span.start)` and + // resolve it to a `Frame { file, line, col, func }` aggregate. + const raw: u64 = @bitCast(frame.getRef(u.operand).asInt() orelse 0); + const fid: u32 = @intCast(raw >> 32); + const offset: u32 = @truncate(raw); + const func = self.module.getFunction(FuncId.fromIndex(fid)); + const func_name = self.module.types.getString(func.name); + const file_full = func.source_file orelse ""; + const file = std.fs.path.basename(file_full); + var line: i64 = 1; + var col: i64 = 1; + if (self.source_map) |sm| { + if (sm.get(file_full)) |src| { + const loc = errors.SourceLoc.compute(src, offset); + line = @intCast(loc.line); + col = @intCast(loc.col); + } + } + const fields = self.alloc.alloc(Value, 4) catch return .{ .value = .undef }; + fields[0] = .{ .string = file }; + fields[1] = .{ .int = line }; + fields[2] = .{ .int = col }; + fields[3] = .{ .string = func_name }; + return .{ .value = .{ .aggregate = fields } }; + }, .const_type => |tid| return .{ .value = .{ .type_tag = tid } }, // ── Arithmetic ────────────────────────────────────── diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 3320f1e..06ea225 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -8554,6 +8554,7 @@ pub const Lowering = struct { var interp = interp_mod.Interpreter.init(self.module, self.alloc); defer interp.deinit(); + if (self.diagnostics) |d| if (d.import_sources) |sm| interp.setSourceMap(sm); const result = interp.call(ct_func_id, &.{}) catch return null; @@ -10345,6 +10346,18 @@ pub const Lowering = struct { // chain at comptime, no-op in compiled code (ERR E4.1). return self.builder.emit(.{ .interp_print_frames = {} }, .void); } + if (std.mem.eql(u8, name, "__trace_resolve_frame")) { + // Backs `trace.sx`'s formatter: a raw trace-buffer u64 → a `Frame`. + // Compiled code reinterprets the operand as `*Frame` and loads it; + // the interp unpacks (func_id, span.start) and resolves (ERR E3.0 + // slice 3b). Result type is the `Frame` struct from trace.sx. + const frame_ty = self.module.types.findByName(self.module.types.internString("Frame")) orelse { + if (self.diagnostics) |d| d.addFmt(.err, null, "`__trace_resolve_frame` needs `Frame` (from trace.sx) in scope", .{}); + return self.builder.constInt(0, .void); + }; + const arg = self.lowerExpr(c.args[0]); + return self.builder.emit(.{ .trace_resolve = .{ .operand = arg } }, frame_ty); + } if (std.mem.eql(u8, name, "error_tag_name")) { // error_tag_name(e) → look the error-set value's runtime tag id up // in the always-linked tag-name table. The value IS its u32 tag id. @@ -13981,6 +13994,8 @@ pub const Lowering = struct { if (std.mem.eql(u8, bare_name, "error_tag_name")) return .string; if (std.mem.eql(u8, bare_name, "is_comptime")) return .bool; if (std.mem.eql(u8, bare_name, "__interp_print_frames")) return .void; + if (std.mem.eql(u8, bare_name, "__trace_resolve_frame")) + return self.module.types.findByName(self.module.types.internString("Frame")) orelse .unresolved; if (std.mem.eql(u8, bare_name, "is_flags")) return .bool; if (std.mem.eql(u8, bare_name, "type_of")) return .any; if (std.mem.eql(u8, bare_name, "field_value")) return .any; diff --git a/src/ir/print.zig b/src/ir/print.zig index d4f1e1c..d1c850b 100644 --- a/src/ir/print.zig +++ b/src/ir/print.zig @@ -146,6 +146,7 @@ fn printInst(instruction: *const Inst, ref_idx: u32, tt: *const TypeTable, write .is_comptime => try writer.writeAll("is_comptime : "), .interp_print_frames => try writer.writeAll("interp_print_frames : "), .trace_frame => try writer.writeAll("trace_frame : "), + .trace_resolve => |u| try writer.print("trace_resolve %{d} : ", .{u.operand.index()}), .const_type => |tid| try writer.print("const type({s}) : ", .{tt.typeName(tid)}), // ── Arithmetic ────────────────────────────────────────── diff --git a/tests/expected/253-comptime-trace.exit b/tests/expected/253-comptime-trace.exit new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/tests/expected/253-comptime-trace.exit @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/tests/expected/253-comptime-trace.txt b/tests/expected/253-comptime-trace.txt new file mode 100644 index 0000000..76a0016 --- /dev/null +++ b/tests/expected/253-comptime-trace.txt @@ -0,0 +1,5 @@ +comptime caught Bad +error return trace (most recent call last): + leaf at 253-comptime-trace.sx:15:5 + mid at 253-comptime-trace.sx:19:5 +--- build done ---