ERR/E3.0 (slice 3c): source snippet + caret in traces
Each trace frame now shows the offending source line with a `^` caret under the column — in the catch-handler formatter, the failable-main C reporter, and the comptime path. The source line is embedded at compile time as a 5th Frame field (line_text), not read from disk at runtime: the file field is a basename and a runtime read would add a filesystem dependency that fails under the test harness and on locked-down targets. - errors.lineAt(src, offset): shared helper for the whole source line. - Frame gains line_text (mirrored in emit_llvm getFrameStructType, trace.sx Frame, sx_trace.c SxFrame). emitTraceFrame embeds it; the interp .trace_resolve extracts it from the source map. - trace.sx (new spaces helper) and the C reporter render the line + a col-aligned caret, guarded on a non-empty line_text. Snapshots 243/244/247/253 regenerated. Gates: zig build, zig build test, run_examples.sh -> 291 passed.
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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;
|
||||
|
||||
6
library/vendors/sx_trace_runtime/sx_trace.c
vendored
6
library/vendors/sx_trace_runtime/sx_trace.c
vendored
@@ -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, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 } },
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
^
|
||||
|
||||
@@ -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
|
||||
^
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
Reference in New Issue
Block a user