diff --git a/docs/debugger.md b/docs/debugger.md index 57ee006..06ae747 100644 --- a/docs/debugger.md +++ b/docs/debugger.md @@ -85,10 +85,12 @@ context (next section). ### The frame: an embedded `Frame`, not a PC **A runtime frame is a pointer to a compile-time-interned -`Frame {file, line, col, func}`.** The lowerer already knows the push +`Frame {file, line, col, func, line_text}`.** The lowerer already knows the push site's source location (the instruction's span + the enclosing function), -so the location is baked into read-only data at compile time and the -formatter reads it directly. No PC capture, no DWARF, no symbolizer. +so the location — *and the offending source line itself* (`line_text`, for the +`^` caret snippet) — is baked into read-only data at compile time and 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 @@ -435,7 +437,7 @@ a Mach-O debug map, never register JIT DWARF. | 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 | -| Source snippet + `^` caret | ⏳ planned — slice 3c | +| Source snippet + `^` caret | ✅ done — slice 3c (line embedded in `Frame`) | | `--emit-obj` / `--debug` artifact plumbing | ⏳ planned — slice 3d | | Stepping verification ladder (macOS → sim → device) | ⏳ planned — slice 3e (capstone) | | DWARF variable info (`DILocalVariable`, for `p x`) | ⏳ optional follow-on | diff --git a/library/modules/trace.sx b/library/modules/trace.sx index 13b8f61..80ed04f 100644 --- a/library/modules/trace.sx +++ b/library/modules/trace.sx @@ -27,6 +27,18 @@ Frame :: struct { line: s32; col: s32; func: string; + line_text: string; // the source line, for the snippet + caret +} + +// `n` spaces — used to position the `^` caret under a column. +spaces :: (n: s32) -> string { + s := ""; + i : s32 = 0; + while i < n { + s = concat(s, " "); + i = i + 1; + } + s; } // The error-trace buffer C API (library/vendors/sx_trace_runtime/sx_trace.c), @@ -54,8 +66,11 @@ to_string :: () -> string { i : u32 = 0; while i < n { 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); + result = concat(result, format(" {} at {}:{}:{}\n", f.func, f.file, f.line, f.col)); + if f.line_text.len > 0 { + result = concat(result, format(" {}\n", f.line_text)); + result = concat(result, concat(" ", concat(spaces(f.col - 1), "^\n"))); + } i = i + 1; } result; diff --git a/library/vendors/sx_trace_runtime/sx_trace.c b/library/vendors/sx_trace_runtime/sx_trace.c index 0773a78..0ca3da8 100644 --- a/library/vendors/sx_trace_runtime/sx_trace.c +++ b/library/vendors/sx_trace_runtime/sx_trace.c @@ -74,7 +74,7 @@ uint64_t sx_trace_frame_at(uint32_t i) { // `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; +typedef struct { SxStr file; int32_t line; int32_t col; SxStr func; SxStr line_text; } 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 @@ -99,5 +99,9 @@ void sx_trace_report_unhandled(uint32_t tag, const char *name, size_t name_len) (int)f->func.len, f->func.ptr, (int)f->file.len, f->file.ptr, f->line, f->col); + if (f->line_text.len > 0) { + dprintf(2, " %.*s\n", (int)f->line_text.len, f->line_text.ptr); + dprintf(2, " %*s^\n", f->col > 0 ? f->col - 1 : 0, ""); + } } } diff --git a/src/errors.zig b/src/errors.zig index d2b9484..ce20753 100644 --- a/src/errors.zig +++ b/src/errors.zig @@ -33,6 +33,18 @@ pub const SourceLoc = struct { } }; +/// The whole source line containing `byte_offset` (no trailing newline). Empty +/// when `source` is empty. Used to embed the offending line in a trace `Frame`. +pub fn lineAt(source: []const u8, byte_offset: u32) []const u8 { + if (source.len == 0) return source; + const at = @min(byte_offset, @as(u32, @intCast(source.len))); + var start: usize = at; + while (start > 0 and source[start - 1] != '\n') start -= 1; + var end: usize = at; + while (end < source.len and source[end] != '\n') end += 1; + return source[start..end]; +} + pub const LineInfo = struct { line_num: u32, text: []const u8, diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 77c74b0..6b1da0d 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -4721,8 +4721,9 @@ pub const LLVMEmitter = struct { self.cached_i32, // line self.cached_i32, // col str_ty, // func + str_ty, // line_text (the source line, for the snippet) }; - self.frame_struct_type = c.LLVMStructTypeInContext(self.context, &field_types, 4, 0); + self.frame_struct_type = c.LLVMStructTypeInContext(self.context, &field_types, 5, 0); return self.frame_struct_type.?; } @@ -4761,9 +4762,10 @@ pub const LLVMEmitter = struct { c.LLVMConstInt(self.cached_i32, loc.line, 0), c.LLVMConstInt(self.cached_i32, loc.col, 0), self.buildStringConst(func_name), + self.buildStringConst(errors.lineAt(src, instruction.span.start)), }; const frame_ty = self.getFrameStructType(); - const frame_const = c.LLVMConstNamedStruct(frame_ty, &fields, 4); + const frame_const = c.LLVMConstNamedStruct(frame_ty, &fields, 5); const g = c.LLVMAddGlobal(self.llvm_module, frame_ty, "trace.frame"); c.LLVMSetInitializer(g, frame_const); c.LLVMSetGlobalConstant(g, 1); diff --git a/src/ir/interp.zig b/src/ir/interp.zig index 85839d5..096d4b0 100644 --- a/src/ir/interp.zig +++ b/src/ir/interp.zig @@ -677,18 +677,21 @@ pub const Interpreter = struct { const file = std.fs.path.basename(file_full); var line: i64 = 1; var col: i64 = 1; + var line_text: []const u8 = ""; 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); + line_text = errors.lineAt(src, offset); } } - const fields = self.alloc.alloc(Value, 4) catch return .{ .value = .undef }; + const fields = self.alloc.alloc(Value, 5) catch return .{ .value = .undef }; fields[0] = .{ .string = file }; fields[1] = .{ .int = line }; fields[2] = .{ .int = col }; fields[3] = .{ .string = func_name }; + fields[4] = .{ .string = line_text }; return .{ .value = .{ .aggregate = fields } }; }, .const_type => |tid| return .{ .value = .{ .type_tag = tid } }, diff --git a/tests/expected/243-trace-format.txt b/tests/expected/243-trace-format.txt index f0d79c9..680f27a 100644 --- a/tests/expected/243-trace-format.txt +++ b/tests/expected/243-trace-format.txt @@ -1,5 +1,9 @@ [stdout] caught BadInput error return trace (most recent call last): leaf at 243-trace-format.sx:20:16 + if n < 0 { raise error.BadInput; } // pushes frame 0 + ^ mid at 243-trace-format.sx:25:5 + try leaf(n); // propagation pushes frame 1 + ^ [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 8a6d133..537b32a 100644 --- a/tests/expected/244-failable-main.txt +++ b/tests/expected/244-failable-main.txt @@ -2,4 +2,8 @@ v = 10 error: unhandled error reached main: error.Empty error return trace (most recent call last): inner at 244-failable-main.sx:18:17 + if n == 0 { raise error.Empty; } // pushes a frame + ^ main at 244-failable-main.sx:26:10 + w := try inner(0); // raises Empty → propagates to main + ^ diff --git a/tests/expected/247-failable-or-chain-propagate.txt b/tests/expected/247-failable-or-chain-propagate.txt index a056646..f330fed 100644 --- a/tests/expected/247-failable-or-chain-propagate.txt +++ b/tests/expected/247-failable-or-chain-propagate.txt @@ -1,8 +1,20 @@ error: unhandled error reached main: error.A error return trace (most recent call last): fa at 247-failable-or-chain-propagate.sx:14:17 + if n == 0 { raise error.A; } + ^ main at 247-failable-or-chain-propagate.sx:19:10 + v := try fa(0) or try fa(0) or try fa(0); // all fail → propagate to main + ^ fa at 247-failable-or-chain-propagate.sx:14:17 + if n == 0 { raise error.A; } + ^ main at 247-failable-or-chain-propagate.sx:19:10 + v := try fa(0) or try fa(0) or try fa(0); // all fail → propagate to main + ^ fa at 247-failable-or-chain-propagate.sx:14:17 + if n == 0 { raise error.A; } + ^ main at 247-failable-or-chain-propagate.sx:19:10 + v := try fa(0) or try fa(0) or try fa(0); // all fail → propagate to main + ^ diff --git a/tests/expected/253-comptime-trace.txt b/tests/expected/253-comptime-trace.txt index 76a0016..adbfefa 100644 --- a/tests/expected/253-comptime-trace.txt +++ b/tests/expected/253-comptime-trace.txt @@ -1,5 +1,9 @@ comptime caught Bad error return trace (most recent call last): leaf at 253-comptime-trace.sx:15:5 + raise error.Bad; + ^ mid at 253-comptime-trace.sx:19:5 + try leaf(); + ^ --- build done ---