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

@@ -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) {