lang F0.2: caret/squiggle rendering + new render dispatch
Adds RenderStyle (compact/extended), renderExtended/renderExtendedOne producing the locked Rust-style format (header, --> location arrow, blank bar, numbered code excerpt, caret line), and dispatches render() through a render_style field on DiagnosticList. Old render body extracted as renderCompact and kept as the default so existing snapshots stay unchanged — F0.3 flips the default. renderExtendedOne builds on F0.1's extractContext. Helpers digitCount (line-number column width) and writeRepeated (no writeByteNTimes in modern std.Io.Writer) are file-private. Line-number column has a min width of 2 to match Rust's visual style. 7 new tests cover single-line span with carets, warning prefix, span-less header, triple-digit line widening the column, empty-span single caret, multi-line span with per-line carets, and the compact- default regression. All 15 errors tests pass via `zig test src/errors.test.zig`; 224 regression tests green. Surfaced gotcha: zig build test doesn't currently exercise src/*.test.zig files because src/root.zig lacks refAllDecls; adding it exposes pre-existing breakage in src/ir/lower.test.zig and src/ir/types.zig. Reverted that addition — out of scope for the lang workstream; unit-test verification uses direct zig test for now.
This commit is contained in:
@@ -107,3 +107,150 @@ test "extractContext: span beyond source length is clamped" {
|
||||
try testing.expectEqual(@as(u32, 4), ctx.start_col);
|
||||
try testing.expectEqual(@as(u32, 4), ctx.end_col);
|
||||
}
|
||||
|
||||
// ─── renderExtended tests ────────────────────────────────────────────
|
||||
|
||||
fn renderToString(dl: *const errors.DiagnosticList, allocator: std.mem.Allocator) ![]u8 {
|
||||
var aw = std.Io.Writer.Allocating.init(allocator);
|
||||
try dl.renderExtended(&aw.writer);
|
||||
var result = aw.writer.toArrayList();
|
||||
return result.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
test "renderExtended: single-line span with carets" {
|
||||
var dl = errors.DiagnosticList.init(testing.allocator, "hello world\n", "test.sx");
|
||||
defer dl.deinit();
|
||||
dl.add(.err, "test error", Span{ .start = 6, .end = 11 });
|
||||
|
||||
const output = try renderToString(&dl, testing.allocator);
|
||||
defer testing.allocator.free(output);
|
||||
|
||||
const expected =
|
||||
\\error: test error
|
||||
\\ --> test.sx:1:7
|
||||
\\ |
|
||||
\\ 1 | hello world
|
||||
\\ | ^^^^^
|
||||
\\
|
||||
;
|
||||
try testing.expectEqualStrings(expected, output);
|
||||
}
|
||||
|
||||
test "renderExtended: warning level prefix" {
|
||||
var dl = errors.DiagnosticList.init(testing.allocator, "abc\n", "w.sx");
|
||||
defer dl.deinit();
|
||||
dl.add(.warn, "soft warning", Span{ .start = 0, .end = 3 });
|
||||
|
||||
const output = try renderToString(&dl, testing.allocator);
|
||||
defer testing.allocator.free(output);
|
||||
|
||||
const expected =
|
||||
\\warning: soft warning
|
||||
\\ --> w.sx:1:1
|
||||
\\ |
|
||||
\\ 1 | abc
|
||||
\\ | ^^^
|
||||
\\
|
||||
;
|
||||
try testing.expectEqualStrings(expected, output);
|
||||
}
|
||||
|
||||
test "renderExtended: diagnostic without span emits header only" {
|
||||
var dl = errors.DiagnosticList.init(testing.allocator, "", "x.sx");
|
||||
defer dl.deinit();
|
||||
dl.add(.err, "no source", null);
|
||||
|
||||
const output = try renderToString(&dl, testing.allocator);
|
||||
defer testing.allocator.free(output);
|
||||
|
||||
const expected = "error: no source\n";
|
||||
try testing.expectEqualStrings(expected, output);
|
||||
}
|
||||
|
||||
test "renderExtended: line-number column widens for triple-digit lines" {
|
||||
// Build a source with many newlines so the diagnostic lands on line 100.
|
||||
var src: std.ArrayList(u8) = .empty;
|
||||
defer src.deinit(testing.allocator);
|
||||
var i: u32 = 0;
|
||||
while (i < 99) : (i += 1) try src.append(testing.allocator, '\n');
|
||||
try src.appendSlice(testing.allocator, "boom");
|
||||
const source = src.items;
|
||||
|
||||
var dl = errors.DiagnosticList.init(testing.allocator, source, "big.sx");
|
||||
defer dl.deinit();
|
||||
const start: u32 = @intCast(source.len - 4);
|
||||
const end: u32 = @intCast(source.len);
|
||||
dl.add(.err, "out here", Span{ .start = start, .end = end });
|
||||
|
||||
const output = try renderToString(&dl, testing.allocator);
|
||||
defer testing.allocator.free(output);
|
||||
|
||||
const expected =
|
||||
\\error: out here
|
||||
\\ --> big.sx:100:1
|
||||
\\ |
|
||||
\\100 | boom
|
||||
\\ | ^^^^
|
||||
\\
|
||||
;
|
||||
try testing.expectEqualStrings(expected, output);
|
||||
}
|
||||
|
||||
test "renderExtended: empty span produces single caret" {
|
||||
var dl = errors.DiagnosticList.init(testing.allocator, "xyz\n", "p.sx");
|
||||
defer dl.deinit();
|
||||
dl.add(.err, "point error", Span{ .start = 1, .end = 1 });
|
||||
|
||||
const output = try renderToString(&dl, testing.allocator);
|
||||
defer testing.allocator.free(output);
|
||||
|
||||
const expected =
|
||||
\\error: point error
|
||||
\\ --> p.sx:1:2
|
||||
\\ |
|
||||
\\ 1 | xyz
|
||||
\\ | ^
|
||||
\\
|
||||
;
|
||||
try testing.expectEqualStrings(expected, output);
|
||||
}
|
||||
|
||||
test "renderExtended: multi-line span renders each line with carets" {
|
||||
var dl = errors.DiagnosticList.init(testing.allocator, "abc\ndef\n", "m.sx");
|
||||
defer dl.deinit();
|
||||
// span covers "bc\nde" — offsets 1..6.
|
||||
dl.add(.err, "spans two lines", Span{ .start = 1, .end = 6 });
|
||||
|
||||
const output = try renderToString(&dl, testing.allocator);
|
||||
defer testing.allocator.free(output);
|
||||
|
||||
const expected =
|
||||
\\error: spans two lines
|
||||
\\ --> m.sx:1:2
|
||||
\\ |
|
||||
\\ 1 | abc
|
||||
\\ | ^^
|
||||
\\ 2 | def
|
||||
\\ | ^^
|
||||
\\
|
||||
;
|
||||
try testing.expectEqualStrings(expected, output);
|
||||
}
|
||||
|
||||
test "render: default compact style still works" {
|
||||
var dl = errors.DiagnosticList.init(testing.allocator, "abc\n", "c.sx");
|
||||
defer dl.deinit();
|
||||
dl.add(.err, "compact error", Span{ .start = 0, .end = 3 });
|
||||
|
||||
// Default render_style is .compact — preserves the existing gcc-style.
|
||||
var aw = std.Io.Writer.Allocating.init(testing.allocator);
|
||||
defer {
|
||||
var r = aw.writer.toArrayList();
|
||||
r.deinit(testing.allocator);
|
||||
}
|
||||
try dl.render(&aw.writer);
|
||||
var result = aw.writer.toArrayList();
|
||||
defer result.deinit(testing.allocator);
|
||||
|
||||
try testing.expectEqualStrings("c.sx:1:1: error: compact error\n", result.items);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,11 @@ pub const Level = enum {
|
||||
note,
|
||||
};
|
||||
|
||||
pub const RenderStyle = enum {
|
||||
compact,
|
||||
extended,
|
||||
};
|
||||
|
||||
pub const SourceLoc = struct {
|
||||
line: u32,
|
||||
col: u32,
|
||||
@@ -107,6 +112,7 @@ pub const DiagnosticList = struct {
|
||||
file_name: []const u8,
|
||||
current_source_file: ?[]const u8 = null,
|
||||
import_sources: ?*const std.StringHashMap([:0]const u8) = null,
|
||||
render_style: RenderStyle = .compact,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, source: []const u8, file_name: []const u8) DiagnosticList {
|
||||
return .{
|
||||
@@ -161,6 +167,13 @@ pub const DiagnosticList = struct {
|
||||
}
|
||||
|
||||
pub fn render(self: *const DiagnosticList, writer: anytype) !void {
|
||||
switch (self.render_style) {
|
||||
.compact => try self.renderCompact(writer),
|
||||
.extended => try self.renderExtended(writer),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn renderCompact(self: *const DiagnosticList, writer: anytype) !void {
|
||||
for (self.items.items) |d| {
|
||||
const level_str = switch (d.level) {
|
||||
.err => "error",
|
||||
@@ -177,6 +190,68 @@ pub const DiagnosticList = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn renderExtended(self: *const DiagnosticList, writer: anytype) !void {
|
||||
for (self.items.items) |d| {
|
||||
try self.renderExtendedOne(writer, d);
|
||||
}
|
||||
}
|
||||
|
||||
fn renderExtendedOne(self: *const DiagnosticList, writer: anytype, d: Diagnostic) !void {
|
||||
const level_str = switch (d.level) {
|
||||
.err => "error",
|
||||
.warn => "warning",
|
||||
.note => "note",
|
||||
};
|
||||
|
||||
try writer.print("{s}: {s}\n", .{ level_str, d.message });
|
||||
|
||||
const span = d.span orelse return;
|
||||
const resolved = self.resolveSourceAndFile(d);
|
||||
const loc = SourceLoc.compute(resolved.source, span.start);
|
||||
try writer.print(" --> {s}:{d}:{d}\n", .{ resolved.file_name, loc.line, loc.col });
|
||||
|
||||
var ctx = extractContext(self.allocator, resolved.source, span) catch return;
|
||||
defer ctx.deinit(self.allocator);
|
||||
if (ctx.lines.len == 0) return;
|
||||
|
||||
const max_line_num = ctx.lines[ctx.lines.len - 1].line_num;
|
||||
const line_num_width = @max(@as(usize, 2), digitCount(max_line_num));
|
||||
|
||||
try writeRepeated(writer, ' ', line_num_width + 1);
|
||||
try writer.writeAll("|\n");
|
||||
|
||||
for (ctx.lines, 0..) |line, idx| {
|
||||
const pad = line_num_width - digitCount(line.line_num);
|
||||
try writeRepeated(writer, ' ', pad);
|
||||
try writer.print("{d} | {s}\n", .{ line.line_num, line.text });
|
||||
|
||||
try writeRepeated(writer, ' ', line_num_width + 1);
|
||||
try writer.writeAll("| ");
|
||||
const col_start: u32 = if (idx == 0) ctx.start_col else 1;
|
||||
const col_end: u32 = if (idx == ctx.lines.len - 1)
|
||||
ctx.end_col
|
||||
else
|
||||
@as(u32, @intCast(line.text.len)) + 1;
|
||||
try writeRepeated(writer, ' ', col_start - 1);
|
||||
const caret_count = if (col_end > col_start) col_end - col_start else 1;
|
||||
try writeRepeated(writer, '^', caret_count);
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
}
|
||||
|
||||
fn digitCount(n: u32) usize {
|
||||
if (n == 0) return 1;
|
||||
var count: usize = 0;
|
||||
var v = n;
|
||||
while (v > 0) : (v /= 10) count += 1;
|
||||
return count;
|
||||
}
|
||||
|
||||
fn writeRepeated(writer: anytype, byte: u8, n: usize) !void {
|
||||
var i: usize = 0;
|
||||
while (i < n) : (i += 1) try writer.writeByte(byte);
|
||||
}
|
||||
|
||||
pub fn renderDebug(self: *const DiagnosticList) void {
|
||||
for (self.items.items) |d| {
|
||||
const level_str = switch (d.level) {
|
||||
|
||||
Reference in New Issue
Block a user