Add Elm-style error diagnostics

Implement beautiful, informative error messages inspired by Elm:
- Rich diagnostic rendering with source code snippets
- Colored output with proper underlines showing error locations
- Categorized error titles (Type Mismatch, Unknown Name, etc.)
- Contextual hints and suggestions for common errors
- Support for type errors, runtime errors, and parse errors

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 04:01:49 -05:00
parent 66132779cc
commit d37f0fb096
5 changed files with 703 additions and 2 deletions

View File

@@ -1,6 +1,7 @@
//! Lux - A functional programming language with first-class effects
mod ast;
mod diagnostics;
mod interpreter;
mod lexer;
mod modules;
@@ -9,6 +10,7 @@ mod schema;
mod typechecker;
mod types;
use diagnostics::render;
use interpreter::Interpreter;
use parser::Parser;
use std::io::{self, Write};
@@ -90,7 +92,8 @@ fn run_file(path: &str) {
let mut checker = TypeChecker::new();
if let Err(errors) = checker.check_program_with_modules(&program, &loader) {
for error in errors {
eprintln!("Type error: {}", error);
let diagnostic = error.to_diagnostic();
eprint!("{}", render(&diagnostic, &source, Some(path)));
}
std::process::exit(1);
}
@@ -103,7 +106,8 @@ fn run_file(path: &str) {
}
}
Err(e) => {
eprintln!("Runtime error: {}", e);
let diagnostic = e.to_diagnostic();
eprint!("{}", render(&diagnostic, &source, Some(path)));
std::process::exit(1);
}
}
@@ -850,4 +854,81 @@ c")"#;
assert!(result.is_err());
assert!(result.unwrap_err().contains("pure but has effects"));
}
// Diagnostic rendering tests
mod diagnostic_tests {
use crate::diagnostics::{render_diagnostic_plain, Diagnostic, Severity};
use crate::ast::Span;
use crate::typechecker::TypeError;
use crate::interpreter::RuntimeError;
#[test]
fn test_type_error_to_diagnostic() {
let error = TypeError {
message: "Type mismatch: expected Int, got String".to_string(),
span: Span { start: 10, end: 20 },
};
let diag = error.to_diagnostic();
assert_eq!(diag.severity, Severity::Error);
assert_eq!(diag.title, "Type Mismatch");
assert!(diag.hints.len() > 0);
}
#[test]
fn test_runtime_error_to_diagnostic() {
let error = RuntimeError {
message: "Division by zero".to_string(),
span: Some(Span { start: 5, end: 10 }),
};
let diag = error.to_diagnostic();
assert_eq!(diag.severity, Severity::Error);
assert_eq!(diag.title, "Division by Zero");
assert!(diag.hints.len() > 0);
}
#[test]
fn test_diagnostic_render_with_real_code() {
let source = "fn add(a: Int, b: Int): Int = a + b\nlet result = add(1, \"two\")";
let diag = Diagnostic {
severity: Severity::Error,
title: "Type Mismatch".to_string(),
message: "Expected Int but got String".to_string(),
span: Span { start: 56, end: 61 },
hints: vec!["The second argument should be an Int.".to_string()],
};
let output = render_diagnostic_plain(&diag, source, Some("example.lux"));
assert!(output.contains("ERROR"));
assert!(output.contains("example.lux"));
assert!(output.contains("Type Mismatch"));
assert!(output.contains("\"two\""));
assert!(output.contains("Hint:"));
}
#[test]
fn test_undefined_variable_categorization() {
let error = TypeError {
message: "Undefined variable: foobar".to_string(),
span: Span { start: 0, end: 6 },
};
let diag = error.to_diagnostic();
assert_eq!(diag.title, "Unknown Name");
assert!(diag.hints.iter().any(|h| h.contains("spelling")));
}
#[test]
fn test_purity_violation_categorization() {
let error = TypeError {
message: "Function 'foo' is declared as pure but has effects: {Console}".to_string(),
span: Span { start: 0, end: 10 },
};
let diag = error.to_diagnostic();
assert_eq!(diag.title, "Purity Violation");
}
}
}