lang F0.1: extractContext utility for diagnostic renderer
Adds LineInfo, ContextLines, and extractContext(allocator, source, span) to errors.zig — a pure utility that returns the source lines covered by a span plus columns for caret rendering. Prereq for F0.2's new render path which will produce Rust-style multi-line diagnostics with code excerpts. 8 unit tests cover the boundary cases: single-line span, multi-line spans (1 and 2 newlines crossed), span on an empty line, span at end-of-file without trailing newline, empty source, and offsets beyond source.len (clamping). No render surface change yet; F0.2 wires this into a new render mode kept behind a RenderStyle flag so old gcc-style output remains available during the transition.
This commit is contained in:
109
src/errors.test.zig
Normal file
109
src/errors.test.zig
Normal file
@@ -0,0 +1,109 @@
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const errors = @import("errors.zig");
|
||||
const ast = @import("ast.zig");
|
||||
const Span = ast.Span;
|
||||
|
||||
test "extractContext: single-line span at start of file" {
|
||||
const source = "hello world\nfoo bar\n";
|
||||
var ctx = try errors.extractContext(testing.allocator, source, Span{ .start = 0, .end = 5 });
|
||||
defer ctx.deinit(testing.allocator);
|
||||
|
||||
try testing.expectEqual(@as(usize, 1), ctx.lines.len);
|
||||
try testing.expectEqual(@as(u32, 1), ctx.lines[0].line_num);
|
||||
try testing.expectEqualStrings("hello world", ctx.lines[0].text);
|
||||
try testing.expectEqual(@as(u32, 1), ctx.start_col);
|
||||
try testing.expectEqual(@as(u32, 6), ctx.end_col);
|
||||
}
|
||||
|
||||
test "extractContext: single-line span in middle of file" {
|
||||
const source = "first\nsecond line here\nthird";
|
||||
// Offsets: "first" = 0..5, '\n' = 5, "second line here" = 6..22.
|
||||
// span covers "line" at offsets 13..17.
|
||||
var ctx = try errors.extractContext(testing.allocator, source, Span{ .start = 13, .end = 17 });
|
||||
defer ctx.deinit(testing.allocator);
|
||||
|
||||
try testing.expectEqual(@as(usize, 1), ctx.lines.len);
|
||||
try testing.expectEqual(@as(u32, 2), ctx.lines[0].line_num);
|
||||
try testing.expectEqualStrings("second line here", ctx.lines[0].text);
|
||||
try testing.expectEqual(@as(u32, 8), ctx.start_col);
|
||||
try testing.expectEqual(@as(u32, 12), ctx.end_col);
|
||||
}
|
||||
|
||||
test "extractContext: multi-line span crossing one newline" {
|
||||
const source = "line1\nline2\nline3";
|
||||
// Span covers "e1\nlin" at offsets 3..9.
|
||||
var ctx = try errors.extractContext(testing.allocator, source, Span{ .start = 3, .end = 9 });
|
||||
defer ctx.deinit(testing.allocator);
|
||||
|
||||
try testing.expectEqual(@as(usize, 2), ctx.lines.len);
|
||||
try testing.expectEqual(@as(u32, 1), ctx.lines[0].line_num);
|
||||
try testing.expectEqualStrings("line1", ctx.lines[0].text);
|
||||
try testing.expectEqual(@as(u32, 2), ctx.lines[1].line_num);
|
||||
try testing.expectEqualStrings("line2", ctx.lines[1].text);
|
||||
try testing.expectEqual(@as(u32, 4), ctx.start_col);
|
||||
try testing.expectEqual(@as(u32, 4), ctx.end_col);
|
||||
}
|
||||
|
||||
test "extractContext: multi-line span crossing two newlines" {
|
||||
const source = "line1\nline2\nline3\nline4";
|
||||
// Span covers offsets 3..14: "e1\nline2\nli".
|
||||
var ctx = try errors.extractContext(testing.allocator, source, Span{ .start = 3, .end = 14 });
|
||||
defer ctx.deinit(testing.allocator);
|
||||
|
||||
try testing.expectEqual(@as(usize, 3), ctx.lines.len);
|
||||
try testing.expectEqualStrings("line1", ctx.lines[0].text);
|
||||
try testing.expectEqualStrings("line2", ctx.lines[1].text);
|
||||
try testing.expectEqualStrings("line3", ctx.lines[2].text);
|
||||
try testing.expectEqual(@as(u32, 4), ctx.start_col);
|
||||
try testing.expectEqual(@as(u32, 3), ctx.end_col);
|
||||
}
|
||||
|
||||
test "extractContext: span on empty line" {
|
||||
const source = "before\n\nafter";
|
||||
// Empty middle line at offset 7 (after the first '\n').
|
||||
var ctx = try errors.extractContext(testing.allocator, source, Span{ .start = 7, .end = 7 });
|
||||
defer ctx.deinit(testing.allocator);
|
||||
|
||||
try testing.expectEqual(@as(usize, 1), ctx.lines.len);
|
||||
try testing.expectEqual(@as(u32, 2), ctx.lines[0].line_num);
|
||||
try testing.expectEqualStrings("", ctx.lines[0].text);
|
||||
try testing.expectEqual(@as(u32, 1), ctx.start_col);
|
||||
try testing.expectEqual(@as(u32, 1), ctx.end_col);
|
||||
}
|
||||
|
||||
test "extractContext: span at end of file (no trailing newline)" {
|
||||
const source = "foo\nbar";
|
||||
// Span covers "ar" at offsets 5..7.
|
||||
var ctx = try errors.extractContext(testing.allocator, source, Span{ .start = 5, .end = 7 });
|
||||
defer ctx.deinit(testing.allocator);
|
||||
|
||||
try testing.expectEqual(@as(usize, 1), ctx.lines.len);
|
||||
try testing.expectEqual(@as(u32, 2), ctx.lines[0].line_num);
|
||||
try testing.expectEqualStrings("bar", ctx.lines[0].text);
|
||||
try testing.expectEqual(@as(u32, 2), ctx.start_col);
|
||||
try testing.expectEqual(@as(u32, 4), ctx.end_col);
|
||||
}
|
||||
|
||||
test "extractContext: empty source" {
|
||||
const source = "";
|
||||
var ctx = try errors.extractContext(testing.allocator, source, Span{ .start = 0, .end = 0 });
|
||||
defer ctx.deinit(testing.allocator);
|
||||
|
||||
try testing.expectEqual(@as(usize, 1), ctx.lines.len);
|
||||
try testing.expectEqual(@as(u32, 1), ctx.lines[0].line_num);
|
||||
try testing.expectEqualStrings("", ctx.lines[0].text);
|
||||
try testing.expectEqual(@as(u32, 1), ctx.start_col);
|
||||
try testing.expectEqual(@as(u32, 1), ctx.end_col);
|
||||
}
|
||||
|
||||
test "extractContext: span beyond source length is clamped" {
|
||||
const source = "foo";
|
||||
var ctx = try errors.extractContext(testing.allocator, source, Span{ .start = 10, .end = 20 });
|
||||
defer ctx.deinit(testing.allocator);
|
||||
|
||||
try testing.expectEqual(@as(usize, 1), ctx.lines.len);
|
||||
try testing.expectEqualStrings("foo", ctx.lines[0].text);
|
||||
try testing.expectEqual(@as(u32, 4), ctx.start_col);
|
||||
try testing.expectEqual(@as(u32, 4), ctx.end_col);
|
||||
}
|
||||
@@ -27,6 +27,72 @@ pub const SourceLoc = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub const LineInfo = struct {
|
||||
line_num: u32,
|
||||
text: []const u8,
|
||||
};
|
||||
|
||||
pub const ContextLines = struct {
|
||||
lines: []LineInfo,
|
||||
start_col: u32,
|
||||
end_col: u32,
|
||||
|
||||
pub fn deinit(self: *ContextLines, allocator: std.mem.Allocator) void {
|
||||
allocator.free(self.lines);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn extractContext(
|
||||
allocator: std.mem.Allocator,
|
||||
source: []const u8,
|
||||
span: Span,
|
||||
) !ContextLines {
|
||||
const source_len: u32 = @intCast(source.len);
|
||||
const start = @min(span.start, source_len);
|
||||
const end = @max(start, @min(span.end, source_len));
|
||||
|
||||
var line_num: u32 = 1;
|
||||
var line_start: u32 = 0;
|
||||
var i: u32 = 0;
|
||||
while (i < start) : (i += 1) {
|
||||
if (source[i] == '\n') {
|
||||
line_num += 1;
|
||||
line_start = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
const start_col = (start - line_start) + 1;
|
||||
|
||||
var lines: std.ArrayList(LineInfo) = .empty;
|
||||
defer lines.deinit(allocator);
|
||||
|
||||
var cur_line_num = line_num;
|
||||
var cur_line_start = line_start;
|
||||
|
||||
while (true) {
|
||||
var cur_line_end = cur_line_start;
|
||||
while (cur_line_end < source_len and source[cur_line_end] != '\n') : (cur_line_end += 1) {}
|
||||
|
||||
try lines.append(allocator, .{
|
||||
.line_num = cur_line_num,
|
||||
.text = source[cur_line_start..cur_line_end],
|
||||
});
|
||||
|
||||
if (end <= cur_line_end) {
|
||||
const end_col = (end - cur_line_start) + 1;
|
||||
const owned = try lines.toOwnedSlice(allocator);
|
||||
return ContextLines{
|
||||
.lines = owned,
|
||||
.start_col = start_col,
|
||||
.end_col = end_col,
|
||||
};
|
||||
}
|
||||
|
||||
cur_line_num += 1;
|
||||
cur_line_start = cur_line_end + 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub const Diagnostic = struct {
|
||||
level: Level,
|
||||
message: []const u8,
|
||||
|
||||
@@ -7,6 +7,7 @@ pub const types = @import("types.zig");
|
||||
pub const target = @import("target.zig");
|
||||
pub const builtins = @import("builtins.zig");
|
||||
pub const errors = @import("errors.zig");
|
||||
pub const errors_tests = @import("errors.test.zig");
|
||||
pub const sema = @import("sema.zig");
|
||||
pub const imports = @import("imports.zig");
|
||||
pub const core = @import("core.zig");
|
||||
|
||||
Reference in New Issue
Block a user