feat: Elm-quality error messages with error codes

- Add ErrorCode enum with categorized codes (E01xx parse, E02xx type,
  E03xx name, E04xx effect, E05xx pattern, E06xx module, E07xx behavioral)
- Extend Diagnostic struct with error code, expected/actual types, and
  secondary spans
- Add format_type_diff() for visual type comparison in error messages
- Add help URLs linking to lux-lang.dev/errors/{code}
- Update typechecker, parser, and interpreter to use error codes
- Categorize errors with specific codes and helpful hints

Error messages now show:
- Error code in header: -- ERROR[E0301] ──
- Clear error category title
- Visual type diff for type mismatches
- Context-aware hints
- "Learn more" URL for documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 04:11:15 -05:00
parent bc1e5aa8a1
commit 3a46299404
5 changed files with 1200 additions and 45 deletions

View File

@@ -3,7 +3,7 @@
#![allow(dead_code)]
use crate::ast::*;
use crate::diagnostics::{Diagnostic, Severity};
use crate::diagnostics::{Diagnostic, ErrorCode, Severity};
use crate::lexer::{LexError, Lexer, StringPart, Token, TokenKind};
use std::fmt;
@@ -27,57 +27,79 @@ impl fmt::Display for ParseError {
impl ParseError {
/// Convert to a rich diagnostic for Elm-style error display
pub fn to_diagnostic(&self) -> Diagnostic {
let (title, hints) = categorize_parse_error(&self.message);
let (code, title, hints) = categorize_parse_error(&self.message);
Diagnostic {
severity: Severity::Error,
code,
title,
message: self.message.clone(),
span: self.span,
hints,
expected_type: None,
actual_type: None,
secondary_spans: Vec::new(),
}
}
}
/// Categorize parse errors to provide better titles and hints
fn categorize_parse_error(message: &str) -> (String, Vec<String>) {
/// Categorize parse errors to provide better titles, hints, and error codes
fn categorize_parse_error(message: &str) -> (Option<ErrorCode>, String, Vec<String>) {
let message_lower = message.to_lowercase();
if message_lower.contains("unexpected") && message_lower.contains("expected") {
(
Some(ErrorCode::E0101),
"Unexpected Token".to_string(),
vec!["Check for missing or misplaced punctuation.".to_string()],
)
} else if message_lower.contains("expected") && message_lower.contains("expression") {
(
Some(ErrorCode::E0101),
"Missing Expression".to_string(),
vec!["An expression was expected here.".to_string()],
)
} else if message_lower.contains("expected") && message_lower.contains(":") {
(
Some(ErrorCode::E0104),
"Missing Type Annotation".to_string(),
vec!["A type annotation is required here.".to_string()],
)
} else if message_lower.contains("unclosed") || message_lower.contains("unterminated") {
(
Some(ErrorCode::E0102),
"Unclosed Delimiter".to_string(),
vec![
"Check for matching opening and closing brackets.".to_string(),
"Make sure all strings are properly closed with quotes.".to_string(),
],
)
} else if message_lower.contains("invalid") && message_lower.contains("literal") {
(
Some(ErrorCode::E0103),
"Invalid Literal".to_string(),
vec!["Check the format of the literal value.".to_string()],
)
} else if message_lower.contains("invalid") && message_lower.contains("operator") {
(
Some(ErrorCode::E0105),
"Invalid Operator".to_string(),
vec!["Check the operator syntax.".to_string()],
)
} else if message_lower.contains("invalid") {
(
Some(ErrorCode::E0100),
"Invalid Syntax".to_string(),
vec!["Check the syntax of this construct.".to_string()],
)
} else if message_lower.contains("identifier") {
(
Some(ErrorCode::E0100),
"Invalid Identifier".to_string(),
vec!["Identifiers must start with a letter and contain only letters, numbers, and underscores.".to_string()],
)
} else {
("Parse Error".to_string(), vec![])
(Some(ErrorCode::E0100), "Parse Error".to_string(), vec![])
}
}