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

@@ -3,6 +3,7 @@
#![allow(dead_code, unused_variables)]
use crate::ast::*;
use crate::diagnostics::{Diagnostic, Severity};
use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt;
@@ -327,6 +328,74 @@ impl fmt::Display for RuntimeError {
impl std::error::Error for RuntimeError {}
impl RuntimeError {
/// Convert to a rich diagnostic for Elm-style error display
pub fn to_diagnostic(&self) -> Diagnostic {
let (title, hints) = categorize_runtime_error(&self.message);
Diagnostic {
severity: Severity::Error,
title,
message: self.message.clone(),
span: self.span.unwrap_or_default(),
hints,
}
}
}
/// Categorize runtime errors to provide better titles and hints
fn categorize_runtime_error(message: &str) -> (String, Vec<String>) {
let message_lower = message.to_lowercase();
if message_lower.contains("undefined") || message_lower.contains("not found") {
(
"Undefined Reference".to_string(),
vec!["Make sure the name is defined and in scope.".to_string()],
)
} else if message_lower.contains("division by zero") || message_lower.contains("divide by zero") {
(
"Division by Zero".to_string(),
vec![
"Check that the divisor is not zero before dividing.".to_string(),
"Consider using a guard or match to handle this case.".to_string(),
],
)
} else if message_lower.contains("type") && message_lower.contains("mismatch") {
(
"Type Mismatch".to_string(),
vec!["The value has a different type than expected.".to_string()],
)
} else if message_lower.contains("effect") && message_lower.contains("unhandled") {
(
"Unhandled Effect".to_string(),
vec![
"This effect must be handled before the program can continue.".to_string(),
"Wrap this code in a 'handle' expression.".to_string(),
],
)
} else if message_lower.contains("pattern") && message_lower.contains("match") {
(
"Non-exhaustive Pattern".to_string(),
vec!["Add more patterns to cover all possible cases.".to_string()],
)
} else if message_lower.contains("argument") {
(
"Wrong Arguments".to_string(),
vec!["Check the number and types of arguments provided.".to_string()],
)
} else if message_lower.contains("index") || message_lower.contains("bounds") {
(
"Index Out of Bounds".to_string(),
vec![
"The index is outside the valid range.".to_string(),
"Check the length of the collection before accessing.".to_string(),
],
)
} else {
("Runtime Error".to_string(), vec![])
}
}
/// Effect operation request
#[derive(Debug, Clone)]
pub struct EffectRequest {