Files
spl/stage2/utils/diag.spl
T
2026-09-17 22:32:36 +08:00

244 lines
6.7 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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();
}