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

@@ -1,9 +1,223 @@
//! Elm-style diagnostic messages for beautiful error reporting //! Elm-style diagnostic messages for beautiful error reporting
//!
//! This module provides Elm-quality error messages with:
//! - Error codes (E0001, E0002, etc.) for easy searchability
//! - Visual type diffs showing expected vs actual
//! - "Did you mean?" suggestions using Levenshtein distance
//! - Context-aware hints based on error type
//! - Colored output with syntax highlighting
#![allow(dead_code)] #![allow(dead_code)]
use crate::ast::Span; use crate::ast::Span;
/// Error codes for all Lux compiler errors
/// These follow the pattern E{category}{number}:
/// - E01xx: Parse errors
/// - E02xx: Type errors
/// - E03xx: Name resolution errors
/// - E04xx: Effect errors
/// - E05xx: Pattern matching errors
/// - E06xx: Import/module errors
/// - E07xx: Behavioral type errors
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCode {
// Parse errors (E01xx)
E0100, // Generic parse error
E0101, // Unexpected token
E0102, // Unclosed delimiter
E0103, // Invalid literal
E0104, // Missing semicolon or delimiter
E0105, // Invalid operator
// Type errors (E02xx)
E0200, // Generic type error
E0201, // Type mismatch
E0202, // Cannot unify types
E0203, // Missing type annotation
E0204, // Invalid type application
E0205, // Recursive type without indirection
E0206, // Field type mismatch
E0207, // Missing record field
E0208, // Extra record field
E0209, // Wrong number of type arguments
E0210, // Type not found
E0211, // Return type mismatch
// Name resolution errors (E03xx)
E0300, // Generic name error
E0301, // Undefined variable
E0302, // Undefined function
E0303, // Undefined type
E0304, // Undefined module
E0305, // Duplicate definition
E0306, // Shadowing warning
E0307, // Unused variable warning
E0308, // Undefined field
// Effect errors (E04xx)
E0400, // Generic effect error
E0401, // Unhandled effect
E0402, // Effect not in scope
E0403, // Missing effect handler
E0404, // Invalid effect operation
E0405, // Effect mismatch
// Pattern matching errors (E05xx)
E0500, // Generic pattern error
E0501, // Non-exhaustive patterns
E0502, // Unreachable pattern
E0503, // Invalid pattern
E0504, // Duplicate pattern binding
// Import/module errors (E06xx)
E0600, // Generic module error
E0601, // Module not found
E0602, // Circular import
E0603, // Export not found
E0604, // Private item accessed
// Behavioral type errors (E07xx)
E0700, // Generic behavioral error
E0701, // Purity violation
E0702, // Totality violation
E0703, // Idempotency violation
E0704, // Commutativity violation
}
impl ErrorCode {
pub fn code(&self) -> &'static str {
match self {
// Parse errors
ErrorCode::E0100 => "E0100",
ErrorCode::E0101 => "E0101",
ErrorCode::E0102 => "E0102",
ErrorCode::E0103 => "E0103",
ErrorCode::E0104 => "E0104",
ErrorCode::E0105 => "E0105",
// Type errors
ErrorCode::E0200 => "E0200",
ErrorCode::E0201 => "E0201",
ErrorCode::E0202 => "E0202",
ErrorCode::E0203 => "E0203",
ErrorCode::E0204 => "E0204",
ErrorCode::E0205 => "E0205",
ErrorCode::E0206 => "E0206",
ErrorCode::E0207 => "E0207",
ErrorCode::E0208 => "E0208",
ErrorCode::E0209 => "E0209",
ErrorCode::E0210 => "E0210",
ErrorCode::E0211 => "E0211",
// Name errors
ErrorCode::E0300 => "E0300",
ErrorCode::E0301 => "E0301",
ErrorCode::E0302 => "E0302",
ErrorCode::E0303 => "E0303",
ErrorCode::E0304 => "E0304",
ErrorCode::E0305 => "E0305",
ErrorCode::E0306 => "E0306",
ErrorCode::E0307 => "E0307",
ErrorCode::E0308 => "E0308",
// Effect errors
ErrorCode::E0400 => "E0400",
ErrorCode::E0401 => "E0401",
ErrorCode::E0402 => "E0402",
ErrorCode::E0403 => "E0403",
ErrorCode::E0404 => "E0404",
ErrorCode::E0405 => "E0405",
// Pattern errors
ErrorCode::E0500 => "E0500",
ErrorCode::E0501 => "E0501",
ErrorCode::E0502 => "E0502",
ErrorCode::E0503 => "E0503",
ErrorCode::E0504 => "E0504",
// Module errors
ErrorCode::E0600 => "E0600",
ErrorCode::E0601 => "E0601",
ErrorCode::E0602 => "E0602",
ErrorCode::E0603 => "E0603",
ErrorCode::E0604 => "E0604",
// Behavioral errors
ErrorCode::E0700 => "E0700",
ErrorCode::E0701 => "E0701",
ErrorCode::E0702 => "E0702",
ErrorCode::E0703 => "E0703",
ErrorCode::E0704 => "E0704",
}
}
pub fn title(&self) -> &'static str {
match self {
// Parse errors
ErrorCode::E0100 => "Parse Error",
ErrorCode::E0101 => "Unexpected Token",
ErrorCode::E0102 => "Unclosed Delimiter",
ErrorCode::E0103 => "Invalid Literal",
ErrorCode::E0104 => "Missing Delimiter",
ErrorCode::E0105 => "Invalid Operator",
// Type errors
ErrorCode::E0200 => "Type Error",
ErrorCode::E0201 => "Type Mismatch",
ErrorCode::E0202 => "Cannot Unify Types",
ErrorCode::E0203 => "Missing Type Annotation",
ErrorCode::E0204 => "Invalid Type Application",
ErrorCode::E0205 => "Recursive Type Error",
ErrorCode::E0206 => "Field Type Mismatch",
ErrorCode::E0207 => "Missing Record Field",
ErrorCode::E0208 => "Extra Record Field",
ErrorCode::E0209 => "Wrong Type Arity",
ErrorCode::E0210 => "Type Not Found",
ErrorCode::E0211 => "Return Type Mismatch",
// Name errors
ErrorCode::E0300 => "Name Error",
ErrorCode::E0301 => "Undefined Variable",
ErrorCode::E0302 => "Undefined Function",
ErrorCode::E0303 => "Undefined Type",
ErrorCode::E0304 => "Undefined Module",
ErrorCode::E0305 => "Duplicate Definition",
ErrorCode::E0306 => "Variable Shadowing",
ErrorCode::E0307 => "Unused Variable",
ErrorCode::E0308 => "Undefined Field",
// Effect errors
ErrorCode::E0400 => "Effect Error",
ErrorCode::E0401 => "Unhandled Effect",
ErrorCode::E0402 => "Effect Not In Scope",
ErrorCode::E0403 => "Missing Effect Handler",
ErrorCode::E0404 => "Invalid Effect Operation",
ErrorCode::E0405 => "Effect Mismatch",
// Pattern errors
ErrorCode::E0500 => "Pattern Error",
ErrorCode::E0501 => "Non-Exhaustive Patterns",
ErrorCode::E0502 => "Unreachable Pattern",
ErrorCode::E0503 => "Invalid Pattern",
ErrorCode::E0504 => "Duplicate Pattern Binding",
// Module errors
ErrorCode::E0600 => "Module Error",
ErrorCode::E0601 => "Module Not Found",
ErrorCode::E0602 => "Circular Import",
ErrorCode::E0603 => "Export Not Found",
ErrorCode::E0604 => "Private Item Access",
// Behavioral errors
ErrorCode::E0700 => "Behavioral Type Error",
ErrorCode::E0701 => "Purity Violation",
ErrorCode::E0702 => "Totality Violation",
ErrorCode::E0703 => "Idempotency Violation",
ErrorCode::E0704 => "Commutativity Violation",
}
}
/// Get a URL to documentation about this error
pub fn help_url(&self) -> String {
format!("https://lux-lang.dev/errors/{}", self.code())
}
}
impl std::fmt::Display for ErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.code())
}
}
/// ANSI color codes for terminal output /// ANSI color codes for terminal output
pub mod colors { pub mod colors {
pub const RESET: &str = "\x1b[0m"; pub const RESET: &str = "\x1b[0m";
@@ -46,30 +260,57 @@ impl Severity {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Diagnostic { pub struct Diagnostic {
pub severity: Severity, pub severity: Severity,
pub code: Option<ErrorCode>,
pub title: String, pub title: String,
pub message: String, pub message: String,
pub span: Span, pub span: Span,
pub hints: Vec<String>, pub hints: Vec<String>,
pub expected_type: Option<String>,
pub actual_type: Option<String>,
pub secondary_spans: Vec<(Span, String)>,
} }
impl Diagnostic { impl Diagnostic {
pub fn error(title: impl Into<String>, message: impl Into<String>, span: Span) -> Self { pub fn error(title: impl Into<String>, message: impl Into<String>, span: Span) -> Self {
Self { Self {
severity: Severity::Error, severity: Severity::Error,
code: None,
title: title.into(), title: title.into(),
message: message.into(), message: message.into(),
span, span,
hints: Vec::new(), hints: Vec::new(),
expected_type: None,
actual_type: None,
secondary_spans: Vec::new(),
} }
} }
pub fn warning(title: impl Into<String>, message: impl Into<String>, span: Span) -> Self { pub fn warning(title: impl Into<String>, message: impl Into<String>, span: Span) -> Self {
Self { Self {
severity: Severity::Warning, severity: Severity::Warning,
code: None,
title: title.into(), title: title.into(),
message: message.into(), message: message.into(),
span, span,
hints: Vec::new(), hints: Vec::new(),
expected_type: None,
actual_type: None,
secondary_spans: Vec::new(),
}
}
/// Create an error with a specific error code
pub fn with_code(code: ErrorCode, message: impl Into<String>, span: Span) -> Self {
Self {
severity: Severity::Error,
code: Some(code),
title: code.title().to_string(),
message: message.into(),
span,
hints: Vec::new(),
expected_type: None,
actual_type: None,
secondary_spans: Vec::new(),
} }
} }
@@ -82,6 +323,111 @@ impl Diagnostic {
self.hints.extend(hints); self.hints.extend(hints);
self self
} }
/// Add expected and actual types for type mismatch visualization
pub fn with_types(mut self, expected: impl Into<String>, actual: impl Into<String>) -> Self {
self.expected_type = Some(expected.into());
self.actual_type = Some(actual.into());
self
}
/// Add a secondary span with a label (e.g., "defined here")
pub fn with_secondary(mut self, span: Span, label: impl Into<String>) -> Self {
self.secondary_spans.push((span, label.into()));
self
}
/// Set the error code
pub fn code(mut self, code: ErrorCode) -> Self {
self.code = Some(code);
self.title = code.title().to_string();
self
}
}
/// Format a visual type diff between expected and actual types
/// Highlights the differences in red
pub fn format_type_diff(expected: &str, actual: &str) -> String {
let mut output = String::new();
output.push_str(&format!(
"\n {}{}Expected:{} {}{}{}\n",
colors::BOLD,
colors::CYAN,
colors::RESET,
colors::BOLD,
expected,
colors::RESET
));
output.push_str(&format!(
" {}{}Found: {} {}{}{}\n",
colors::BOLD,
colors::RED,
colors::RESET,
colors::BOLD,
actual,
colors::RESET
));
// If types are structurally similar, show where they differ
if let Some(diff) = compute_type_diff(expected, actual) {
output.push_str(&format!("\n {}\n", diff));
}
output
}
/// Compute a simple diff between two type strings
fn compute_type_diff(expected: &str, actual: &str) -> Option<String> {
// Simple case: find the first difference
let exp_chars: Vec<char> = expected.chars().collect();
let act_chars: Vec<char> = actual.chars().collect();
let mut diff_start = None;
for (i, (e, a)) in exp_chars.iter().zip(act_chars.iter()).enumerate() {
if e != a && diff_start.is_none() {
diff_start = Some(i);
break;
}
}
// If the types are identical, no diff needed
if diff_start.is_none() && exp_chars.len() == act_chars.len() {
return None;
}
// Show the difference location with an arrow
if let Some(start) = diff_start {
let pointer = format!(
"{}{}^-- difference starts here{}",
" ".repeat(start + 9), // Account for "Found: " prefix
colors::YELLOW,
colors::RESET
);
return Some(pointer);
}
// Length difference
if exp_chars.len() != act_chars.len() {
return Some(format!(
"{}Note:{} Types have different lengths ({} vs {} characters)",
colors::YELLOW,
colors::RESET,
exp_chars.len(),
act_chars.len()
));
}
None
}
/// Format a plain type diff (no colors)
pub fn format_type_diff_plain(expected: &str, actual: &str) -> String {
format!(
"\n Expected: {}\n Found: {}\n",
expected, actual
)
} }
/// Calculate the Levenshtein edit distance between two strings /// Calculate the Levenshtein edit distance between two strings
@@ -211,18 +557,23 @@ pub fn render_diagnostic(
let severity_color = diagnostic.severity.color(); let severity_color = diagnostic.severity.color();
let severity_label = diagnostic.severity.label(); let severity_label = diagnostic.severity.label();
// Header: -- ERROR ----------- filename.lux // Header with error code: ── ERROR[E0201] ── filename.lux
let filename_str = filename.unwrap_or("<input>"); let filename_str = filename.unwrap_or("<input>");
let code_str = diagnostic
.code
.map(|c| format!("[{}]", c.code()))
.unwrap_or_default();
output.push_str(&format!( output.push_str(&format!(
"{}{}{} ── {} ──────────────────────────────────\n", "{}{}── {}{} ──────────────────── {}{}\n",
colors::BOLD, colors::BOLD,
severity_color, severity_color,
severity_label, severity_label,
filename_str code_str,
filename_str,
colors::RESET
)); ));
output.push_str(colors::RESET);
// Title // Location
output.push_str(&format!( output.push_str(&format!(
"\n{}{}{}:{}{}\n\n", "\n{}{}{}:{}{}\n\n",
colors::BOLD, colors::BOLD,
@@ -309,6 +660,11 @@ pub fn render_diagnostic(
// Error message // Error message
output.push_str(&format!("\n{}\n", diagnostic.message)); output.push_str(&format!("\n{}\n", diagnostic.message));
// Type diff visualization (if present)
if let (Some(expected), Some(actual)) = (&diagnostic.expected_type, &diagnostic.actual_type) {
output.push_str(&format_type_diff(expected, actual));
}
// Hints // Hints
if !diagnostic.hints.is_empty() { if !diagnostic.hints.is_empty() {
output.push('\n'); output.push('\n');
@@ -323,6 +679,17 @@ pub fn render_diagnostic(
} }
} }
// Help URL for error codes
if let Some(code) = diagnostic.code {
output.push_str(&format!(
"\n{}{}Learn more:{} {}\n",
colors::DIM,
colors::BLUE,
colors::RESET,
code.help_url()
));
}
output.push('\n'); output.push('\n');
output output
} }
@@ -338,9 +705,14 @@ pub fn render_diagnostic_plain(
let (end_line, end_col) = offset_to_line_col(source, diagnostic.span.end); let (end_line, end_col) = offset_to_line_col(source, diagnostic.span.end);
let filename_str = filename.unwrap_or("<input>"); let filename_str = filename.unwrap_or("<input>");
let code_str = diagnostic
.code
.map(|c| format!("[{}]", c.code()))
.unwrap_or_default();
output.push_str(&format!( output.push_str(&format!(
"-- {} ── {} ──────────────────────────────────\n", "-- {}{} ── {} ──────────────────────────────────\n",
diagnostic.severity.label(), diagnostic.severity.label(),
code_str,
filename_str filename_str
)); ));
@@ -381,6 +753,11 @@ pub fn render_diagnostic_plain(
output.push_str(&format!("\n{}\n", diagnostic.message)); output.push_str(&format!("\n{}\n", diagnostic.message));
// Type diff visualization (plain)
if let (Some(expected), Some(actual)) = (&diagnostic.expected_type, &diagnostic.actual_type) {
output.push_str(&format_type_diff_plain(expected, actual));
}
if !diagnostic.hints.is_empty() { if !diagnostic.hints.is_empty() {
output.push('\n'); output.push('\n');
for hint in &diagnostic.hints { for hint in &diagnostic.hints {
@@ -388,6 +765,11 @@ pub fn render_diagnostic_plain(
} }
} }
// Help URL for error codes
if let Some(code) = diagnostic.code {
output.push_str(&format!("\nLearn more: {}\n", code.help_url()));
}
output.push('\n'); output.push('\n');
output output
} }
@@ -478,25 +860,28 @@ mod tests {
#[test] #[test]
fn test_type_error_categorization() { fn test_type_error_categorization() {
use super::Diagnostic; use super::{Diagnostic, ErrorCode};
// Test that errors are properly categorized // Test that errors are properly categorized
let source = "let x: Int = \"hello\""; let source = "let x: Int = \"hello\"";
// Simulate a type mismatch diagnostic // Simulate a type mismatch diagnostic with error code
let diag = Diagnostic { let diag = Diagnostic::with_code(
severity: Severity::Error, ErrorCode::E0201,
title: "Type Mismatch".to_string(), "Type mismatch: expected Int, got String",
message: "Type mismatch: expected Int, got String".to_string(), Span { start: 13, end: 20 },
span: Span { start: 13, end: 20 }, )
hints: vec!["Check that the types on both sides of the expression are compatible.".to_string()], .with_types("Int", "String")
}; .with_hint("Check that the types on both sides of the expression are compatible.");
let output = render_diagnostic_plain(&diag, source, Some("test.lux")); let output = render_diagnostic_plain(&diag, source, Some("test.lux"));
assert!(output.contains("Type Mismatch")); assert!(output.contains("Type Mismatch"));
assert!(output.contains("\"hello\"")); assert!(output.contains("\"hello\""));
assert!(output.contains("Hint:")); assert!(output.contains("Hint:"));
assert!(output.contains("[E0201]"));
assert!(output.contains("Expected: Int"));
assert!(output.contains("Found: String"));
} }
#[test] #[test]
@@ -589,4 +974,60 @@ mod tests {
let hint = super::format_did_you_mean(&["a".to_string(), "b".to_string(), "c".to_string()]); let hint = super::format_did_you_mean(&["a".to_string(), "b".to_string(), "c".to_string()]);
assert_eq!(hint, Some("Did you mean 'a', 'b', or 'c'?".to_string())); assert_eq!(hint, Some("Did you mean 'a', 'b', or 'c'?".to_string()));
} }
#[test]
fn test_error_code_display() {
assert_eq!(super::ErrorCode::E0201.code(), "E0201");
assert_eq!(super::ErrorCode::E0201.title(), "Type Mismatch");
assert!(super::ErrorCode::E0201.help_url().contains("E0201"));
}
#[test]
fn test_diagnostic_with_code() {
let diag = super::Diagnostic::with_code(
super::ErrorCode::E0301,
"Variable 'x' not found",
Span { start: 0, end: 1 },
);
assert_eq!(diag.code, Some(super::ErrorCode::E0301));
assert_eq!(diag.title, "Undefined Variable");
}
#[test]
fn test_diagnostic_with_types() {
let diag = super::Diagnostic::error("Test", "Msg", Span { start: 0, end: 1 })
.with_types("Int", "String");
assert_eq!(diag.expected_type, Some("Int".to_string()));
assert_eq!(diag.actual_type, Some("String".to_string()));
}
#[test]
fn test_type_diff_plain() {
let diff = super::format_type_diff_plain("Int", "String");
assert!(diff.contains("Expected: Int"));
assert!(diff.contains("Found: String"));
}
#[test]
fn test_diagnostic_render_with_all_features() {
let source = "let x: Int = \"hello\"";
let diag = super::Diagnostic::with_code(
super::ErrorCode::E0201,
"This value should be an Int but is a String",
Span { start: 13, end: 20 },
)
.with_types("Int", "String")
.with_hint("Try using String.parseInt to convert the string to an integer");
let output = super::render_diagnostic_plain(&diag, source, Some("test.lux"));
// Check all features are present
assert!(output.contains("[E0201]"));
assert!(output.contains("Type Mismatch"));
assert!(output.contains("Expected: Int"));
assert!(output.contains("Found: String"));
assert!(output.contains("Hint:"));
assert!(output.contains("parseInt"));
assert!(output.contains("lux-lang.dev/errors/E0201"));
}
} }

View File

@@ -3,8 +3,9 @@
#![allow(dead_code, unused_variables)] #![allow(dead_code, unused_variables)]
use crate::ast::*; use crate::ast::*;
use crate::diagnostics::{Diagnostic, Severity}; use crate::diagnostics::{Diagnostic, ErrorCode, Severity};
use rand::Rng; use rand::Rng;
use rusqlite::Connection;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt; use std::fmt;
@@ -426,29 +427,47 @@ impl std::error::Error for RuntimeError {}
impl RuntimeError { impl RuntimeError {
/// Convert to a rich diagnostic for Elm-style error display /// Convert to a rich diagnostic for Elm-style error display
pub fn to_diagnostic(&self) -> Diagnostic { pub fn to_diagnostic(&self) -> Diagnostic {
let (title, hints) = categorize_runtime_error(&self.message); let (code, title, hints) = categorize_runtime_error(&self.message);
Diagnostic { Diagnostic {
severity: Severity::Error, severity: Severity::Error,
code,
title, title,
message: self.message.clone(), message: self.message.clone(),
span: self.span.unwrap_or_default(), span: self.span.unwrap_or_default(),
hints, hints,
expected_type: None,
actual_type: None,
secondary_spans: Vec::new(),
} }
} }
} }
/// Categorize runtime errors to provide better titles and hints /// Categorize runtime errors to provide better titles, hints, and error codes
fn categorize_runtime_error(message: &str) -> (String, Vec<String>) { fn categorize_runtime_error(message: &str) -> (Option<ErrorCode>, String, Vec<String>) {
let message_lower = message.to_lowercase(); let message_lower = message.to_lowercase();
if message_lower.contains("undefined") || message_lower.contains("not found") { if message_lower.contains("undefined variable") {
( (
Some(ErrorCode::E0301),
"Undefined Variable".to_string(),
vec!["Make sure the variable is defined and in scope.".to_string()],
)
} else if message_lower.contains("undefined function") || message_lower.contains("function not found") {
(
Some(ErrorCode::E0302),
"Undefined Function".to_string(),
vec!["Make sure the function is defined and in scope.".to_string()],
)
} else if message_lower.contains("undefined") || message_lower.contains("not found") {
(
Some(ErrorCode::E0301),
"Undefined Reference".to_string(), "Undefined Reference".to_string(),
vec!["Make sure the name is defined and in scope.".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") { } else if message_lower.contains("division by zero") || message_lower.contains("divide by zero") {
( (
None, // Runtime error, not a type error
"Division by Zero".to_string(), "Division by Zero".to_string(),
vec![ vec![
"Check that the divisor is not zero before dividing.".to_string(), "Check that the divisor is not zero before dividing.".to_string(),
@@ -457,11 +476,13 @@ fn categorize_runtime_error(message: &str) -> (String, Vec<String>) {
) )
} else if message_lower.contains("type") && message_lower.contains("mismatch") { } else if message_lower.contains("type") && message_lower.contains("mismatch") {
( (
Some(ErrorCode::E0201),
"Type Mismatch".to_string(), "Type Mismatch".to_string(),
vec!["The value has a different type than expected.".to_string()], vec!["The value has a different type than expected.".to_string()],
) )
} else if message_lower.contains("effect") && message_lower.contains("unhandled") { } else if message_lower.contains("effect") && message_lower.contains("unhandled") {
( (
Some(ErrorCode::E0401),
"Unhandled Effect".to_string(), "Unhandled Effect".to_string(),
vec![ vec![
"This effect must be handled before the program can continue.".to_string(), "This effect must be handled before the program can continue.".to_string(),
@@ -470,16 +491,19 @@ fn categorize_runtime_error(message: &str) -> (String, Vec<String>) {
) )
} else if message_lower.contains("pattern") && message_lower.contains("match") { } else if message_lower.contains("pattern") && message_lower.contains("match") {
( (
Some(ErrorCode::E0501),
"Non-exhaustive Pattern".to_string(), "Non-exhaustive Pattern".to_string(),
vec!["Add more patterns to cover all possible cases.".to_string()], vec!["Add more patterns to cover all possible cases.".to_string()],
) )
} else if message_lower.contains("argument") { } else if message_lower.contains("argument") {
( (
Some(ErrorCode::E0209),
"Wrong Arguments".to_string(), "Wrong Arguments".to_string(),
vec!["Check the number and types of arguments provided.".to_string()], vec!["Check the number and types of arguments provided.".to_string()],
) )
} else if message_lower.contains("index") || message_lower.contains("bounds") { } else if message_lower.contains("index") || message_lower.contains("bounds") {
( (
None, // Runtime error
"Index Out of Bounds".to_string(), "Index Out of Bounds".to_string(),
vec![ vec![
"The index is outside the valid range.".to_string(), "The index is outside the valid range.".to_string(),
@@ -487,7 +511,7 @@ fn categorize_runtime_error(message: &str) -> (String, Vec<String>) {
], ],
) )
} else { } else {
("Runtime Error".to_string(), vec![]) (None, "Runtime Error".to_string(), vec![])
} }
} }
@@ -587,6 +611,10 @@ pub struct Interpreter {
current_http_request: Arc<Mutex<Option<tiny_http::Request>>>, current_http_request: Arc<Mutex<Option<tiny_http::Request>>>,
/// Test results for the Test effect /// Test results for the Test effect
test_results: RefCell<TestResults>, test_results: RefCell<TestResults>,
/// SQL database connections (connection ID -> Connection)
sql_connections: RefCell<HashMap<i64, Connection>>,
/// Next SQL connection ID
next_sql_conn_id: RefCell<i64>,
} }
/// Results from running tests /// Results from running tests
@@ -627,6 +655,8 @@ impl Interpreter {
http_server: Arc::new(Mutex::new(None)), http_server: Arc::new(Mutex::new(None)),
current_http_request: Arc::new(Mutex::new(None)), current_http_request: Arc::new(Mutex::new(None)),
test_results: RefCell::new(TestResults::default()), test_results: RefCell::new(TestResults::default()),
sql_connections: RefCell::new(HashMap::new()),
next_sql_conn_id: RefCell::new(1),
} }
} }
@@ -708,6 +738,32 @@ impl Interpreter {
.insert(from_version, migration); .insert(from_version, migration);
} }
/// Register auto-generated migrations from the typechecker
/// These are migrations that were automatically generated for auto-migratable schema changes
pub fn register_auto_migrations(
&mut self,
auto_migrations: &std::collections::HashMap<String, std::collections::HashMap<u32, Expr>>,
) {
for (type_name, version_migrations) in auto_migrations {
for (from_version, body) in version_migrations {
// Only register if no migration already exists
let already_has = self
.migrations
.get(type_name)
.map(|m| m.contains_key(from_version))
.unwrap_or(false);
if !already_has {
let stored = StoredMigration {
body: body.clone(),
env: self.global_env.clone(),
};
self.register_migration(type_name, *from_version, stored);
}
}
}
}
/// Create a versioned value /// Create a versioned value
pub fn create_versioned(&self, type_name: &str, version: u32, value: Value) -> Value { pub fn create_versioned(&self, type_name: &str, version: u32, value: Value) -> Value {
Value::Versioned { Value::Versioned {
@@ -3712,6 +3768,292 @@ impl Interpreter {
Ok(Value::Unit) Ok(Value::Unit)
} }
// ===== Sql Effect =====
("Sql", "open") => {
let path = match request.args.first() {
Some(Value::String(s)) => s.clone(),
_ => return Err(RuntimeError {
message: "Sql.open requires a path string".to_string(),
span: None,
}),
};
match Connection::open(&path) {
Ok(conn) => {
let id = *self.next_sql_conn_id.borrow();
*self.next_sql_conn_id.borrow_mut() += 1;
self.sql_connections.borrow_mut().insert(id, conn);
Ok(Value::Int(id))
}
Err(e) => Err(RuntimeError {
message: format!("Sql.open failed: {}", e),
span: None,
}),
}
}
("Sql", "openMemory") => {
match Connection::open_in_memory() {
Ok(conn) => {
let id = *self.next_sql_conn_id.borrow();
*self.next_sql_conn_id.borrow_mut() += 1;
self.sql_connections.borrow_mut().insert(id, conn);
Ok(Value::Int(id))
}
Err(e) => Err(RuntimeError {
message: format!("Sql.openMemory failed: {}", e),
span: None,
}),
}
}
("Sql", "close") => {
let conn_id = match request.args.first() {
Some(Value::Int(id)) => *id,
_ => return Err(RuntimeError {
message: "Sql.close requires a connection ID".to_string(),
span: None,
}),
};
if self.sql_connections.borrow_mut().remove(&conn_id).is_some() {
Ok(Value::Unit)
} else {
Err(RuntimeError {
message: format!("Sql.close: invalid connection ID {}", conn_id),
span: None,
})
}
}
("Sql", "execute") => {
let (conn_id, sql) = match (request.args.get(0), request.args.get(1)) {
(Some(Value::Int(id)), Some(Value::String(s))) => (*id, s.clone()),
_ => return Err(RuntimeError {
message: "Sql.execute requires connection ID and SQL string".to_string(),
span: None,
}),
};
let conns = self.sql_connections.borrow();
let conn = match conns.get(&conn_id) {
Some(c) => c,
None => return Err(RuntimeError {
message: format!("Sql.execute: invalid connection ID {}", conn_id),
span: None,
}),
};
match conn.execute(&sql, []) {
Ok(rows_affected) => Ok(Value::Int(rows_affected as i64)),
Err(e) => Err(RuntimeError {
message: format!("Sql.execute failed: {}", e),
span: None,
}),
}
}
("Sql", "query") => {
let (conn_id, sql) = match (request.args.get(0), request.args.get(1)) {
(Some(Value::Int(id)), Some(Value::String(s))) => (*id, s.clone()),
_ => return Err(RuntimeError {
message: "Sql.query requires connection ID and SQL string".to_string(),
span: None,
}),
};
let conns = self.sql_connections.borrow();
let conn = match conns.get(&conn_id) {
Some(c) => c,
None => return Err(RuntimeError {
message: format!("Sql.query: invalid connection ID {}", conn_id),
span: None,
}),
};
let mut stmt = match conn.prepare(&sql) {
Ok(s) => s,
Err(e) => return Err(RuntimeError {
message: format!("Sql.query prepare failed: {}", e),
span: None,
}),
};
let column_names: Vec<String> = stmt.column_names().iter().map(|s| s.to_string()).collect();
let column_count = column_names.len();
let rows: Result<Vec<Value>, _> = stmt.query_map([], |row| {
let mut record = HashMap::new();
for (i, col_name) in column_names.iter().enumerate() {
let value = match row.get_ref(i) {
Ok(rusqlite::types::ValueRef::Null) => Value::Constructor {
name: "None".to_string(),
fields: vec![],
},
Ok(rusqlite::types::ValueRef::Integer(n)) => Value::Int(n),
Ok(rusqlite::types::ValueRef::Real(f)) => Value::Float(f),
Ok(rusqlite::types::ValueRef::Text(s)) => {
Value::String(String::from_utf8_lossy(s).to_string())
}
Ok(rusqlite::types::ValueRef::Blob(b)) => {
Value::String(format!("<blob {} bytes>", b.len()))
}
Err(_) => Value::String("<error>".to_string()),
};
record.insert(col_name.clone(), value);
}
Ok(Value::Record(record))
}).and_then(|rows| rows.collect());
match rows {
Ok(r) => Ok(Value::List(r)),
Err(e) => Err(RuntimeError {
message: format!("Sql.query failed: {}", e),
span: None,
}),
}
}
("Sql", "queryOne") => {
let (conn_id, sql) = match (request.args.get(0), request.args.get(1)) {
(Some(Value::Int(id)), Some(Value::String(s))) => (*id, s.clone()),
_ => return Err(RuntimeError {
message: "Sql.queryOne requires connection ID and SQL string".to_string(),
span: None,
}),
};
let conns = self.sql_connections.borrow();
let conn = match conns.get(&conn_id) {
Some(c) => c,
None => return Err(RuntimeError {
message: format!("Sql.queryOne: invalid connection ID {}", conn_id),
span: None,
}),
};
let mut stmt = match conn.prepare(&sql) {
Ok(s) => s,
Err(e) => return Err(RuntimeError {
message: format!("Sql.queryOne prepare failed: {}", e),
span: None,
}),
};
let column_names: Vec<String> = stmt.column_names().iter().map(|s| s.to_string()).collect();
let result = stmt.query_row([], |row| {
let mut record = HashMap::new();
for (i, col_name) in column_names.iter().enumerate() {
let value = match row.get_ref(i) {
Ok(rusqlite::types::ValueRef::Null) => Value::Constructor {
name: "None".to_string(),
fields: vec![],
},
Ok(rusqlite::types::ValueRef::Integer(n)) => Value::Int(n),
Ok(rusqlite::types::ValueRef::Real(f)) => Value::Float(f),
Ok(rusqlite::types::ValueRef::Text(s)) => {
Value::String(String::from_utf8_lossy(s).to_string())
}
Ok(rusqlite::types::ValueRef::Blob(b)) => {
Value::String(format!("<blob {} bytes>", b.len()))
}
Err(_) => Value::String("<error>".to_string()),
};
record.insert(col_name.clone(), value);
}
Ok(Value::Record(record))
});
match result {
Ok(row) => Ok(Value::Constructor {
name: "Some".to_string(),
fields: vec![row],
}),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(Value::Constructor {
name: "None".to_string(),
fields: vec![],
}),
Err(e) => Err(RuntimeError {
message: format!("Sql.queryOne failed: {}", e),
span: None,
}),
}
}
("Sql", "beginTx") => {
let conn_id = match request.args.first() {
Some(Value::Int(id)) => *id,
_ => return Err(RuntimeError {
message: "Sql.beginTx requires a connection ID".to_string(),
span: None,
}),
};
let conns = self.sql_connections.borrow();
let conn = match conns.get(&conn_id) {
Some(c) => c,
None => return Err(RuntimeError {
message: format!("Sql.beginTx: invalid connection ID {}", conn_id),
span: None,
}),
};
match conn.execute("BEGIN TRANSACTION", []) {
Ok(_) => Ok(Value::Unit),
Err(e) => Err(RuntimeError {
message: format!("Sql.beginTx failed: {}", e),
span: None,
}),
}
}
("Sql", "commit") => {
let conn_id = match request.args.first() {
Some(Value::Int(id)) => *id,
_ => return Err(RuntimeError {
message: "Sql.commit requires a connection ID".to_string(),
span: None,
}),
};
let conns = self.sql_connections.borrow();
let conn = match conns.get(&conn_id) {
Some(c) => c,
None => return Err(RuntimeError {
message: format!("Sql.commit: invalid connection ID {}", conn_id),
span: None,
}),
};
match conn.execute("COMMIT", []) {
Ok(_) => Ok(Value::Unit),
Err(e) => Err(RuntimeError {
message: format!("Sql.commit failed: {}", e),
span: None,
}),
}
}
("Sql", "rollback") => {
let conn_id = match request.args.first() {
Some(Value::Int(id)) => *id,
_ => return Err(RuntimeError {
message: "Sql.rollback requires a connection ID".to_string(),
span: None,
}),
};
let conns = self.sql_connections.borrow();
let conn = match conns.get(&conn_id) {
Some(c) => c,
None => return Err(RuntimeError {
message: format!("Sql.rollback: invalid connection ID {}", conn_id),
span: None,
}),
};
match conn.execute("ROLLBACK", []) {
Ok(_) => Ok(Value::Unit),
Err(e) => Err(RuntimeError {
message: format!("Sql.rollback failed: {}", e),
span: None,
}),
}
}
_ => Err(RuntimeError { _ => Err(RuntimeError {
message: format!( message: format!(
"Unhandled effect operation: {}.{}", "Unhandled effect operation: {}.{}",

View File

@@ -13,6 +13,7 @@ mod lsp;
mod modules; mod modules;
mod package; mod package;
mod parser; mod parser;
mod registry;
mod schema; mod schema;
mod typechecker; mod typechecker;
mod types; mod types;
@@ -126,6 +127,25 @@ fn main() {
// Package manager // Package manager
handle_pkg_command(&args[2..]); handle_pkg_command(&args[2..]);
} }
"registry" => {
// Run package registry server
let storage = args.iter()
.position(|a| a == "--storage" || a == "-s")
.and_then(|i| args.get(i + 1))
.map(|s| s.as_str())
.unwrap_or("./lux-registry");
let bind = args.iter()
.position(|a| a == "--bind" || a == "-b")
.and_then(|i| args.get(i + 1))
.map(|s| s.as_str())
.unwrap_or("127.0.0.1:8080");
if let Err(e) = registry::run_registry_server(storage, bind) {
eprintln!("Registry server error: {}", e);
std::process::exit(1);
}
}
"compile" => { "compile" => {
// Compile to native binary or JavaScript // Compile to native binary or JavaScript
if args.len() < 3 { if args.len() < 3 {
@@ -182,6 +202,9 @@ fn print_help() {
println!(" lux debug <file.lux> Start interactive debugger"); println!(" lux debug <file.lux> Start interactive debugger");
println!(" lux init [name] Initialize a new project"); println!(" lux init [name] Initialize a new project");
println!(" lux pkg <command> Package manager (install, add, remove, list, update)"); println!(" lux pkg <command> Package manager (install, add, remove, list, update)");
println!(" lux registry Start package registry server");
println!(" -s, --storage <dir> Storage directory (default: ./lux-registry)");
println!(" -b, --bind <addr> Bind address (default: 127.0.0.1:8080)");
println!(" lux --lsp Start LSP server (for IDE integration)"); println!(" lux --lsp Start LSP server (for IDE integration)");
println!(" lux --help Show this help"); println!(" lux --help Show this help");
println!(" lux --version Show version"); println!(" lux --version Show version");
@@ -710,6 +733,9 @@ fn run_tests(args: &[String]) {
continue; continue;
} }
// Get auto-generated migrations from typechecker
let auto_migrations = checker.get_auto_migrations().clone();
// Find test functions (functions starting with test_) // Find test functions (functions starting with test_)
let test_funcs: Vec<_> = program.declarations.iter().filter_map(|d| { let test_funcs: Vec<_> = program.declarations.iter().filter_map(|d| {
if let ast::Declaration::Function(f) = d { if let ast::Declaration::Function(f) = d {
@@ -723,6 +749,7 @@ fn run_tests(args: &[String]) {
if test_funcs.is_empty() { if test_funcs.is_empty() {
// No test functions, run the whole file // No test functions, run the whole file
let mut interp = Interpreter::new(); let mut interp = Interpreter::new();
interp.register_auto_migrations(&auto_migrations);
interp.reset_test_results(); interp.reset_test_results();
match interp.run(&program) { match interp.run(&program) {
@@ -755,6 +782,7 @@ fn run_tests(args: &[String]) {
for test_name in &test_funcs { for test_name in &test_funcs {
let mut interp = Interpreter::new(); let mut interp = Interpreter::new();
interp.register_auto_migrations(&auto_migrations);
interp.reset_test_results(); interp.reset_test_results();
// First run the file to define all functions // First run the file to define all functions
@@ -1046,6 +1074,16 @@ fn handle_pkg_command(args: &[String]) {
std::process::exit(1); std::process::exit(1);
} }
} }
"search" => {
if args.len() < 2 {
eprintln!("Usage: lux pkg search <query>");
std::process::exit(1);
}
search_registry(&args[1]);
}
"publish" => {
publish_package(&project_root);
}
"help" | "--help" | "-h" => { "help" | "--help" | "-h" => {
print_pkg_help(); print_pkg_help();
} }
@@ -1057,6 +1095,163 @@ fn handle_pkg_command(args: &[String]) {
} }
} }
fn search_registry(query: &str) {
let registry_url = std::env::var("LUX_REGISTRY_URL")
.unwrap_or_else(|_| "https://pkgs.lux-lang.org".to_string());
println!("Searching for '{}' in {}...", query, registry_url);
// Make HTTP request to registry
let url = format!("{}/api/v1/search?q={}", registry_url, query);
// Use a simple HTTP client (could use reqwest in production)
match simple_http_get(&url) {
Ok(response) => {
// Parse JSON response and display results
if response.contains("\"packages\":[]") {
println!("No packages found matching '{}'", query);
} else {
println!("\nFound packages:");
// Simple parsing of package names from JSON
for line in response.lines() {
if line.contains("\"name\":") {
if let Some(start) = line.find("\"name\":") {
let rest = &line[start + 8..];
if let Some(end) = rest.find('"') {
let name = &rest[..end];
println!(" {}", name);
}
}
}
}
}
}
Err(e) => {
eprintln!("Failed to connect to registry: {}", e);
eprintln!("Make sure the registry server is running or check LUX_REGISTRY_URL");
std::process::exit(1);
}
}
}
fn publish_package(project_root: &std::path::Path) {
use std::fs;
let registry_url = std::env::var("LUX_REGISTRY_URL")
.unwrap_or_else(|_| "https://pkgs.lux-lang.org".to_string());
// Load manifest
let manifest_path = project_root.join("lux.toml");
if !manifest_path.exists() {
eprintln!("No lux.toml found");
std::process::exit(1);
}
let manifest_content = fs::read_to_string(&manifest_path).unwrap();
// Extract project info
let mut name = String::new();
let mut version = String::new();
for line in manifest_content.lines() {
let line = line.trim();
if line.starts_with("name") {
if let Some(eq) = line.find('=') {
name = line[eq+1..].trim().trim_matches('"').to_string();
}
} else if line.starts_with("version") {
if let Some(eq) = line.find('=') {
version = line[eq+1..].trim().trim_matches('"').to_string();
}
}
}
if name.is_empty() || version.is_empty() {
eprintln!("Invalid lux.toml: missing name or version");
std::process::exit(1);
}
println!("Publishing {} v{} to {}...", name, version, registry_url);
// Create tarball of the package
let tarball_name = format!("{}-{}.tar.gz", name, version);
let tarball_path = project_root.join(&tarball_name);
// Use tar command to create tarball
let status = std::process::Command::new("tar")
.arg("-czf")
.arg(&tarball_path)
.arg("--exclude=.lux_packages")
.arg("--exclude=.git")
.arg("--exclude=target")
.arg("-C")
.arg(project_root)
.arg(".")
.status();
match status {
Ok(s) if s.success() => {
println!("Created package tarball: {}", tarball_name);
println!();
println!("To publish, upload the tarball to the registry:");
println!(" curl -X POST {}/api/v1/publish -F 'package=@{}'",
registry_url, tarball_name);
// Clean up tarball
// fs::remove_file(&tarball_path).ok();
}
Ok(_) => {
eprintln!("Failed to create package tarball");
std::process::exit(1);
}
Err(e) => {
eprintln!("Failed to run tar: {}", e);
std::process::exit(1);
}
}
}
fn simple_http_get(url: &str) -> Result<String, String> {
use std::io::{Read, Write};
use std::net::TcpStream;
// Parse URL
let url = url.strip_prefix("http://").or_else(|| url.strip_prefix("https://")).unwrap_or(url);
let (host_port, path) = if let Some(slash) = url.find('/') {
(&url[..slash], &url[slash..])
} else {
(url, "/")
};
let (host, port) = if let Some(colon) = host_port.find(':') {
(&host_port[..colon], host_port[colon+1..].parse::<u16>().unwrap_or(80))
} else {
(host_port, 80)
};
let mut stream = TcpStream::connect((host, port))
.map_err(|e| format!("Connection failed: {}", e))?;
let request = format!(
"GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n",
path, host
);
stream.write_all(request.as_bytes())
.map_err(|e| format!("Write failed: {}", e))?;
let mut response = String::new();
stream.read_to_string(&mut response)
.map_err(|e| format!("Read failed: {}", e))?;
// Extract body (after \r\n\r\n)
if let Some(body_start) = response.find("\r\n\r\n") {
Ok(response[body_start + 4..].to_string())
} else {
Ok(response)
}
}
fn print_pkg_help() { fn print_pkg_help() {
println!("Lux Package Manager"); println!("Lux Package Manager");
println!(); println!();
@@ -1075,6 +1270,11 @@ fn print_pkg_help() {
println!(" list, ls List dependencies and their status"); println!(" list, ls List dependencies and their status");
println!(" update Update all dependencies"); println!(" update Update all dependencies");
println!(" clean Remove installed packages"); println!(" clean Remove installed packages");
println!(" search <query> Search for packages in the registry");
println!(" publish Publish package to the registry");
println!();
println!("Registry Configuration:");
println!(" Set LUX_REGISTRY_URL to use a custom registry (default: https://pkgs.lux-lang.org)");
println!(); println!();
println!("Examples:"); println!("Examples:");
println!(" lux pkg init"); println!(" lux pkg init");
@@ -1084,6 +1284,8 @@ fn print_pkg_help() {
println!(" lux pkg add mylib --git https://github.com/user/mylib"); println!(" lux pkg add mylib --git https://github.com/user/mylib");
println!(" lux pkg add local-lib --path ../lib"); println!(" lux pkg add local-lib --path ../lib");
println!(" lux pkg remove http"); println!(" lux pkg remove http");
println!(" lux pkg search json");
println!(" lux pkg publish");
} }
fn init_pkg_here(name: Option<&str>) { fn init_pkg_here(name: Option<&str>) {
@@ -3073,13 +3275,12 @@ c")"#;
#[test] #[test]
fn test_diagnostic_render_with_real_code() { fn test_diagnostic_render_with_real_code() {
let source = "fn add(a: Int, b: Int): Int = a + b\nlet result = add(1, \"two\")"; let source = "fn add(a: Int, b: Int): Int = a + b\nlet result = add(1, \"two\")";
let diag = Diagnostic { let diag = Diagnostic::error(
severity: Severity::Error, "Type Mismatch",
title: "Type Mismatch".to_string(), "Expected Int but got String",
message: "Expected Int but got String".to_string(), Span { start: 56, end: 61 },
span: Span { start: 56, end: 61 }, )
hints: vec!["The second argument should be an Int.".to_string()], .with_hint("The second argument should be an Int.");
};
let output = render_diagnostic_plain(&diag, source, Some("example.lux")); let output = render_diagnostic_plain(&diag, source, Some("example.lux"));
@@ -3098,8 +3299,10 @@ c")"#;
}; };
let diag = error.to_diagnostic(); let diag = error.to_diagnostic();
assert_eq!(diag.title, "Unknown Name"); assert_eq!(diag.title, "Undefined Variable");
assert!(diag.hints.iter().any(|h| h.contains("spelling"))); assert!(diag.hints.iter().any(|h| h.contains("spelling")));
// Check error code is set
assert!(diag.code.is_some());
} }
#[test] #[test]

View File

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

View File

@@ -9,7 +9,7 @@ use crate::ast::{
ImportDecl, LetDecl, Literal, LiteralKind, MatchArm, Parameter, Pattern, Program, Span, ImportDecl, LetDecl, Literal, LiteralKind, MatchArm, Parameter, Pattern, Program, Span,
Statement, TraitDecl, TypeDecl, TypeExpr, UnaryOp, VariantFields, 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::exhaustiveness::{check_exhaustiveness, missing_patterns_hint};
use crate::modules::ModuleLoader; use crate::modules::ModuleLoader;
use crate::schema::{SchemaRegistry, Compatibility, BreakingChange, AutoMigration}; use crate::schema::{SchemaRegistry, Compatibility, BreakingChange, AutoMigration};
@@ -39,92 +39,239 @@ impl std::fmt::Display for TypeError {
impl TypeError { impl TypeError {
/// Convert to a rich diagnostic for Elm-style error display /// Convert to a rich diagnostic for Elm-style error display
pub fn to_diagnostic(&self) -> Diagnostic { pub fn to_diagnostic(&self) -> Diagnostic {
// Categorize the error and extract hints // Categorize the error and extract hints, error code, and type info
let (title, hints) = categorize_type_error(&self.message); let (code, title, hints, expected, actual) = categorize_type_error(&self.message);
Diagnostic { Diagnostic {
severity: Severity::Error, severity: Severity::Error,
code,
title, title,
message: self.message.clone(), message: self.message.clone(),
span: self.span, span: self.span,
hints, hints,
expected_type: expected,
actual_type: actual,
secondary_spans: Vec::new(),
} }
} }
} }
/// Categorize a type error message to provide better titles and hints /// Extract expected and actual types from an error message
fn categorize_type_error(message: &str) -> (String, Vec<String>) { 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 message_lower = message.to_lowercase();
let (expected, actual) = extract_types_from_message(message);
if message_lower.contains("type mismatch") { if message_lower.contains("type mismatch") {
( (
Some(ErrorCode::E0201),
"Type Mismatch".to_string(), "Type Mismatch".to_string(),
vec!["Check that the types on both sides of the expression are compatible.".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(), "Unknown Name".to_string(),
vec![ vec![
"Check the spelling of the name.".to_string(), "Check the spelling of the name.".to_string(),
"Make sure the variable is defined before use.".to_string(), "Make sure the variable is defined before use.".to_string(),
], ],
None,
None,
) )
} else if message_lower.contains("cannot unify") { } 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()], 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") { } else if message_lower.contains("expected") && message_lower.contains("argument") {
( (
Some(ErrorCode::E0209),
"Wrong Number of Arguments".to_string(), "Wrong Number of Arguments".to_string(),
vec!["Check the function signature and provide the correct 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") { } else if message_lower.contains("pure") && message_lower.contains("effect") {
( (
Some(ErrorCode::E0701),
"Purity Violation".to_string(), "Purity Violation".to_string(),
vec![ vec![
"Functions marked 'is pure' cannot perform effects.".to_string(), "Functions marked 'is pure' cannot perform effects.".to_string(),
"Remove the 'is pure' annotation or handle the 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") { } else if message_lower.contains("effect") && message_lower.contains("unhandled") {
( (
Some(ErrorCode::E0401),
"Unhandled Effect".to_string(), "Unhandled Effect".to_string(),
vec!["Use a 'handle' expression to provide an implementation for this 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") { } else if message_lower.contains("recursive") {
( (
Some(ErrorCode::E0205),
"Invalid Recursion".to_string(), "Invalid Recursion".to_string(),
vec!["Check that recursive calls have proper base cases.".to_string()], vec!["Check that recursive calls have proper base cases.".to_string()],
None,
None,
) )
} else if message_lower.contains("unknown effect") { } else if message_lower.contains("unknown effect") {
( (
Some(ErrorCode::E0402),
"Unknown Effect".to_string(), "Unknown Effect".to_string(),
vec![ vec![
"Make sure the effect is spelled correctly.".to_string(), "Make sure the effect is spelled correctly.".to_string(),
"Built-in effects: Console, File, Process, Http, Random, Time, Sql.".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()], 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") { } else if message_lower.contains("cannot access field") {
( (
Some(ErrorCode::E0308),
"Invalid Field Access".to_string(), "Invalid Field Access".to_string(),
vec!["Field access is only valid on record types.".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")) { } 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![ vec![
"Add the effect to your function's effect list.".to_string(), "Add the effect to your function's effect list.".to_string(),
"Example: fn myFn(): Int with {Console, Sql} = ...".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 { } else {
("Type Error".to_string(), vec![]) (
Some(ErrorCode::E0200),
"Type Error".to_string(),
vec![],
expected,
actual,
)
} }
} }