Nova's error reporting system aims to provide Rust-quality diagnostics with rich context, multi-span highlighting, and helpful suggestions. Beautiful errors are not just nice to have— they're essential for developer productivity and AI-assisted debugging.
Three mature Rust libraries dominate the diagnostic space. Each has different tradeoffs for Nova's needs.
| Library | Approach | Multi-file | Color Control | Dependencies |
|---|---|---|---|---|
| ariadne | Builder API | Excellent | ColorGenerator | Minimal |
| miette | Derive macro | Good | Auto + NO_COLOR | More (fancy feature) |
| codespan-reporting | File database | Primary/Secondary | termcolor | Minimal |
Sister project of Chumsky parser (which we may use). Best-in-class visual output with automatic label overlap handling. The ColorGenerator ensures distinct colors for each diagnostic element without manual color management.
use ariadne::{Color, ColorGenerator, Fmt, Label, Report, ReportKind, Source};
fn report_type_mismatch(
source: &str,
filename: &str,
expected_span: Range<usize>,
found_span: Range<usize>,
expected_ty: &str,
found_ty: &str,
) {
let mut colors = ColorGenerator::new();
let expected_color = colors.next();
let found_color = colors.next();
Report::build(ReportKind::Error, (filename, found_span.clone()))
.with_code("E0308")
.with_message("incompatible types")
.with_label(
Label::new((filename, expected_span))
.with_message(format!("expected type {}", expected_ty.fg(expected_color)))
.with_color(expected_color),
)
.with_label(
Label::new((filename, found_span))
.with_message(format!("found type {}", found_ty.fg(found_color)))
.with_color(found_color),
)
.with_note(format!(
"expected {}, found {}",
expected_ty.fg(expected_color),
found_ty.fg(found_color)
))
.finish()
.print((filename, Source::from(source)))
.unwrap();
}
// ariadne handles multi-file diagnostics elegantly
Report::build(ReportKind::Error, ("main.nova", 10..20))
.with_label(
Label::new(("utils.nova", 50..60))
.with_message("function defined here")
.with_color(Color::Blue),
)
.with_label(
Label::new(("main.nova", 10..20))
.with_message("called with wrong argument type")
.with_color(Color::Red),
)
.finish()
.print(sources) // Cache<(filename, Source)>
.unwrap();
If Nova's errors are defined as Rust types (not just dynamic strings), miette's derive
macro provides excellent ergonomics. Integrates well with thiserror.
use miette::{Diagnostic, NamedSource, SourceSpan};
use thiserror::Error;
#[derive(Error, Debug, Diagnostic)]
#[error("type mismatch")]
#[diagnostic(
code(nova::type_error::E0308),
url(docsrs),
help("consider adding a type annotation or cast")
)]
struct TypeMismatch {
// The source code being diagnosed
#[source_code]
src: NamedSource<String>,
// Primary label at the error location
#[label("expected {expected}, found {found}")]
span: SourceSpan,
// Additional context
expected: String,
found: String,
// Optional secondary label
#[label("type declared here")]
declaration: Option<SourceSpan>,
}
// Usage
fn check_types() -> miette::Result<()> {
Err(TypeMismatch {
src: NamedSource::new("main.nova", source.to_string()),
span: (offset, length).into(),
expected: "String".to_string(),
found: "Int".to_string(),
declaration: Some((decl_offset, decl_len).into()),
})?
}
# Enable fancy output only in the binary crate
[dependencies]
miette = { version = "7.6", features = ["fancy"] }
thiserror = "2.0"
Minimal dependencies, battle-tested in production compilers. File database pattern separates source management from diagnostic rendering. Inspired rustc's own diagnostics.
use codespan_reporting::diagnostic::{Diagnostic, Label};
use codespan_reporting::files::SimpleFiles;
use codespan_reporting::term::termcolor::{ColorChoice, StandardStream};
use codespan_reporting::term;
fn compile(sources: Vec<(&str, &str)>) -> Result<(), ()> {
// Build file database
let mut files = SimpleFiles::new();
for (name, content) in sources {
files.add(name, content);
}
// Create diagnostic with primary and secondary labels
let diagnostic = Diagnostic::error()
.with_message("`case` clauses have incompatible types")
.with_code("E0308")
.with_labels(vec![
Label::primary(file_id, 328..331)
.with_message("expected `String`, found `Nat`"),
Label::secondary(file_id, 186..192)
.with_message("expected type `String` found here"),
Label::secondary(file_id, 258..268)
.with_message("this is of type `String`"),
])
.with_notes(vec![
"expected type `String`\n found type `Nat`".to_string(),
]);
// Render to stderr
let writer = StandardStream::stderr(ColorChoice::Always);
let config = term::Config::default();
term::emit(&mut writer.lock(), &config, &files, &diagnostic)?;
Err(())
}
/// All Nova compiler errors
pub enum NovaError {
// Lexer errors
UnterminatedString { span: Span },
InvalidNumber { span: Span, reason: String },
UnexpectedCharacter { span: Span, ch: char },
// Parser errors
UnexpectedToken { span: Span, expected: Vec<TokenKind>, found: TokenKind },
MissingClosingDelimiter { open_span: Span, expected: char },
// Type errors
TypeMismatch { span: Span, expected: Type, found: Type, context: Option<Span> },
UndefinedVariable { span: Span, name: String, suggestions: Vec<String> },
// Verification errors (Nova-specific)
VerificationFailed { span: Span, property: String, counterexample: Option<String> },
UnsatisfiableContract { span: Span, contract: String },
}
/// Source location tracking
#[derive(Clone, Copy, Debug)]
pub struct Span {
pub file: FileId,
pub start: u32,
pub end: u32,
}
impl Span {
pub fn merge(self, other: Span) -> Span {
debug_assert_eq!(self.file, other.file);
Span {
file: self.file,
start: self.start.min(other.start),
end: self.end.max(other.end),
}
}
}
/// Newtype for file identifiers
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct FileId(u32);
impl NovaError {
pub fn render(&self, sources: &SourceCache) {
use ariadne::{Report, ReportKind, Label, Color};
match self {
NovaError::TypeMismatch { span, expected, found, context } => {
let mut report = Report::build(ReportKind::Error, span.file, span.start)
.with_code("E0308")
.with_message("type mismatch")
.with_label(
Label::new((span.file, span.start..span.end))
.with_message(format!("expected `{}`, found `{}`", expected, found))
.with_color(Color::Red),
);
if let Some(ctx) = context {
report = report.with_label(
Label::new((ctx.file, ctx.start..ctx.end))
.with_message("type expected due to this")
.with_color(Color::Blue),
);
}
report.finish().print(sources).unwrap();
}
NovaError::UndefinedVariable { span, name, suggestions } => {
let mut report = Report::build(ReportKind::Error, span.file, span.start)
.with_code("E0425")
.with_message(format!("undefined variable `{}`", name))
.with_label(
Label::new((span.file, span.start..span.end))
.with_message("not found in this scope")
.with_color(Color::Red),
);
if !suggestions.is_empty() {
report = report.with_help(format!(
"did you mean: {}?",
suggestions.join(", ")
));
}
report.finish().print(sources).unwrap();
}
// ... other error variants
}
}
}
For AI-assisted debugging, Nova errors include structured JSON output alongside human-readable messages. This enables LLMs to understand and fix errors programmatically.
impl NovaError {
pub fn to_json(&self) -> serde_json::Value {
json!({
"code": self.code(),
"severity": "error",
"message": self.message(),
"spans": self.spans().iter().map(|s| {
json!({
"file": s.file.name(),
"start": { "line": s.start_line, "column": s.start_col },
"end": { "line": s.end_line, "column": s.end_col },
"label": s.label,
"primary": s.is_primary,
})
}).collect::<Vec<_>>(),
"suggestions": self.suggestions(),
"help": self.help(),
})
}
}
Use ariadne for error rendering. It's a sister project of Chumsky (potential parser), has the best visual output, and handles label overlap automatically. The ColorGenerator ensures distinct colors without manual management.
Additionally, implement JSON error output for AI integration. Nova's value proposition includes AI-assisted development, and structured errors enable LLMs to understand and fix issues automatically.
NovaError enum with all error variantsSpan tracking throughout lexer/parser/type-checkerrender() method using ariadne for human-readable outputto_json() method for AI/LSP integration