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

@@ -9,7 +9,7 @@ use crate::ast::{
ImportDecl, LetDecl, Literal, LiteralKind, MatchArm, Parameter, Pattern, Program, Span,
Statement, TraitDecl, TypeDecl, TypeExpr, UnaryOp, VariantFields,
};
use crate::diagnostics::{find_similar_names, format_did_you_mean, Diagnostic, Severity};
use crate::diagnostics::{find_similar_names, format_did_you_mean, Diagnostic, ErrorCode, Severity};
use crate::exhaustiveness::{check_exhaustiveness, missing_patterns_hint};
use crate::modules::ModuleLoader;
use crate::schema::{SchemaRegistry, Compatibility, BreakingChange, AutoMigration};
@@ -39,92 +39,239 @@ impl std::fmt::Display for TypeError {
impl TypeError {
/// Convert to a rich diagnostic for Elm-style error display
pub fn to_diagnostic(&self) -> Diagnostic {
// Categorize the error and extract hints
let (title, hints) = categorize_type_error(&self.message);
// Categorize the error and extract hints, error code, and type info
let (code, title, hints, expected, actual) = categorize_type_error(&self.message);
Diagnostic {
severity: Severity::Error,
code,
title,
message: self.message.clone(),
span: self.span,
hints,
expected_type: expected,
actual_type: actual,
secondary_spans: Vec::new(),
}
}
}
/// Categorize a type error message to provide better titles and hints
fn categorize_type_error(message: &str) -> (String, Vec<String>) {
/// Extract expected and actual types from an error message
fn extract_types_from_message(message: &str) -> (Option<String>, Option<String>) {
// Try to extract "expected X, got Y" patterns
let patterns = [
("expected ", ", got "),
("expected ", " but got "),
("expected ", " found "),
];
for (exp_prefix, got_prefix) in patterns {
if let Some(exp_start) = message.to_lowercase().find(exp_prefix) {
let after_expected = &message[exp_start + exp_prefix.len()..];
if let Some(got_pos) = after_expected.to_lowercase().find(got_prefix) {
let expected = after_expected[..got_pos].trim().to_string();
let after_got = &after_expected[got_pos + got_prefix.len()..];
// Take until end of word or punctuation
let actual = after_got
.split(|c: char| c == ',' || c == '.' || c == ';' || c == ')' || c.is_whitespace())
.next()
.unwrap_or("")
.trim()
.to_string();
if !expected.is_empty() && !actual.is_empty() {
return (Some(expected), Some(actual));
}
}
}
}
// Try "cannot unify X with Y" pattern
if let Some(unify_pos) = message.to_lowercase().find("cannot unify ") {
let after_unify = &message[unify_pos + 13..];
if let Some(with_pos) = after_unify.to_lowercase().find(" with ") {
let type1 = after_unify[..with_pos].trim().to_string();
let type2 = after_unify[with_pos + 6..]
.split(|c: char| c == ',' || c == '.' || c == ';' || c == ')' || c.is_whitespace())
.next()
.unwrap_or("")
.trim()
.to_string();
if !type1.is_empty() && !type2.is_empty() {
return (Some(type1), Some(type2));
}
}
}
(None, None)
}
/// Categorize a type error message to provide better titles, hints, and error codes
fn categorize_type_error(message: &str) -> (Option<ErrorCode>, String, Vec<String>, Option<String>, Option<String>) {
let message_lower = message.to_lowercase();
let (expected, actual) = extract_types_from_message(message);
if message_lower.contains("type mismatch") {
(
Some(ErrorCode::E0201),
"Type Mismatch".to_string(),
vec!["Check that the types on both sides of the expression are compatible.".to_string()],
expected,
actual,
)
} else if message_lower.contains("undefined variable") || message_lower.contains("not found") {
} else if message_lower.contains("undefined variable") {
(
Some(ErrorCode::E0301),
"Undefined Variable".to_string(),
vec![
"Check the spelling of the variable name.".to_string(),
"Make sure the variable is defined before use.".to_string(),
],
None,
None,
)
} else if message_lower.contains("undefined function") || message_lower.contains("function") && message_lower.contains("not found") {
(
Some(ErrorCode::E0302),
"Undefined Function".to_string(),
vec![
"Check the spelling of the function name.".to_string(),
"Make sure the function is imported or defined.".to_string(),
],
None,
None,
)
} else if message_lower.contains("not found") || message_lower.contains("unknown") && message_lower.contains("name") {
(
Some(ErrorCode::E0301),
"Unknown Name".to_string(),
vec![
"Check the spelling of the name.".to_string(),
"Make sure the variable is defined before use.".to_string(),
],
None,
None,
)
} else if message_lower.contains("cannot unify") {
(
"Type Mismatch".to_string(),
Some(ErrorCode::E0202),
"Cannot Unify Types".to_string(),
vec!["The types are not compatible. Check your function arguments and return types.".to_string()],
expected,
actual,
)
} else if message_lower.contains("expected") && message_lower.contains("argument") {
(
Some(ErrorCode::E0209),
"Wrong Number of Arguments".to_string(),
vec!["Check the function signature and provide the correct number of arguments.".to_string()],
None,
None,
)
} else if message_lower.contains("pure") && message_lower.contains("effect") {
(
Some(ErrorCode::E0701),
"Purity Violation".to_string(),
vec![
"Functions marked 'is pure' cannot perform effects.".to_string(),
"Remove the 'is pure' annotation or handle the effects.".to_string(),
],
None,
None,
)
} else if message_lower.contains("effect") && message_lower.contains("unhandled") {
(
Some(ErrorCode::E0401),
"Unhandled Effect".to_string(),
vec!["Use a 'handle' expression to provide an implementation for this effect.".to_string()],
None,
None,
)
} else if message_lower.contains("recursive") {
(
Some(ErrorCode::E0205),
"Invalid Recursion".to_string(),
vec!["Check that recursive calls have proper base cases.".to_string()],
None,
None,
)
} else if message_lower.contains("unknown effect") {
(
Some(ErrorCode::E0402),
"Unknown Effect".to_string(),
vec![
"Make sure the effect is spelled correctly.".to_string(),
"Built-in effects: Console, File, Process, Http, Random, Time, Sql.".to_string(),
],
None,
None,
)
} else if message_lower.contains("record has no field") {
} else if message_lower.contains("record has no field") || message_lower.contains("missing field") {
(
"Missing Field".to_string(),
Some(ErrorCode::E0207),
"Missing Record Field".to_string(),
vec!["Check the field name spelling or review the record definition.".to_string()],
None,
None,
)
} else if message_lower.contains("undefined field") {
(
Some(ErrorCode::E0308),
"Undefined Field".to_string(),
vec!["Check the field name spelling or review the record definition.".to_string()],
None,
None,
)
} else if message_lower.contains("cannot access field") {
(
Some(ErrorCode::E0308),
"Invalid Field Access".to_string(),
vec!["Field access is only valid on record types.".to_string()],
None,
None,
)
} else if message_lower.contains("effect") && (message_lower.contains("not available") || message_lower.contains("missing")) {
(
"Missing Effect".to_string(),
Some(ErrorCode::E0403),
"Missing Effect Handler".to_string(),
vec![
"Add the effect to your function's effect list.".to_string(),
"Example: fn myFn(): Int with {Console, Sql} = ...".to_string(),
],
None,
None,
)
} else if message_lower.contains("non-exhaustive") || message_lower.contains("exhaustive") {
(
Some(ErrorCode::E0501),
"Non-Exhaustive Patterns".to_string(),
vec!["Add patterns for the missing cases or use a wildcard pattern '_'.".to_string()],
None,
None,
)
} else if message_lower.contains("duplicate") {
(
Some(ErrorCode::E0305),
"Duplicate Definition".to_string(),
vec!["Each name can only be defined once in the same scope.".to_string()],
None,
None,
)
} else if message_lower.contains("return type") {
(
Some(ErrorCode::E0211),
"Return Type Mismatch".to_string(),
vec!["The function body's type doesn't match the declared return type.".to_string()],
expected,
actual,
)
} else {
("Type Error".to_string(), vec![])
(
Some(ErrorCode::E0200),
"Type Error".to_string(),
vec![],
expected,
actual,
)
}
}