792 lines
27 KiB
Plaintext
792 lines
27 KiB
Plaintext
// 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();
|
||
}
|