diff --git a/library/modules/trace.sx b/library/modules/trace.sx index e05308b..8880576 100644 --- a/library/modules/trace.sx +++ b/library/modules/trace.sx @@ -11,22 +11,30 @@ // `catch` handler (the clear fires when the handler completes, so the body // still sees the chain) or the (future) failable-`main` wrapper. // -// Frame resolution: a frame is an opaque u64. Resolving it to `file:line:col` -// needs DWARF line-info (ERR E3.0), which sx does not emit yet — so for now -// each frame prints as "". The frame COUNT, ordering, -// and overflow note are already meaningful; once E3.0 lands, only the -// per-frame location string changes. (The comptime path — resolving a packed -// `(func_id, ir_offset)` via the interpreter's IR tables — also lands with the -// resolver in E3.0/E3.3-full.) +// Frame resolution (ERR E3.0 slice 3a): in compiled code a frame is a pointer +// to an interned `Frame` the compiler stamped in at the push site, so the +// location resolves in-process with no DWARF and no symbolizer. (The comptime +// path — a packed `(func_id, ir_offset)` resolved via the interpreter's IR +// tables — lands with slice 3b.) // ===================================================================== libc :: #library "c"; +// The compiled return-trace frame. Layout MUST match `getFrameStructType` in +// src/ir/emit_llvm.zig and `SxFrame` in library/vendors/sx_trace_runtime/sx_trace.c. +Frame :: struct { + file: string; + line: s32; + col: s32; + func: string; +} + // 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). sx_trace_len :: () -> u32 #foreign; sx_trace_truncated :: () -> u32 #foreign; -sx_trace_frame_at :: (i: u32) -> u64 #foreign; +sx_trace_frame_at :: (i: u32) -> *Frame #foreign; write :: (fd: s32, buf: [*]u8, count: usize) -> isize #foreign libc; @@ -43,10 +51,8 @@ to_string :: () -> string { i : u32 = 0; while i < n { - frame := sx_trace_frame_at(i); - // DWARF (E3.0) will resolve `frame` to file:line:col + function name. - // Until then the raw frame value is shown (a placeholder, not a PC yet). - line := format(" frame {}: (raw {})\n", i, xx frame); + f := 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/library/vendors/sx_trace_runtime/sx_trace.c b/library/vendors/sx_trace_runtime/sx_trace.c index e359827..0773a78 100644 --- a/library/vendors/sx_trace_runtime/sx_trace.c +++ b/library/vendors/sx_trace_runtime/sx_trace.c @@ -69,14 +69,20 @@ uint64_t sx_trace_frame_at(uint32_t i) { return sx_trace_frames[(base + i) % SX_TRACE_CAP]; } +// A compiled trace frame (ERR E3.0 slice 3a) is a pointer to an interned +// `Frame { string file; i32 line; i32 col; string func; }`, where an sx +// `string` is `{ const char* ptr; int64_t len; }`. This mirror MUST stay in +// lockstep with `getFrameStructType` in emit_llvm.zig and `Frame` in trace.sx. +typedef struct { const char *ptr; int64_t len; } SxStr; +typedef struct { SxStr file; int32_t line; int32_t col; SxStr func; } SxFrame; + // The failable-`main` entry-point reporter (ERR step E4.2). Called by the // emitted main wrapper when an error reaches the function boundary: prints the // unhandled-error header (with the tag name passed in — the compiler resolves // it from the always-linked tag-name table) followed by the surviving trace // frames, all to stderr. `name` is borrowed (a `string` slice, not NUL- // terminated), so `name_len` bounds the print. The frame format mirrors -// trace.sx's `to_string`; both stay placeholder ("") -// until DWARF line-info (E3.0) lands, after which both gain real file:line:col. +// trace.sx's `to_string` — `func at file:line:col`. void sx_trace_report_unhandled(uint32_t tag, const char *name, size_t name_len) { (void)tag; dprintf(2, "error: unhandled error reached main: error.%.*s\n", @@ -88,8 +94,10 @@ void sx_trace_report_unhandled(uint32_t tag, const char *name, size_t name_len) dprintf(2, " ... older frames omitted (buffer full)\n"); } for (uint32_t i = 0u; i < n; i++) { - uint64_t frame = sx_trace_frame_at(i); - dprintf(2, " frame %u: (raw %llu)\n", - i, (unsigned long long)frame); + const SxFrame *f = (const SxFrame *)(uintptr_t)sx_trace_frame_at(i); + dprintf(2, " %.*s at %.*s:%d:%d\n", + (int)f->func.len, f->func.ptr, + (int)f->file.len, f->file.ptr, + f->line, f->col); } } diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index e7d1c35..9547178 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -185,6 +185,19 @@ pub const LLVMEmitter = struct { // fallback for functions with no recorded source file. main_file: []const u8 = "", + // ── Error-trace `Frame` (ERR E3.0 slice 3a) ────────────────────── + // The compiled return-trace frame type: `{ string file, i32 line, + // i32 col, string func }`. Hand-built here (not looked up from a sx + // `TypeId`) so traces work even when the program doesn't import the + // module that declares `Frame`. The layout MUST stay in lockstep with + // `Frame` in `library/modules/trace.sx` (sx-side reader) and `SxFrame` + // in `sx_trace.c` (the failable-main reporter). + frame_struct_type: ?c.LLVMTypeRef = null, + // Interns the `{ptr,i64}` string constants the `Frame` globals embed, + // keyed by content, so a file/func name shared by N push sites is + // emitted once. Keys are owned. + frame_str_cache: std.StringHashMap(c.LLVMValueRef), + const PendingPhi = struct { phi: c.LLVMValueRef, block_id: BlockId, // the block this phi belongs to @@ -266,6 +279,7 @@ pub const LLVMEmitter = struct { .target_config = target_config, .build_config = .{}, .di_files = std.StringHashMap(c.LLVMMetadataRef).init(alloc), + .frame_str_cache = std.StringHashMap(c.LLVMValueRef).init(alloc), }; } @@ -280,6 +294,9 @@ pub const LLVMEmitter = struct { self.global_map.deinit(); self.block_map.deinit(); self.di_files.deinit(); + var fsc_it = self.frame_str_cache.keyIterator(); + while (fsc_it.next()) |k| self.alloc.free(k.*); + self.frame_str_cache.deinit(); if (self.di_builder != null) c.LLVMDisposeDIBuilder(self.di_builder); if (self.target_machine) |tm| c.LLVMDisposeTargetMachine(tm); c.LLVMDisposeBuilder(self.builder); @@ -1722,6 +1739,9 @@ pub const LLVMEmitter = struct { const name = self.ir_mod.types.getString(func.name); self.current_func_is_main = std.mem.eql(u8, name, "main"); self.current_func_idx = func_idx; + // Source file for span resolution — needed by `.trace_frame` even when + // DWARF is off (traces gate on opt level only, not on a source map). + self.current_func_file = func.source_file orelse self.main_file; // DWARF: describe this function and make it the scope for the // per-instruction locations set in emitInst (no-op if off). @@ -1873,6 +1893,9 @@ pub const LLVMEmitter = struct { // a dead `is_comptime()` branch. Emit nothing. self.advanceRefCounter(); }, + .trace_frame => { + self.mapRef(self.emitTraceFrame(instruction)); + }, .const_string => |str_id| { const str = self.ir_mod.types.getString(str_id); const llvm_val = self.emitStringConstant(str); @@ -4677,6 +4700,66 @@ pub const LLVMEmitter = struct { return self.string_struct_type.?; } + /// The compiled error-trace `Frame` type: `{ string, i32, i32, string }`. + /// Layout must match `Frame` in `trace.sx` and `SxFrame` in `sx_trace.c`. + fn getFrameStructType(self: *LLVMEmitter) c.LLVMTypeRef { + if (self.frame_struct_type) |t| return t; + const str_ty = self.getStringStructType(); + var field_types = [_]c.LLVMTypeRef{ + str_ty, // file + self.cached_i32, // line + self.cached_i32, // col + str_ty, // func + }; + self.frame_struct_type = c.LLVMStructTypeInContext(self.context, &field_types, 4, 0); + return self.frame_struct_type.?; + } + + /// An interned constant sx `string` (`{ ptr, i64 }`) of the cached string + /// struct type, backed by a private NUL-terminated data global. Cached by + /// content so a path/name shared by many push sites is emitted once. + fn buildStringConst(self: *LLVMEmitter, s: []const u8) c.LLVMValueRef { + if (self.frame_str_cache.get(s)) |v| return v; + const str_z = self.alloc.dupeZ(u8, s) catch unreachable; + defer self.alloc.free(str_z); + const data = c.LLVMAddGlobal(self.llvm_module, c.LLVMArrayType(self.cached_i8, @intCast(s.len + 1)), "frame.str"); + c.LLVMSetInitializer(data, c.LLVMConstStringInContext(self.context, str_z.ptr, @intCast(s.len + 1), 1)); + c.LLVMSetGlobalConstant(data, 1); + c.LLVMSetLinkage(data, c.LLVMPrivateLinkage); + c.LLVMSetUnnamedAddress(data, c.LLVMGlobalUnnamedAddr); + var fields = [_]c.LLVMValueRef{ data, c.LLVMConstInt(self.cached_i64, s.len, 0) }; + const str_const = c.LLVMConstNamedStruct(self.getStringStructType(), &fields, 2); + const key = self.alloc.dupe(u8, s) catch return str_const; + self.frame_str_cache.put(key, str_const) catch self.alloc.free(key); + return str_const; + } + + /// Build the interned `Frame` global for a `.trace_frame` push site and + /// return its address as `i64` (the value `sx_trace_push` stores). Resolves + /// the instruction's span + current function to `{file,line,col,func}`. The + /// file is shown as its basename so trace output is machine-independent + /// (the harness passes absolute paths); full paths live in DWARF. + fn emitTraceFrame(self: *LLVMEmitter, instruction: *const Inst) c.LLVMValueRef { + const file = std.fs.path.basename(self.current_func_file); + const src = self.sourceForFile(self.current_func_file); + const loc = errors.SourceLoc.compute(src, instruction.span.start); + const func_name = self.ir_mod.types.getString(self.ir_mod.functions.items[self.current_func_idx].name); + + var fields = [_]c.LLVMValueRef{ + self.buildStringConst(file), + c.LLVMConstInt(self.cached_i32, loc.line, 0), + c.LLVMConstInt(self.cached_i32, loc.col, 0), + self.buildStringConst(func_name), + }; + const frame_ty = self.getFrameStructType(); + const frame_const = c.LLVMConstNamedStruct(frame_ty, &fields, 4); + const g = c.LLVMAddGlobal(self.llvm_module, frame_ty, "trace.frame"); + c.LLVMSetInitializer(g, frame_const); + c.LLVMSetGlobalConstant(g, 1); + c.LLVMSetLinkage(g, c.LLVMPrivateLinkage); + return c.LLVMConstPtrToInt(g, self.cached_i64); + } + fn getAnyStructType(self: *LLVMEmitter) c.LLVMTypeRef { if (self.any_struct_type) |t| return t; var field_types = [_]c.LLVMTypeRef{ diff --git a/src/ir/inst.zig b/src/ir/inst.zig index 66d2ef7..bcd8536 100644 --- a/src/ir/inst.zig +++ b/src/ir/inst.zig @@ -96,6 +96,14 @@ pub const Op = union(enum) { /// code it's a no-op (only ever reached from a dead `is_comptime()` branch, /// where there is no interpreter stack to walk). interp_print_frames, + /// ERR E3.0 slice 3a — a return-trace frame value (`u64`) for the push site. + /// Niladic + span-stamped: it carries NO operands; each backend derives the + /// frame from its own context. `emit_llvm` resolves this instruction's span + /// + the current function → `{file,line,col,func}`, interns a `Frame` global, + /// and yields its address (`ptrtoint`). `interp` yields a packed + /// `(func_id << 32 | span.start)` for the comptime resolver (slice 3b). The + /// result feeds the existing `sx_trace_push(u64)` call. + trace_frame, /// 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 82452c4..797ea6a 100644 --- a/src/ir/interp.zig +++ b/src/ir/interp.zig @@ -640,6 +640,18 @@ pub const Interpreter = struct { self.printInterpFrames(); return .{ .value = .void_val }; }, + .trace_frame => { + // Comptime frame: pack (func_id, span.start) so the slice-3b + // resolver can recover file:line:col via the IR/source tables. + // The interp never produces a `Frame*` — only the compiled + // backend does — so this stays a packed id, never a pointer. + const fid: u64 = if (self.call_chain.items.len > 0) + self.call_chain.items[self.call_chain.items.len - 1].index() + else + 0; + const packed_frame: u64 = (fid << 32) | @as(u64, instruction.span.start); + return .{ .value = .{ .int = @bitCast(packed_frame) } }; + }, .const_type => |tid| return .{ .value = .{ .type_tag = tid } }, // ── Arithmetic ────────────────────────────────────── diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 84d3f15..3320f1e 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -13193,12 +13193,12 @@ pub const Lowering = struct { _ = self.builder.emit(.{ .call = .{ .callee = fids.clear, .args = &.{} } }, .void); } - /// A placeholder trace frame for a failure site (ERR E3.2). Until DWARF - /// (E3.0) provides return-address PCs, push a nonzero constant so the buffer - /// records that a frame occurred (the formatter notes "frame unavailable"). - /// Nonzero because frame value 0 is the buffer's out-of-range / empty marker. + /// The trace frame value for a failure site (ERR E3.0 slice 3a). Emits the + /// niladic `.trace_frame` op (span-stamped via `Builder.current_span`); each + /// backend resolves it to a real frame — `emit_llvm` to a `Frame*`, `interp` + /// to a packed `(func_id, offset)`. The result feeds `sx_trace_push`. fn placeholderTraceFrame(self: *Lowering) Ref { - return self.builder.constInt(1, .u64); + return self.builder.emit(.{ .trace_frame = {} }, .u64); } /// When a namespaced import (`Ns :: #import "..."`) contains foreign-class diff --git a/src/ir/print.zig b/src/ir/print.zig index fb810bc..d4f1e1c 100644 --- a/src/ir/print.zig +++ b/src/ir/print.zig @@ -145,6 +145,7 @@ fn printInst(instruction: *const Inst, ref_idx: u32, tt: *const TypeTable, write .const_undef => try writer.writeAll("const undef : "), .is_comptime => try writer.writeAll("is_comptime : "), .interp_print_frames => try writer.writeAll("interp_print_frames : "), + .trace_frame => try writer.writeAll("trace_frame : "), .const_type => |tid| try writer.print("const type({s}) : ", .{tt.typeName(tid)}), // ── Arithmetic ────────────────────────────────────────── diff --git a/tests/expected/243-trace-format.txt b/tests/expected/243-trace-format.txt index b0a9c88..f0d79c9 100644 --- a/tests/expected/243-trace-format.txt +++ b/tests/expected/243-trace-format.txt @@ -1,5 +1,5 @@ [stdout] caught BadInput error return trace (most recent call last): - frame 0: (raw 1) - frame 1: (raw 1) + leaf at 243-trace-format.sx:20:16 + mid at 243-trace-format.sx:25:5 [stdout] recovered; trace buffer now empty (len 0) diff --git a/tests/expected/244-failable-main.txt b/tests/expected/244-failable-main.txt index 61323b6..8a6d133 100644 --- a/tests/expected/244-failable-main.txt +++ b/tests/expected/244-failable-main.txt @@ -1,5 +1,5 @@ v = 10 error: unhandled error reached main: error.Empty error return trace (most recent call last): - frame 0: (raw 1) - frame 1: (raw 1) + inner at 244-failable-main.sx:18:17 + main at 244-failable-main.sx:26:10 diff --git a/tests/expected/247-failable-or-chain-propagate.txt b/tests/expected/247-failable-or-chain-propagate.txt index 96d057a..a056646 100644 --- a/tests/expected/247-failable-or-chain-propagate.txt +++ b/tests/expected/247-failable-or-chain-propagate.txt @@ -1,8 +1,8 @@ error: unhandled error reached main: error.A error return trace (most recent call last): - frame 0: (raw 1) - frame 1: (raw 1) - frame 2: (raw 1) - frame 3: (raw 1) - frame 4: (raw 1) - frame 5: (raw 1) + fa at 247-failable-or-chain-propagate.sx:14:17 + main at 247-failable-or-chain-propagate.sx:19:10 + fa at 247-failable-or-chain-propagate.sx:14:17 + main at 247-failable-or-chain-propagate.sx:19:10 + fa at 247-failable-or-chain-propagate.sx:14:17 + main at 247-failable-or-chain-propagate.sx:19:10