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:
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user