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:
agra
2026-05-28 20:32:53 +03:00
parent e347f59e50
commit cc08f9a9fe
2 changed files with 222 additions and 0 deletions

View File

@@ -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);
}