Feat: add structured diagnostics with yansi colors

Introduces fluxc/src/diagnostics.rs with Level (Critical, Error,
Warning, Note), Label (Primary/Secondary with optional message), and
Diagnostic types. Diagnostic::render(src, filename) produces
rustc-style output: colored header, --> file:line:col pointer, source
line with gutter, and ^ / - underlines aligned to the offending span.

Replaces the flat ParseError struct in the parser; all five error
sites now emit Diagnostic values with source-pointing labels.
This commit is contained in:
2026-03-10 18:42:40 +01:00
parent a82b7e4633
commit 5bf4a494cb
7 changed files with 365 additions and 41 deletions

View File

@@ -1,29 +1,14 @@
use std::fmt;
use crate::{
ast::{
BinaryOp, Block, CompoundAssignOp, ElseBranch, Expr, ExprKind, FieldDef, FuncDef, Param,
Program, Stmt, StmtKind, StructDef, StructField, TopLevelDef, TopLevelDefKind, Type,
UnaryOp,
},
diagnostics::{Diagnostic, Label},
lexer::Lexer,
token::{Span, Token, TokenKind},
};
// ── Parse error ───────────────────────────────────────────────────────────────
#[derive(Debug, Clone)]
pub struct ParseError {
pub span: Span,
pub message: String,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "error at {}: {}", self.span, self.message)
}
}
// ── Binding powers ─────────────────────────────────────────────────────────────
//
// Returns `(left_bp, right_bp)` for infix operators.
@@ -139,7 +124,7 @@ fn token_to_binary_op(kind: TokenKind) -> BinaryOp {
pub struct Parser<'src> {
tokens: Vec<Token<'src>>,
pos: usize,
pub errors: Vec<ParseError>,
pub errors: Vec<Diagnostic>,
}
impl<'src> Parser<'src> {
@@ -175,11 +160,12 @@ impl<'src> Parser<'src> {
if tok.kind == kind {
self.advance()
} else {
self.errors.push(
Diagnostic::error(format!("expected {kind}, found {}", tok.kind)).with_label(
Label::primary(tok.span).with_message(format!("expected {kind} here")),
),
);
let span = Span::new(tok.span.start, tok.span.start);
self.errors.push(ParseError {
span,
message: format!("expected {}, found {}", kind, tok.kind),
});
Token::new(kind, span, "")
}
}
@@ -261,10 +247,10 @@ impl<'src> Parser<'src> {
// Error — insert recovery placeholder
_ => {
self.errors.push(ParseError {
span: tok.span,
message: format!("expected type, found {}", tok.kind),
});
self.errors.push(
Diagnostic::error(format!("expected type, found {}", tok.kind))
.with_label(Label::primary(tok.span).with_message("expected a type here")),
);
Type::Error
}
}
@@ -339,10 +325,10 @@ impl<'src> Parser<'src> {
| TokenKind::Fn
| TokenKind::Struct
| TokenKind::Eof => {
self.errors.push(ParseError {
span: tok.span,
message: format!("unexpected {} in statement position", tok.kind),
});
self.errors.push(
Diagnostic::error(format!("unexpected {} in statement position", tok.kind))
.with_label(Label::primary(tok.span)),
);
self.synchronize();
Stmt {
kind: StmtKind::Error,
@@ -553,10 +539,10 @@ impl<'src> Parser<'src> {
// Error recovery
_ => {
self.errors.push(ParseError {
span: tok.span,
message: format!("unexpected token {} in expression", tok.kind),
});
self.errors.push(
Diagnostic::error(format!("unexpected token {} in expression", tok.kind))
.with_label(Label::primary(tok.span)),
);
Expr::new(ExprKind::Error, tok.span)
}
}
@@ -730,10 +716,12 @@ impl<'src> Parser<'src> {
TokenKind::Fn => self.parse_func_def(),
TokenKind::Struct => self.parse_struct_def(),
_ => {
self.errors.push(ParseError {
span: tok.span,
message: format!("expected `fn` or `struct`, found {}", tok.kind),
});
self.errors.push(
Diagnostic::error(format!("expected `fn` or `struct`, found {}", tok.kind))
.with_label(
Label::primary(tok.span).with_message("expected `fn` or `struct`"),
),
);
self.synchronize_top_level();
TopLevelDef {
kind: TopLevelDefKind::Error,