// Expect retcode = 0 // 诊断收集与渲染:severity + 文件/行/列 + 消息;统计错误/警告数。 // 消息与文件名按拥有型 Str 保存(深拷贝),Diag 拥有它们直到 clear/drop。 // 渲染输出到 ByteBuffer;不直接写文件,便于 LSP/测试。 type .{ .Error, .Status, .make } = @import("error.spl"); type .{ .Vec } = @import("vec.spl"); type .{ .ByteBuffer } = @import("byte_buffer.spl"); type .{ .Str } = @import("str.spl"); type .{ .append_usize } = @import("fmt.spl"); type Severity = enum { Debug, Info, Warning, Error, Fatal, } type DiagItem = struct { severity: Severity, line: usize, col: usize, fname: Str, message: Str, } type Diag = @self; items: Vec, // 元素类型 DiagItem errors: usize, warnings: usize, fn new(): Diag { ret Diag { .items = Vec.blank(@sizeof(DiagItem), 8), .errors = 0, .warnings = 0, }; } fn item_at(self: *Diag, index: usize): *DiagItem { match Vec.get(&self.items, index) { .Ok(ptr) => { ret ptr as *DiagItem; }, .Err(error) => { ret null; }, } ret null; } fn count(self: Diag): usize { ret self.items.len; } fn error_count(self: Diag): usize { ret self.errors; } fn warning_count(self: Diag): usize { ret self.warnings; } fn has_error(self: Diag): bool { ret self.errors > 0; } // 添加一条诊断;失败不改变已有内容。 fn emit(self: *Diag, fname: []u8, line: usize, col: usize, severity: Severity, msg: []u8): Status { var item = DiagItem { .severity = severity, .line = line, .col = col, .fname = Str.new(), .message = Str.new(), }; match Str.from_slice(fname) { .Ok(v) => { item.fname = v; }, .Err(error) => { ret .Err(error); }, } match Str.from_slice(msg) { .Ok(v) => { item.message = v; }, .Err(error) => { Str.drop(&item.fname); ret .Err(error); }, } match Vec.push(&self.items, &item as *u8) { .Ok => {}, .Err(error) => { Str.drop(&item.fname); Str.drop(&item.message); ret .Err(error); }, } match severity { .Error => { self.errors = self.errors + 1; }, .Fatal => { self.errors = self.errors + 1; }, .Warning => { self.warnings = self.warnings + 1; }, _ => {}, } ret .Ok; } fn err(self: *Diag, fname: []u8, line: usize, col: usize, msg: []u8): Status { ret emit(self, fname, line, col, .Error, msg); } fn warn(self: *Diag, fname: []u8, line: usize, col: usize, msg: []u8): Status { ret emit(self, fname, line, col, .Warning, msg); } fn info(self: *Diag, fname: []u8, line: usize, col: usize, msg: []u8): Status { ret emit(self, fname, line, col, .Info, msg); } fn level_name(severity: Severity): []u8 { match severity { .Debug => { ret "debug"; }, .Info => { ret "info"; }, .Warning => { ret "warning"; }, .Error => { ret "error"; }, .Fatal => { ret "fatal"; }, } ret "?"; } fn append_str(out: *ByteBuffer, s: *Str): Status { ret ByteBuffer.append(out, Str.as_slice(s)); } // 渲染为 "file:line:col: level: message\n"(file 为空时省略前缀)。 fn render(self: *Diag, out: *ByteBuffer): Status { var i: usize = 0; while i < self.items.len { var it = item_at(self, i); if it == null { ret .Err(make(.OutOfRange)); } if Str.size(it.fname) != 0 { match append_str(out, &it.fname) { .Ok => {}, .Err(e) => { ret .Err(e); } } match ByteBuffer.append_byte(out, ':') { .Ok => {}, .Err(e) => { ret .Err(e); } } } match append_usize(out, it.line, 10, false) { .Ok => {}, .Err(e) => { ret .Err(e); } } match ByteBuffer.append_byte(out, ':') { .Ok => {}, .Err(e) => { ret .Err(e); } } match append_usize(out, it.col, 10, false) { .Ok => {}, .Err(e) => { ret .Err(e); } } match ByteBuffer.append(out, ": ") { .Ok => {}, .Err(e) => { ret .Err(e); } } match ByteBuffer.append(out, level_name(it.severity)) { .Ok => {}, .Err(e) => { ret .Err(e); } } match ByteBuffer.append(out, ": ") { .Ok => {}, .Err(e) => { ret .Err(e); } } match append_str(out, &it.message) { .Ok => {}, .Err(e) => { ret .Err(e); } } match ByteBuffer.append_byte(out, '\n') { .Ok => {}, .Err(e) => { ret .Err(e); } } i = i + 1; } ret .Ok; } // 丢弃所有条目内容但保留缓冲区。 fn clear(self: *Diag) { var i: usize = 0; while i < self.items.len { var it = item_at(self, i); if it != null { Str.drop(&it.fname); Str.drop(&it.message); } i = i + 1; } Vec.clear(&self.items); self.errors = 0; self.warnings = 0; } fn drop(self: *Diag) { var i: usize = 0; while i < self.items.len { var it = item_at(self, i); if it != null { Str.drop(&it.fname); Str.drop(&it.message); } i = i + 1; } Vec.drop(&self.items); self.errors = 0; self.warnings = 0; } // ---- 测试辅助 ---- fn expect_ok(result: Status) { match result { .Ok => {}, .Err(error) => { @assert(false, "unexpected diag error"); }, } } fn expect_bytes(out: *ByteBuffer, expected: []u8) { var actual = ByteBuffer.as_slice(out.*); @assert(actual.len == expected.len, "render length"); var i: usize = 0; while i < expected.len { @assert(actual.ptr[i] == expected.ptr[i], "render byte"); i = i + 1; } } fn test_collect_and_count() { var d = new(); @assert(count(d) == 0 && !has_error(d), "empty"); expect_ok(err(&d, "a.spl", 3, 5, "undefined identifier `x`")); expect_ok(warn(&d, "a.spl", 7, 1, "unused variable")); expect_ok(info(&d, "a.spl", 9, 2, "note")); expect_ok(err(&d, "", 1, 1, "fatal-ish")); @assert(count(d) == 4, "count"); @assert(error_count(d) == 2, "errors"); @assert(warning_count(d) == 1, "warnings"); @assert(has_error(d), "has_error"); var out = ByteBuffer.new(); expect_ok(render(&d, &out)); expect_bytes(&out, "a.spl:3:5: error: undefined identifier `x`\na.spl:7:1: warning: unused variable\na.spl:9:2: info: note\n1:1: error: fatal-ish\n"); ByteBuffer.drop(&out); drop(&d); } fn test_clear_and_reuse() { var d = new(); expect_ok(err(&d, "f", 1, 1, "one")); expect_ok(warn(&d, "f", 2, 2, "two")); clear(&d); @assert(count(d) == 0 && !has_error(d) && warning_count(d) == 0, "cleared"); expect_ok(err(&d, "f", 3, 3, "three")); @assert(count(d) == 1 && error_count(d) == 1, "reuse"); drop(&d); } fn main() { test_collect_and_count(); test_clear_and_reuse(); }