Files
spl/stage2/diag.spl
T

792 lines
27 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
// 丰富诊断(参考 rustc / Clang / Swift 的设计,不照搬 stage1 C 实现):
// Severity Note/Help/Warning/Error/Fatal
// Code 固定语义枚举 + 稳定 E 码;集中管理,利于 diff
// Span 字节区间 {file,start,end};file 是 Sources 的下标,NO_FILE 表示无位置
// Label 主/次标签(span + 文本),可多个
// Child 子诊断(note/help,可带 span),挂在条目上
// 渲染:人类可读(源码行 + caret 下划线)与 JSON(字段稳定)双格式。
// 位置只存下标,渲染时经 Sources 解析行列/取行,避免复制文件名。
// 消息按拥有型 Str 保存;条目拥有其 labels/children;clear/drop 释放全部内部资源。
type .{ .Error, .Status, .make } = @import("./utils/error.spl");
type .{ .Vec } = @import("./utils/vec.spl");
type .{ .ByteBuffer } = @import("./utils/byte_buffer.spl");
type .{ .Str } = @import("./utils/str.spl");
type .{ .ByteView } = @import("./utils/byte_view.spl");
type .{ .append_usize } = @import("./utils/fmt.spl");
type .{ .Sources, .Source, .set_new, .set_add, .set_at, .set_drop } = @import("./utils/source.spl");
type Severity = enum {
Note,
Help,
Warning,
Error,
Fatal,
}
type Stage = enum {
None,
Lex,
Parse,
Sema,
Lower,
Vm,
Driver,
}
type Code = enum {
None,
// 词法 / 语法
LexUnexpected,
ParseExpected,
ParseUnrecognized,
// sema
UndefinedSymbol,
DuplicateSymbol,
TypeMismatch,
ArgCountMismatch,
NotAFunction,
NoField,
NoMethod,
IndexBaseInvalid,
TupleIndexOob,
FitsOverflow,
CircularDep,
BreakOutsideLoop,
ContinueOutsideLoop,
UndefinedLabel,
CastInvalid,
UnknownAttr,
UnknownIntrin,
IntrinArgCount,
VarUninitialized,
CannotInfer,
MustReturnValue,
// lowering
UnhandledNode,
ImplicitCastFail,
NoLvalue,
BadFieldIndex,
BadBlock,
// vm
IrUnknownKind,
FrameOverflow,
// 通用
Internal,
}
// file 下标不可用时的哨兵。
fn no_file(): usize {
ret ~(0 as usize);
}
type Span = struct {
file: usize,
start: usize,
end: usize,
}
fn span_none(): Span {
ret Span { .file = no_file(), .start = 0, .end = 0 };
}
fn span_make(file: usize, start: usize, end: usize): Span {
ret Span { .file = file, .start = start, .end = end };
}
fn span_is_none(span: Span): bool {
ret span.file == no_file();
}
type Label = struct {
span: Span,
message: Str,
primary: bool,
}
type Child = struct {
severity: Severity,
span: Span,
message: Str,
}
type Item = struct {
severity: Severity,
stage: Stage,
code: Code,
span: Span,
message: Str,
labels: Vec, // Label
children: Vec, // Child
}
type Diag = @self;
items: Vec, // Item
sources: *Sources, // 借用;可为 null(则只输出消息)
errors: usize,
warnings: usize,
fatals: usize,
fn new(sources: *Sources): Diag {
ret Diag {
.items = Vec.blank(@sizeof(Item), @alignof(Item)),
.sources = sources,
.errors = 0,
.warnings = 0,
.fatals = 0,
};
}
fn item_at(self: *Diag, index: usize): *Item {
match Vec.get(&self.items, index) {
.Ok(ptr) => { ret ptr as *Item; },
.Err(error) => { ret null; },
}
ret null;
}
fn label_at(it: *Item, index: usize): *Label {
match Vec.get(&it.labels, index) {
.Ok(ptr) => { ret ptr as *Label; },
.Err(error) => { ret null; },
}
ret null;
}
fn child_at(it: *Item, index: usize): *Child {
match Vec.get(&it.children, index) {
.Ok(ptr) => { ret ptr as *Child; },
.Err(error) => { ret null; },
}
ret null;
}
fn last(self: *Diag): usize {
if self.items.len == 0 { ret no_file(); }
ret self.items.len - 1;
}
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 fatal_count(self: Diag): usize { ret self.fatals; }
fn has_error(self: Diag): bool { ret self.errors > 0; }
fn has_fatal(self: Diag): bool { ret self.fatals > 0; }
// ---- 采集 ----
// 追加一条诊断,返回其下标(失败返回 no_file())。span 为主位置。
fn emit(self: *Diag, severity: Severity, stage: Stage, code: Code, span: Span,
msg: []u8): usize {
var message = Str.new();
match Str.from_slice(msg) {
.Ok(v) => { message = v; },
.Err(error) => { ret no_file(); },
}
var item = Item {
.severity = severity,
.stage = stage,
.code = code,
.span = span,
.message = message,
.labels = Vec.blank(@sizeof(Label), @alignof(Label)),
.children = Vec.blank(@sizeof(Child), @alignof(Child)),
};
match Vec.push(&self.items, &item as *u8) {
.Ok => {},
.Err(error) => { Str.drop(&item.message); ret no_file(); },
}
match severity {
.Error => { self.errors = self.errors + 1; },
.Fatal => { self.errors = self.errors + 1; self.fatals = self.fatals + 1; },
.Warning => { self.warnings = self.warnings + 1; },
_ => {},
}
ret self.items.len - 1;
}
fn err(self: *Diag, span: Span, code: Code, msg: []u8): usize {
ret emit(self, .Error, .None, code, span, msg);
}
fn warn(self: *Diag, span: Span, code: Code, msg: []u8): usize {
ret emit(self, .Warning, .None, code, span, msg);
}
fn note(self: *Diag, span: Span, code: Code, msg: []u8): usize {
ret emit(self, .Note, .None, code, span, msg);
}
// 给最近一条诊断附加标签(primary 决定主/次)。
fn attach_label(self: *Diag, span: Span, msg: []u8, primary: bool): Status {
var idx = last(self);
if idx == no_file() { ret .Err(make(.OutOfRange)); }
var it = item_at(self, idx);
if it == null { ret .Err(make(.OutOfRange)); }
var message = Str.new();
match Str.from_slice(msg) {
.Ok(v) => { message = v; },
.Err(error) => { ret .Err(error); },
}
var lab = Label { .span = span, .message = message, .primary = primary };
match Vec.push(&it.labels, &lab as *u8) {
.Ok => {},
.Err(error) => { Str.drop(&lab.message); ret .Err(error); },
}
ret .Ok;
}
// 给最近一条诊断附加子诊断(note/help 等)。
fn attach_child(self: *Diag, severity: Severity, span: Span, msg: []u8): Status {
var idx = last(self);
if idx == no_file() { ret .Err(make(.OutOfRange)); }
var it = item_at(self, idx);
if it == null { ret .Err(make(.OutOfRange)); }
var message = Str.new();
match Str.from_slice(msg) {
.Ok(v) => { message = v; },
.Err(error) => { ret .Err(error); },
}
var ch = Child { .severity = severity, .span = span, .message = message };
match Vec.push(&it.children, &ch as *u8) {
.Ok => {},
.Err(error) => { Str.drop(&ch.message); ret .Err(error); },
}
ret .Ok;
}
// ---- 名称 ----
fn severity_name(severity: Severity): []u8 {
match severity {
.Note => { ret "note"; },
.Help => { ret "help"; },
.Warning => { ret "warning"; },
.Error => { ret "error"; },
.Fatal => { ret "fatal"; },
}
ret "?";
}
fn stage_name(stage: Stage): []u8 {
match stage {
.None => { ret "none"; },
.Lex => { ret "lex"; },
.Parse => { ret "parse"; },
.Sema => { ret "sema"; },
.Lower => { ret "lower"; },
.Vm => { ret "vm"; },
.Driver => { ret "driver"; },
}
ret "?";
}
// 稳定 E 码(diff 依赖;新增 Code 时分配新码,勿改旧值)。
fn code_str(code: Code): []u8 {
match code {
.None => { ret "E0000"; },
.LexUnexpected => { ret "E0001"; },
.ParseExpected => { ret "E0002"; },
.ParseUnrecognized => { ret "E0003"; },
.UndefinedSymbol => { ret "E0101"; },
.DuplicateSymbol => { ret "E0102"; },
.TypeMismatch => { ret "E0103"; },
.ArgCountMismatch => { ret "E0104"; },
.NotAFunction => { ret "E0105"; },
.NoField => { ret "E0106"; },
.NoMethod => { ret "E0107"; },
.IndexBaseInvalid => { ret "E0108"; },
.TupleIndexOob => { ret "E0109"; },
.FitsOverflow => { ret "E0110"; },
.CircularDep => { ret "E0111"; },
.BreakOutsideLoop => { ret "E0112"; },
.ContinueOutsideLoop => { ret "E0113"; },
.UndefinedLabel => { ret "E0114"; },
.CastInvalid => { ret "E0115"; },
.UnknownAttr => { ret "E0116"; },
.UnknownIntrin => { ret "E0117"; },
.IntrinArgCount => { ret "E0118"; },
.VarUninitialized => { ret "E0119"; },
.CannotInfer => { ret "E0120"; },
.MustReturnValue => { ret "E0121"; },
.UnhandledNode => { ret "E0201"; },
.ImplicitCastFail => { ret "E0202"; },
.NoLvalue => { ret "E0203"; },
.BadFieldIndex => { ret "E0204"; },
.BadBlock => { ret "E0205"; },
.IrUnknownKind => { ret "E0301"; },
.FrameOverflow => { ret "E0302"; },
.Internal => { ret "E9000"; },
}
ret "E0000";
}
fn code_name(code: Code): []u8 {
match code {
.None => { ret "none"; },
.LexUnexpected => { ret "lex_unexpected"; },
.ParseExpected => { ret "parse_expected"; },
.ParseUnrecognized => { ret "parse_unrecognized"; },
.UndefinedSymbol => { ret "undefined_symbol"; },
.DuplicateSymbol => { ret "duplicate_symbol"; },
.TypeMismatch => { ret "type_mismatch"; },
.ArgCountMismatch => { ret "arg_count_mismatch"; },
.NotAFunction => { ret "not_a_function"; },
.NoField => { ret "no_field"; },
.NoMethod => { ret "no_method"; },
.IndexBaseInvalid => { ret "index_base_invalid"; },
.TupleIndexOob => { ret "tuple_index_oob"; },
.FitsOverflow => { ret "fits_overflow"; },
.CircularDep => { ret "circular_dep"; },
.BreakOutsideLoop => { ret "break_outside_loop"; },
.ContinueOutsideLoop => { ret "continue_outside_loop"; },
.UndefinedLabel => { ret "undefined_label"; },
.CastInvalid => { ret "cast_invalid"; },
.UnknownAttr => { ret "unknown_attr"; },
.UnknownIntrin => { ret "unknown_intrin"; },
.IntrinArgCount => { ret "intrin_arg_count"; },
.VarUninitialized => { ret "var_uninitialized"; },
.CannotInfer => { ret "cannot_infer"; },
.MustReturnValue => { ret "must_return_value"; },
.UnhandledNode => { ret "unhandled_node"; },
.ImplicitCastFail => { ret "implicit_cast_fail"; },
.NoLvalue => { ret "no_lvalue"; },
.BadFieldIndex => { ret "bad_field_index"; },
.BadBlock => { ret "bad_block"; },
.IrUnknownKind => { ret "ir_unknown_kind"; },
.FrameOverflow => { ret "frame_overflow"; },
.Internal => { ret "internal"; },
}
ret "none";
}
// ---- 渲染:公共小工具 ----
fn append_num(out: *ByteBuffer, value: usize): Status {
ret append_usize(out, value, 10, false);
}
fn append_str(out: *ByteBuffer, s: *Str): Status {
ret ByteBuffer.append(out, Str.as_slice(s));
}
fn append_view(out: *ByteBuffer, view: ByteView): Status {
ret ByteBuffer.append(out, ByteView.as_slice(view));
}
type Resolved = struct {
ok: bool,
fname: ByteView,
line: usize,
col: usize,
text: ByteView,
width: usize,
}
fn resolve(self: *Diag, span: Span): Resolved {
var r = Resolved {
.ok = false,
.fname = ByteView.empty(),
.line = 0,
.col = 0,
.text = ByteView.empty(),
.width = 1,
};
if self.sources == null { ret r; }
if span_is_none(span) { ret r; }
var src = set_at(self.sources, span.file);
if src == null { ret r; }
var pos = Source.line_col(src.*, span.start);
var width: usize = 1;
if span.end > span.start { width = span.end - span.start; }
r.ok = true;
r.fname = src.fname;
r.line = pos.line;
r.col = pos.col;
r.text = Source.line_text(src.*, pos.line);
r.width = width;
ret r;
}
fn render_header(it: *Item, r: *Resolved, out: *ByteBuffer): Status {
if r.ok {
match append_view(out, r.fname) { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append_byte(out, ':') { .Ok => {}, .Err(e) => { ret .Err(e); } }
match append_num(out, r.line) { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append_byte(out, ':') { .Ok => {}, .Err(e) => { ret .Err(e); } }
match append_num(out, r.col) { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append(out, ": ") { .Ok => {}, .Err(e) => { ret .Err(e); } }
}
match ByteBuffer.append(out, severity_name(it.severity)) { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append_byte(out, '[') { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append(out, code_str(it.code)) { .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); } }
ret ByteBuffer.append_byte(out, '\n');
}
// 输出一行源码 + caret 下划线(列范围按字节;制表符不特殊处理)。
fn render_span_visual(self: *Diag, span: Span, out: *ByteBuffer): Status {
var r = resolve(self, span);
if !r.ok { ret .Ok; }
match ByteBuffer.append(out, " ") { .Ok => {}, .Err(e) => { ret .Err(e); } }
match append_view(out, r.text) { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append_byte(out, '\n') { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append(out, " ") { .Ok => {}, .Err(e) => { ret .Err(e); } }
var pad: usize = 0;
if r.col > 0 { pad = r.col - 1; }
match ByteBuffer.append_repeat(out, ' ', pad) { .Ok => {}, .Err(e) => { ret .Err(e); } }
var width = r.width;
if width == 0 { width = 1; }
if r.col > 0 {
var room: usize = 0;
if r.text.len >= pad { room = r.text.len - pad; }
if room == 0 { room = 1; }
if width > room { width = room; }
}
var k: usize = 0;
while k < width {
var ch: u8 = '~';
if k == 0 { ch = '^'; }
match ByteBuffer.append_byte(out, ch) { .Ok => {}, .Err(e) => { ret .Err(e); } }
k = k + 1;
}
ret ByteBuffer.append_byte(out, '\n');
}
fn render_child_line(ch: *Child, out: *ByteBuffer): Status {
match ByteBuffer.append(out, " ") { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append(out, severity_name(ch.severity)) { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append(out, ": ") { .Ok => {}, .Err(e) => { ret .Err(e); } }
match append_str(out, &ch.message) { .Ok => {}, .Err(e) => { ret .Err(e); } }
ret ByteBuffer.append_byte(out, '\n');
}
fn render_human(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)); }
var r = resolve(self, it.span);
match render_header(it, &r, out) { .Ok => {}, .Err(e) => { ret .Err(e); } }
match render_span_visual(self, it.span, out) { .Ok => {}, .Err(e) => { ret .Err(e); } }
var j: usize = 0;
while j < it.labels.len {
var lab = label_at(it, j);
if lab != null {
match render_span_visual(self, lab.span, out) { .Ok => {}, .Err(e) => { ret .Err(e); } }
if Str.size(lab.message) != 0 {
match ByteBuffer.append(out, " ") { .Ok => {}, .Err(e) => { ret .Err(e); } }
match append_str(out, &lab.message) { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append_byte(out, '\n') { .Ok => {}, .Err(e) => { ret .Err(e); } }
}
}
j = j + 1;
}
var c: usize = 0;
while c < it.children.len {
var ch = child_at(it, c);
if ch != null {
match render_child_line(ch, out) { .Ok => {}, .Err(e) => { ret .Err(e); } }
match render_span_visual(self, ch.span, out) { .Ok => {}, .Err(e) => { ret .Err(e); } }
}
c = c + 1;
}
if i + 1 < self.items.len {
match ByteBuffer.append_byte(out, '\n') { .Ok => {}, .Err(e) => { ret .Err(e); } }
}
i = i + 1;
}
ret .Ok;
}
// ---- JSON 渲染(字段稳定,供 LSP / diff 工具)----
fn append_json_hex4(out: *ByteBuffer, b: u8): Status {
var hex: []u8 = "0123456789abcdef";
match ByteBuffer.append(out, "\\u00") { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append_byte(out, hex[(b >> 4) as usize]) { .Ok => {}, .Err(e) => { ret .Err(e); } }
ret ByteBuffer.append_byte(out, hex[(b & 15) as usize]);
}
fn append_json_str(out: *ByteBuffer, bytes: []u8): Status {
match ByteBuffer.append_byte(out, '"') { .Ok => {}, .Err(e) => { ret .Err(e); } }
var i: usize = 0;
while i < bytes.len {
var b = bytes[i];
if b == '"' {
match ByteBuffer.append(out, "\\\"") { .Ok => {}, .Err(e) => { ret .Err(e); } }
} else if b == '\\' {
match ByteBuffer.append(out, "\\\\") { .Ok => {}, .Err(e) => { ret .Err(e); } }
} else if b == '\n' {
match ByteBuffer.append(out, "\\n") { .Ok => {}, .Err(e) => { ret .Err(e); } }
} else if b == '\r' {
match ByteBuffer.append(out, "\\r") { .Ok => {}, .Err(e) => { ret .Err(e); } }
} else if b == '\t' {
match ByteBuffer.append(out, "\\t") { .Ok => {}, .Err(e) => { ret .Err(e); } }
} else if b < ' ' {
match append_json_hex4(out, b) { .Ok => {}, .Err(e) => { ret .Err(e); } }
} else {
match ByteBuffer.append_byte(out, b) { .Ok => {}, .Err(e) => { ret .Err(e); } }
}
i = i + 1;
}
ret ByteBuffer.append_byte(out, '"');
}
fn append_str_json(out: *ByteBuffer, s: *Str): Status {
ret append_json_str(out, Str.as_slice(s));
}
fn append_bool(out: *ByteBuffer, value: bool): Status {
if value { ret ByteBuffer.append(out, "true"); }
ret ByteBuffer.append(out, "false");
}
fn append_loc_json(self: *Diag, span: Span, out: *ByteBuffer): Status {
var r = resolve(self, span);
match ByteBuffer.append(out, "{\"file\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
if r.ok {
match append_json_str(out, ByteView.as_slice(r.fname)) { .Ok => {}, .Err(e) => { ret .Err(e); } }
} else {
match ByteBuffer.append(out, "null") { .Ok => {}, .Err(e) => { ret .Err(e); } }
}
match ByteBuffer.append(out, ",\"start\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
match append_num(out, span.start) { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append(out, ",\"end\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
match append_num(out, span.end) { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append(out, ",\"line\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
if r.ok {
match append_num(out, r.line) { .Ok => {}, .Err(e) => { ret .Err(e); } }
} else {
match ByteBuffer.append(out, "null") { .Ok => {}, .Err(e) => { ret .Err(e); } }
}
match ByteBuffer.append(out, ",\"col\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
if r.ok {
match append_num(out, r.col) { .Ok => {}, .Err(e) => { ret .Err(e); } }
} else {
match ByteBuffer.append(out, "null") { .Ok => {}, .Err(e) => { ret .Err(e); } }
}
ret ByteBuffer.append_byte(out, '}');
}
fn render_item_json(self: *Diag, it: *Item, out: *ByteBuffer): Status {
match ByteBuffer.append(out, "{\"severity\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
match append_json_str(out, severity_name(it.severity)) { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append(out, ",\"stage\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
match append_json_str(out, stage_name(it.stage)) { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append(out, ",\"code\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
match append_json_str(out, code_str(it.code)) { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append(out, ",\"name\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
match append_json_str(out, code_name(it.code)) { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append(out, ",\"message\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
match append_str_json(out, &it.message) { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append(out, ",\"span\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
match append_loc_json(self, it.span, out) { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append(out, ",\"labels\":[") { .Ok => {}, .Err(e) => { ret .Err(e); } }
var j: usize = 0;
while j < it.labels.len {
var lab = label_at(it, j);
if lab != null {
if j != 0 {
match ByteBuffer.append_byte(out, ',') { .Ok => {}, .Err(e) => { ret .Err(e); } }
}
match ByteBuffer.append(out, "{\"message\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
match append_str_json(out, &lab.message) { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append(out, ",\"primary\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
match append_bool(out, lab.primary) { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append(out, ",\"span\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
match append_loc_json(self, lab.span, out) { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append_byte(out, '}') { .Ok => {}, .Err(e) => { ret .Err(e); } }
}
j = j + 1;
}
match ByteBuffer.append_byte(out, ']') { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append(out, ",\"children\":[") { .Ok => {}, .Err(e) => { ret .Err(e); } }
var c: usize = 0;
while c < it.children.len {
var ch = child_at(it, c);
if ch != null {
if c != 0 {
match ByteBuffer.append_byte(out, ',') { .Ok => {}, .Err(e) => { ret .Err(e); } }
}
match ByteBuffer.append(out, "{\"severity\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
match append_json_str(out, severity_name(ch.severity)) { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append(out, ",\"message\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
match append_str_json(out, &ch.message) { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append(out, ",\"span\":") { .Ok => {}, .Err(e) => { ret .Err(e); } }
match append_loc_json(self, ch.span, out) { .Ok => {}, .Err(e) => { ret .Err(e); } }
match ByteBuffer.append_byte(out, '}') { .Ok => {}, .Err(e) => { ret .Err(e); } }
}
c = c + 1;
}
match ByteBuffer.append_byte(out, ']') { .Ok => {}, .Err(e) => { ret .Err(e); } }
ret ByteBuffer.append_byte(out, '}');
}
fn render_json(self: *Diag, out: *ByteBuffer): Status {
match ByteBuffer.append_byte(out, '[') { .Ok => {}, .Err(e) => { ret .Err(e); } }
var i: usize = 0;
while i < self.items.len {
var it = item_at(self, i);
if it == null { ret .Err(make(.OutOfRange)); }
if i != 0 {
match ByteBuffer.append_byte(out, ',') { .Ok => {}, .Err(e) => { ret .Err(e); } }
}
match render_item_json(self, it, out) { .Ok => {}, .Err(e) => { ret .Err(e); } }
i = i + 1;
}
ret ByteBuffer.append_byte(out, ']');
}
// ---- 生命周期 ----
fn drop_item(it: *Item) {
Str.drop(&it.message);
var j: usize = 0;
while j < it.labels.len {
var lab = label_at(it, j);
if lab != null { Str.drop(&lab.message); }
j = j + 1;
}
Vec.drop(&it.labels);
var c: usize = 0;
while c < it.children.len {
var ch = child_at(it, c);
if ch != null { Str.drop(&ch.message); }
c = c + 1;
}
Vec.drop(&it.children);
}
fn clear(self: *Diag) {
var i: usize = 0;
while i < self.items.len {
var it = item_at(self, i);
if it != null { drop_item(it); }
i = i + 1;
}
Vec.clear(&self.items);
self.errors = 0;
self.warnings = 0;
self.fatals = 0;
}
fn drop(self: *Diag) {
clear(self);
Vec.drop(&self.items);
self.sources = null;
}
// ---- 测试辅助 ----
fn view(text: []u8): ByteView {
ret ByteView.from_slice(text);
}
fn expect_ok(result: Status) {
match result {
.Ok => {},
.Err(error) => { @assert(false, "unexpected diag error"); },
}
}
fn has_bytes(out: *ByteBuffer, needle: []u8): bool {
ret ByteView.contains(ByteView.from_slice(ByteBuffer.as_slice(out.*)), view(needle));
}
fn test_collect_render_human() {
var sources = set_new();
match set_add(&sources, view("a.spl"), view("fn main() {\n y;\n}\n")) {
.Ok(index) => {},
.Err(error) => { @assert(false, "add source"); },
}
var d = new(&sources);
var idx = err(&d, span_make(0, 16, 17), .UndefinedSymbol, "undefined symbol `y`");
@assert(idx != no_file(), "emit");
expect_ok(attach_label(&d, span_make(0, 16, 17), "not found in scope", false));
expect_ok(attach_child(&d, .Note, span_none(), "did you mean `x`?"));
@assert(count(d) == 1, "count");
@assert(error_count(d) == 1 && has_error(d), "errors");
@assert(warning_count(d) == 0 && !has_fatal(d), "warnings/fatal");
var out = ByteBuffer.new();
expect_ok(render_human(&d, &out));
@assert(has_bytes(&out, "a.spl:2:5: error[E0101]: undefined symbol `y`"), "human header");
@assert(has_bytes(&out, " y;"), "human source line");
@assert(has_bytes(&out, " ^"), "human caret");
@assert(has_bytes(&out, "not found in scope"), "human label");
@assert(has_bytes(&out, "note: did you mean `x`?"), "human child");
ByteBuffer.drop(&out);
clear(&d);
@assert(count(d) == 0 && error_count(d) == 0, "clear");
drop(&d);
set_drop(&sources);
}
fn test_render_json() {
var sources = set_new();
match set_add(&sources, view("b.spl"), view("let z = 1;\n")) {
.Ok(index) => {},
.Err(error) => { @assert(false, "add source"); },
}
var d = new(&sources);
err(&d, span_make(0, 4, 5), .TypeMismatch, "expected i32, got bool");
expect_ok(attach_child(&d, .Help, span_none(), "annotate the type"));
var out = ByteBuffer.new();
expect_ok(render_json(&d, &out));
@assert(has_bytes(&out, "\"severity\":\"error\""), "json severity");
@assert(has_bytes(&out, "\"code\":\"E0103\""), "json code");
@assert(has_bytes(&out, "\"name\":\"type_mismatch\""), "json name");
@assert(has_bytes(&out, "\"line\":1"), "json line");
@assert(has_bytes(&out, "\"col\":5"), "json col");
@assert(has_bytes(&out, "\"children\":[{\"severity\":\"help\""), "json child");
ByteBuffer.drop(&out);
drop(&d);
set_drop(&sources);
}
fn test_empty_and_headerless() {
var d = new(null);
err(&d, span_none(), .Internal, "no location");
var out = ByteBuffer.new();
expect_ok(render_human(&d, &out));
@assert(has_bytes(&out, "error[E9000]: no location"), "headerless");
ByteBuffer.drop(&out);
drop(&d);
}
fn main() {
test_collect_render_human();
test_render_json();
test_empty_and_headerless();
}