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:
426
src/diagnostics.rs
Normal file
426
src/diagnostics.rs
Normal file
@@ -0,0 +1,426 @@
|
||||
//! Elm-style diagnostic messages for beautiful error reporting
|
||||
|
||||
use crate::ast::Span;
|
||||
|
||||
/// ANSI color codes for terminal output
|
||||
pub mod colors {
|
||||
pub const RESET: &str = "\x1b[0m";
|
||||
pub const BOLD: &str = "\x1b[1m";
|
||||
pub const DIM: &str = "\x1b[2m";
|
||||
pub const RED: &str = "\x1b[31m";
|
||||
pub const YELLOW: &str = "\x1b[33m";
|
||||
pub const BLUE: &str = "\x1b[34m";
|
||||
pub const CYAN: &str = "\x1b[36m";
|
||||
pub const WHITE: &str = "\x1b[37m";
|
||||
}
|
||||
|
||||
/// Severity level for diagnostics
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Severity {
|
||||
Error,
|
||||
Warning,
|
||||
Hint,
|
||||
}
|
||||
|
||||
impl Severity {
|
||||
fn color(&self) -> &'static str {
|
||||
match self {
|
||||
Severity::Error => colors::RED,
|
||||
Severity::Warning => colors::YELLOW,
|
||||
Severity::Hint => colors::CYAN,
|
||||
}
|
||||
}
|
||||
|
||||
fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Severity::Error => "ERROR",
|
||||
Severity::Warning => "WARNING",
|
||||
Severity::Hint => "HINT",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A diagnostic message with source location and optional hints
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Diagnostic {
|
||||
pub severity: Severity,
|
||||
pub title: String,
|
||||
pub message: String,
|
||||
pub span: Span,
|
||||
pub hints: Vec<String>,
|
||||
}
|
||||
|
||||
impl Diagnostic {
|
||||
pub fn error(title: impl Into<String>, message: impl Into<String>, span: Span) -> Self {
|
||||
Self {
|
||||
severity: Severity::Error,
|
||||
title: title.into(),
|
||||
message: message.into(),
|
||||
span,
|
||||
hints: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn warning(title: impl Into<String>, message: impl Into<String>, span: Span) -> Self {
|
||||
Self {
|
||||
severity: Severity::Warning,
|
||||
title: title.into(),
|
||||
message: message.into(),
|
||||
span,
|
||||
hints: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
|
||||
self.hints.push(hint.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_hints(mut self, hints: Vec<String>) -> Self {
|
||||
self.hints.extend(hints);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts byte offset to (line, column) - 1-indexed
|
||||
pub fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
|
||||
let mut line = 1;
|
||||
let mut col = 1;
|
||||
|
||||
for (i, ch) in source.char_indices() {
|
||||
if i >= offset {
|
||||
break;
|
||||
}
|
||||
if ch == '\n' {
|
||||
line += 1;
|
||||
col = 1;
|
||||
} else {
|
||||
col += 1;
|
||||
}
|
||||
}
|
||||
|
||||
(line, col)
|
||||
}
|
||||
|
||||
/// Extract a source line by line number (1-indexed)
|
||||
pub fn get_source_line(source: &str, line_num: usize) -> Option<&str> {
|
||||
source.lines().nth(line_num.saturating_sub(1))
|
||||
}
|
||||
|
||||
/// Get context lines around a span
|
||||
pub fn get_context_lines(source: &str, span: Span, context: usize) -> Vec<(usize, &str)> {
|
||||
let (start_line, _) = offset_to_line_col(source, span.start);
|
||||
let (end_line, _) = offset_to_line_col(source, span.end);
|
||||
|
||||
let first_line = start_line.saturating_sub(context);
|
||||
let last_line = end_line + context;
|
||||
|
||||
source
|
||||
.lines()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| *i + 1 >= first_line && *i + 1 <= last_line)
|
||||
.map(|(i, line)| (i + 1, line))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Render a diagnostic to a string with colors
|
||||
pub fn render_diagnostic(
|
||||
diagnostic: &Diagnostic,
|
||||
source: &str,
|
||||
filename: Option<&str>,
|
||||
) -> String {
|
||||
let mut output = String::new();
|
||||
let (line, col) = offset_to_line_col(source, diagnostic.span.start);
|
||||
let (end_line, end_col) = offset_to_line_col(source, diagnostic.span.end);
|
||||
let severity_color = diagnostic.severity.color();
|
||||
let severity_label = diagnostic.severity.label();
|
||||
|
||||
// Header: -- ERROR ----------- filename.lux
|
||||
let filename_str = filename.unwrap_or("<input>");
|
||||
output.push_str(&format!(
|
||||
"{}{}{} ── {} ──────────────────────────────────\n",
|
||||
colors::BOLD,
|
||||
severity_color,
|
||||
severity_label,
|
||||
filename_str
|
||||
));
|
||||
output.push_str(colors::RESET);
|
||||
|
||||
// Title
|
||||
output.push_str(&format!(
|
||||
"\n{}{}{}:{}{}\n\n",
|
||||
colors::BOLD,
|
||||
colors::WHITE,
|
||||
line,
|
||||
col,
|
||||
colors::RESET
|
||||
));
|
||||
|
||||
// Error title/category
|
||||
output.push_str(&format!(
|
||||
"{}{}{}{}\n\n",
|
||||
colors::BOLD,
|
||||
severity_color,
|
||||
diagnostic.title,
|
||||
colors::RESET
|
||||
));
|
||||
|
||||
// Source code snippet with line numbers
|
||||
let line_num_width = end_line.to_string().len().max(4);
|
||||
|
||||
// Show the problematic line(s)
|
||||
for line_num in line..=end_line {
|
||||
if let Some(source_line) = get_source_line(source, line_num) {
|
||||
// Line number
|
||||
output.push_str(&format!(
|
||||
"{}{:>width$} │{} ",
|
||||
colors::DIM,
|
||||
line_num,
|
||||
colors::RESET,
|
||||
width = line_num_width
|
||||
));
|
||||
|
||||
// Source line
|
||||
output.push_str(source_line);
|
||||
output.push('\n');
|
||||
|
||||
// Underline the error span
|
||||
let underline_start = if line_num == line { col - 1 } else { 0 };
|
||||
let underline_end = if line_num == end_line {
|
||||
end_col - 1
|
||||
} else {
|
||||
source_line.len()
|
||||
};
|
||||
|
||||
let underline_len = underline_end.saturating_sub(underline_start).max(1);
|
||||
|
||||
output.push_str(&format!(
|
||||
"{}{:>width$} │{} ",
|
||||
colors::DIM,
|
||||
"",
|
||||
colors::RESET,
|
||||
width = line_num_width
|
||||
));
|
||||
output.push_str(&" ".repeat(underline_start));
|
||||
output.push_str(&format!(
|
||||
"{}{}{}",
|
||||
severity_color,
|
||||
"^".repeat(underline_len),
|
||||
colors::RESET
|
||||
));
|
||||
output.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// Error message
|
||||
output.push_str(&format!("\n{}\n", diagnostic.message));
|
||||
|
||||
// Hints
|
||||
if !diagnostic.hints.is_empty() {
|
||||
output.push('\n');
|
||||
for hint in &diagnostic.hints {
|
||||
output.push_str(&format!(
|
||||
"{}{}Hint:{} {}\n",
|
||||
colors::BOLD,
|
||||
colors::CYAN,
|
||||
colors::RESET,
|
||||
hint
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
output.push('\n');
|
||||
output
|
||||
}
|
||||
|
||||
/// Render a diagnostic without colors (for testing or non-TTY output)
|
||||
pub fn render_diagnostic_plain(
|
||||
diagnostic: &Diagnostic,
|
||||
source: &str,
|
||||
filename: Option<&str>,
|
||||
) -> String {
|
||||
let mut output = String::new();
|
||||
let (line, col) = offset_to_line_col(source, diagnostic.span.start);
|
||||
let (end_line, end_col) = offset_to_line_col(source, diagnostic.span.end);
|
||||
|
||||
let filename_str = filename.unwrap_or("<input>");
|
||||
output.push_str(&format!(
|
||||
"-- {} ── {} ──────────────────────────────────\n",
|
||||
diagnostic.severity.label(),
|
||||
filename_str
|
||||
));
|
||||
|
||||
output.push_str(&format!("\n{}:{}\n\n", line, col));
|
||||
output.push_str(&format!("{}\n\n", diagnostic.title));
|
||||
|
||||
let line_num_width = end_line.to_string().len().max(4);
|
||||
|
||||
for line_num in line..=end_line {
|
||||
if let Some(source_line) = get_source_line(source, line_num) {
|
||||
output.push_str(&format!(
|
||||
"{:>width$} | ",
|
||||
line_num,
|
||||
width = line_num_width
|
||||
));
|
||||
output.push_str(source_line);
|
||||
output.push('\n');
|
||||
|
||||
let underline_start = if line_num == line { col - 1 } else { 0 };
|
||||
let underline_end = if line_num == end_line {
|
||||
end_col - 1
|
||||
} else {
|
||||
source_line.len()
|
||||
};
|
||||
|
||||
let underline_len = underline_end.saturating_sub(underline_start).max(1);
|
||||
|
||||
output.push_str(&format!(
|
||||
"{:>width$} | ",
|
||||
"",
|
||||
width = line_num_width
|
||||
));
|
||||
output.push_str(&" ".repeat(underline_start));
|
||||
output.push_str(&"^".repeat(underline_len));
|
||||
output.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
output.push_str(&format!("\n{}\n", diagnostic.message));
|
||||
|
||||
if !diagnostic.hints.is_empty() {
|
||||
output.push('\n');
|
||||
for hint in &diagnostic.hints {
|
||||
output.push_str(&format!("Hint: {}\n", hint));
|
||||
}
|
||||
}
|
||||
|
||||
output.push('\n');
|
||||
output
|
||||
}
|
||||
|
||||
/// Check if stdout is a TTY (for color support)
|
||||
pub fn supports_color() -> bool {
|
||||
// Simple check - in production you'd use atty crate
|
||||
std::env::var("NO_COLOR").is_err() && std::env::var("TERM").map(|t| t != "dumb").unwrap_or(true)
|
||||
}
|
||||
|
||||
/// Render diagnostic with automatic color detection
|
||||
pub fn render(diagnostic: &Diagnostic, source: &str, filename: Option<&str>) -> String {
|
||||
if supports_color() {
|
||||
render_diagnostic(diagnostic, source, filename)
|
||||
} else {
|
||||
render_diagnostic_plain(diagnostic, source, filename)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_offset_to_line_col() {
|
||||
let source = "line one\nline two\nline three";
|
||||
assert_eq!(offset_to_line_col(source, 0), (1, 1));
|
||||
assert_eq!(offset_to_line_col(source, 5), (1, 6));
|
||||
assert_eq!(offset_to_line_col(source, 9), (2, 1));
|
||||
assert_eq!(offset_to_line_col(source, 14), (2, 6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_source_line() {
|
||||
let source = "line one\nline two\nline three";
|
||||
assert_eq!(get_source_line(source, 1), Some("line one"));
|
||||
assert_eq!(get_source_line(source, 2), Some("line two"));
|
||||
assert_eq!(get_source_line(source, 3), Some("line three"));
|
||||
assert_eq!(get_source_line(source, 4), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_diagnostic_plain() {
|
||||
let source = "fn test() {\n unknownVar\n}";
|
||||
let diag = Diagnostic::error(
|
||||
"Unknown Variable",
|
||||
"The variable 'unknownVar' is not in scope.",
|
||||
Span { start: 16, end: 26 },
|
||||
)
|
||||
.with_hint("Did you mean 'knownVar'?");
|
||||
|
||||
let output = render_diagnostic_plain(&diag, source, Some("test.lux"));
|
||||
|
||||
assert!(output.contains("ERROR"));
|
||||
assert!(output.contains("test.lux"));
|
||||
assert!(output.contains("Unknown Variable"));
|
||||
assert!(output.contains("unknownVar"));
|
||||
assert!(output.contains("Hint:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_diagnostic_builder() {
|
||||
let diag = Diagnostic::error("Test", "Message", Span { start: 0, end: 5 })
|
||||
.with_hint("Hint 1")
|
||||
.with_hint("Hint 2")
|
||||
.with_hints(vec!["Hint 3".to_string()]);
|
||||
|
||||
assert_eq!(diag.hints.len(), 3);
|
||||
assert_eq!(diag.severity, Severity::Error);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiline_span() {
|
||||
let source = "fn add(a: Int, b: Int): Int {\n a +\n b\n}";
|
||||
let diag = Diagnostic::error(
|
||||
"Multiline Error",
|
||||
"This spans multiple lines.",
|
||||
Span { start: 29, end: 43 },
|
||||
);
|
||||
|
||||
let output = render_diagnostic_plain(&diag, source, None);
|
||||
|
||||
assert!(output.contains("Multiline Error"));
|
||||
// Should show both lines
|
||||
assert!(output.contains("a +"));
|
||||
assert!(output.contains("b"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_type_error_categorization() {
|
||||
use super::Diagnostic;
|
||||
|
||||
// Test that errors are properly categorized
|
||||
let source = "let x: Int = \"hello\"";
|
||||
|
||||
// Simulate a type mismatch diagnostic
|
||||
let diag = Diagnostic {
|
||||
severity: Severity::Error,
|
||||
title: "Type Mismatch".to_string(),
|
||||
message: "Type mismatch: expected Int, got String".to_string(),
|
||||
span: Span { start: 13, end: 20 },
|
||||
hints: vec!["Check that the types on both sides of the expression are compatible.".to_string()],
|
||||
};
|
||||
|
||||
let output = render_diagnostic_plain(&diag, source, Some("test.lux"));
|
||||
|
||||
assert!(output.contains("Type Mismatch"));
|
||||
assert!(output.contains("\"hello\""));
|
||||
assert!(output.contains("Hint:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_source() {
|
||||
let source = "";
|
||||
let diag = Diagnostic::error("Empty", "No source.", Span { start: 0, end: 0 });
|
||||
|
||||
let output = render_diagnostic_plain(&diag, source, None);
|
||||
assert!(output.contains("Empty"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_char_error() {
|
||||
let source = "x + y";
|
||||
let diag = Diagnostic::error("Operator", "Invalid op.", Span { start: 2, end: 3 });
|
||||
|
||||
let output = render_diagnostic_plain(&diag, source, None);
|
||||
assert!(output.contains("+"));
|
||||
assert!(output.contains("^"));
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
85
src/main.rs
85
src/main.rs
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Parser for the Lux language
|
||||
|
||||
use crate::ast::*;
|
||||
use crate::diagnostics::{Diagnostic, Severity};
|
||||
use crate::lexer::{LexError, Lexer, Token, TokenKind};
|
||||
use std::fmt;
|
||||
|
||||
@@ -21,6 +22,63 @@ 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);
|
||||
|
||||
Diagnostic {
|
||||
severity: Severity::Error,
|
||||
title,
|
||||
message: self.message.clone(),
|
||||
span: self.span,
|
||||
hints,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Categorize parse errors to provide better titles and hints
|
||||
fn categorize_parse_error(message: &str) -> (String, Vec<String>) {
|
||||
let message_lower = message.to_lowercase();
|
||||
|
||||
if message_lower.contains("unexpected") && message_lower.contains("expected") {
|
||||
(
|
||||
"Unexpected Token".to_string(),
|
||||
vec!["Check for missing or misplaced punctuation.".to_string()],
|
||||
)
|
||||
} else if message_lower.contains("expected") && message_lower.contains("expression") {
|
||||
(
|
||||
"Missing Expression".to_string(),
|
||||
vec!["An expression was expected here.".to_string()],
|
||||
)
|
||||
} else if message_lower.contains("expected") && message_lower.contains(":") {
|
||||
(
|
||||
"Missing Type Annotation".to_string(),
|
||||
vec!["A type annotation is required here.".to_string()],
|
||||
)
|
||||
} else if message_lower.contains("unclosed") || message_lower.contains("unterminated") {
|
||||
(
|
||||
"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") {
|
||||
(
|
||||
"Invalid Syntax".to_string(),
|
||||
vec!["Check the syntax of this construct.".to_string()],
|
||||
)
|
||||
} else if message_lower.contains("identifier") {
|
||||
(
|
||||
"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![])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LexError> for ParseError {
|
||||
fn from(err: LexError) -> Self {
|
||||
ParseError {
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::ast::{
|
||||
LetDecl, Literal, LiteralKind, MatchArm, Parameter, Pattern, Program, Span, Statement,
|
||||
TypeDecl, TypeExpr, UnaryOp, VariantFields,
|
||||
};
|
||||
use crate::diagnostics::{Diagnostic, Severity};
|
||||
use crate::modules::ModuleLoader;
|
||||
use crate::types::{
|
||||
self, unify, EffectDef, EffectOpDef, EffectSet, HandlerDef, PropertySet, Type, TypeEnv,
|
||||
@@ -30,6 +31,72 @@ 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);
|
||||
|
||||
Diagnostic {
|
||||
severity: Severity::Error,
|
||||
title,
|
||||
message: self.message.clone(),
|
||||
span: self.span,
|
||||
hints,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Categorize a type error message to provide better titles and hints
|
||||
fn categorize_type_error(message: &str) -> (String, Vec<String>) {
|
||||
let message_lower = message.to_lowercase();
|
||||
|
||||
if message_lower.contains("type mismatch") {
|
||||
(
|
||||
"Type Mismatch".to_string(),
|
||||
vec!["Check that the types on both sides of the expression are compatible.".to_string()],
|
||||
)
|
||||
} else if message_lower.contains("undefined variable") || message_lower.contains("not found") {
|
||||
(
|
||||
"Unknown Name".to_string(),
|
||||
vec![
|
||||
"Check the spelling of the name.".to_string(),
|
||||
"Make sure the variable is defined before use.".to_string(),
|
||||
],
|
||||
)
|
||||
} else if message_lower.contains("cannot unify") {
|
||||
(
|
||||
"Type Mismatch".to_string(),
|
||||
vec!["The types are not compatible. Check your function arguments and return types.".to_string()],
|
||||
)
|
||||
} else if message_lower.contains("expected") && message_lower.contains("argument") {
|
||||
(
|
||||
"Wrong Number of Arguments".to_string(),
|
||||
vec!["Check the function signature and provide the correct number of arguments.".to_string()],
|
||||
)
|
||||
} else if message_lower.contains("pure") && message_lower.contains("effect") {
|
||||
(
|
||||
"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(),
|
||||
],
|
||||
)
|
||||
} else if message_lower.contains("effect") && message_lower.contains("unhandled") {
|
||||
(
|
||||
"Unhandled Effect".to_string(),
|
||||
vec!["Use a 'handle' expression to provide an implementation for this effect.".to_string()],
|
||||
)
|
||||
} else if message_lower.contains("recursive") {
|
||||
(
|
||||
"Invalid Recursion".to_string(),
|
||||
vec!["Check that recursive calls have proper base cases.".to_string()],
|
||||
)
|
||||
} else {
|
||||
("Type Error".to_string(), vec![])
|
||||
}
|
||||
}
|
||||
|
||||
/// Type checker
|
||||
pub struct TypeChecker {
|
||||
env: TypeEnv,
|
||||
|
||||
Reference in New Issue
Block a user