// Expect retcode = 0 // 丰富诊断:Severity / 稳定 E 码 / Span / Label / Child,渲染为人类可读或 JSON。 // 位置只存下标,渲染时经 Sources 解析行列;条目拥有其字符串与 labels/children。 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; } 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(); }