From 3a462994046a0995bff8c129b49d2d8cbf3db53d Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Mon, 16 Feb 2026 04:11:15 -0500 Subject: [PATCH] feat: Elm-quality error messages with error codes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/diagnostics.rs | 471 +++++++++++++++++++++++++++++++++++++++++++-- src/interpreter.rs | 354 +++++++++++++++++++++++++++++++++- src/main.rs | 219 ++++++++++++++++++++- src/parser.rs | 32 ++- src/typechecker.rs | 169 ++++++++++++++-- 5 files changed, 1200 insertions(+), 45 deletions(-) diff --git a/src/diagnostics.rs b/src/diagnostics.rs index 10c691b..f6578bf 100644 --- a/src/diagnostics.rs +++ b/src/diagnostics.rs @@ -1,9 +1,223 @@ //! 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)] 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 pub mod colors { pub const RESET: &str = "\x1b[0m"; @@ -46,30 +260,57 @@ impl Severity { #[derive(Debug, Clone)] pub struct Diagnostic { pub severity: Severity, + pub code: Option, pub title: String, pub message: String, pub span: Span, pub hints: Vec, + pub expected_type: Option, + pub actual_type: Option, + pub secondary_spans: Vec<(Span, String)>, } impl Diagnostic { pub fn error(title: impl Into, message: impl Into, span: Span) -> Self { Self { severity: Severity::Error, + code: None, title: title.into(), message: message.into(), span, hints: Vec::new(), + expected_type: None, + actual_type: None, + secondary_spans: Vec::new(), } } pub fn warning(title: impl Into, message: impl Into, span: Span) -> Self { Self { severity: Severity::Warning, + code: None, title: title.into(), message: message.into(), span, 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, 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 } + + /// Add expected and actual types for type mismatch visualization + pub fn with_types(mut self, expected: impl Into, actual: impl Into) -> 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) -> 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 { + // Simple case: find the first difference + let exp_chars: Vec = expected.chars().collect(); + let act_chars: Vec = 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 @@ -211,18 +557,23 @@ pub fn render_diagnostic( let severity_color = diagnostic.severity.color(); let severity_label = diagnostic.severity.label(); - // Header: -- ERROR ----------- filename.lux + // Header with error code: ── ERROR[E0201] ── filename.lux let filename_str = filename.unwrap_or(""); + let code_str = diagnostic + .code + .map(|c| format!("[{}]", c.code())) + .unwrap_or_default(); output.push_str(&format!( - "{}{}{} ── {} ──────────────────────────────────\n", + "{}{}── {}{} ──────────────────── {}{}\n", colors::BOLD, severity_color, severity_label, - filename_str + code_str, + filename_str, + colors::RESET )); - output.push_str(colors::RESET); - // Title + // Location output.push_str(&format!( "\n{}{}{}:{}{}\n\n", colors::BOLD, @@ -309,6 +660,11 @@ pub fn render_diagnostic( // Error 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 if !diagnostic.hints.is_empty() { 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 } @@ -338,9 +705,14 @@ pub fn render_diagnostic_plain( let (end_line, end_col) = offset_to_line_col(source, diagnostic.span.end); let filename_str = filename.unwrap_or(""); + let code_str = diagnostic + .code + .map(|c| format!("[{}]", c.code())) + .unwrap_or_default(); output.push_str(&format!( - "-- {} ── {} ──────────────────────────────────\n", + "-- {}{} ── {} ──────────────────────────────────\n", diagnostic.severity.label(), + code_str, filename_str )); @@ -381,6 +753,11 @@ pub fn render_diagnostic_plain( 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() { output.push('\n'); 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 } @@ -478,25 +860,28 @@ mod tests { #[test] fn test_type_error_categorization() { - use super::Diagnostic; + use super::{Diagnostic, ErrorCode}; // 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()], - }; + // Simulate a type mismatch diagnostic with error code + let diag = Diagnostic::with_code( + ErrorCode::E0201, + "Type mismatch: expected Int, got String", + Span { start: 13, end: 20 }, + ) + .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")); assert!(output.contains("Type Mismatch")); assert!(output.contains("\"hello\"")); assert!(output.contains("Hint:")); + assert!(output.contains("[E0201]")); + assert!(output.contains("Expected: Int")); + assert!(output.contains("Found: String")); } #[test] @@ -589,4 +974,60 @@ mod tests { 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())); } + + #[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")); + } } diff --git a/src/interpreter.rs b/src/interpreter.rs index 5806f9c..1e71357 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -3,8 +3,9 @@ #![allow(dead_code, unused_variables)] use crate::ast::*; -use crate::diagnostics::{Diagnostic, Severity}; +use crate::diagnostics::{Diagnostic, ErrorCode, Severity}; use rand::Rng; +use rusqlite::Connection; use std::cell::RefCell; use std::collections::HashMap; use std::fmt; @@ -426,29 +427,47 @@ 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); + let (code, title, hints) = categorize_runtime_error(&self.message); Diagnostic { severity: Severity::Error, + code, title, message: self.message.clone(), span: self.span.unwrap_or_default(), hints, + expected_type: None, + actual_type: None, + secondary_spans: Vec::new(), } } } -/// Categorize runtime errors to provide better titles and hints -fn categorize_runtime_error(message: &str) -> (String, Vec) { +/// Categorize runtime errors to provide better titles, hints, and error codes +fn categorize_runtime_error(message: &str) -> (Option, String, Vec) { 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(), 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") { ( + None, // Runtime error, not a type error "Division by Zero".to_string(), vec![ "Check that the divisor is not zero before dividing.".to_string(), @@ -457,11 +476,13 @@ fn categorize_runtime_error(message: &str) -> (String, Vec) { ) } else if message_lower.contains("type") && message_lower.contains("mismatch") { ( + Some(ErrorCode::E0201), "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") { ( + Some(ErrorCode::E0401), "Unhandled Effect".to_string(), vec![ "This effect must be handled before the program can continue.".to_string(), @@ -470,16 +491,19 @@ fn categorize_runtime_error(message: &str) -> (String, Vec) { ) } else if message_lower.contains("pattern") && message_lower.contains("match") { ( + Some(ErrorCode::E0501), "Non-exhaustive Pattern".to_string(), vec!["Add more patterns to cover all possible cases.".to_string()], ) } else if message_lower.contains("argument") { ( + Some(ErrorCode::E0209), "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") { ( + None, // Runtime error "Index Out of Bounds".to_string(), vec![ "The index is outside the valid range.".to_string(), @@ -487,7 +511,7 @@ fn categorize_runtime_error(message: &str) -> (String, Vec) { ], ) } else { - ("Runtime Error".to_string(), vec![]) + (None, "Runtime Error".to_string(), vec![]) } } @@ -587,6 +611,10 @@ pub struct Interpreter { current_http_request: Arc>>, /// Test results for the Test effect test_results: RefCell, + /// SQL database connections (connection ID -> Connection) + sql_connections: RefCell>, + /// Next SQL connection ID + next_sql_conn_id: RefCell, } /// Results from running tests @@ -627,6 +655,8 @@ impl Interpreter { http_server: Arc::new(Mutex::new(None)), current_http_request: Arc::new(Mutex::new(None)), 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); } + /// 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>, + ) { + 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 pub fn create_versioned(&self, type_name: &str, version: u32, value: Value) -> Value { Value::Versioned { @@ -3712,6 +3768,292 @@ impl Interpreter { 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 = stmt.column_names().iter().map(|s| s.to_string()).collect(); + let column_count = column_names.len(); + + let rows: Result, _> = 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!("", b.len())) + } + Err(_) => Value::String("".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 = 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!("", b.len())) + } + Err(_) => Value::String("".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 { message: format!( "Unhandled effect operation: {}.{}", diff --git a/src/main.rs b/src/main.rs index b5aed5a..9474433 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ mod lsp; mod modules; mod package; mod parser; +mod registry; mod schema; mod typechecker; mod types; @@ -126,6 +127,25 @@ fn main() { // Package manager 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 to native binary or JavaScript if args.len() < 3 { @@ -182,6 +202,9 @@ fn print_help() { println!(" lux debug Start interactive debugger"); println!(" lux init [name] Initialize a new project"); println!(" lux pkg Package manager (install, add, remove, list, update)"); + println!(" lux registry Start package registry server"); + println!(" -s, --storage Storage directory (default: ./lux-registry)"); + println!(" -b, --bind Bind address (default: 127.0.0.1:8080)"); println!(" lux --lsp Start LSP server (for IDE integration)"); println!(" lux --help Show this help"); println!(" lux --version Show version"); @@ -710,6 +733,9 @@ fn run_tests(args: &[String]) { continue; } + // Get auto-generated migrations from typechecker + let auto_migrations = checker.get_auto_migrations().clone(); + // Find test functions (functions starting with test_) let test_funcs: Vec<_> = program.declarations.iter().filter_map(|d| { if let ast::Declaration::Function(f) = d { @@ -723,6 +749,7 @@ fn run_tests(args: &[String]) { if test_funcs.is_empty() { // No test functions, run the whole file let mut interp = Interpreter::new(); + interp.register_auto_migrations(&auto_migrations); interp.reset_test_results(); match interp.run(&program) { @@ -755,6 +782,7 @@ fn run_tests(args: &[String]) { for test_name in &test_funcs { let mut interp = Interpreter::new(); + interp.register_auto_migrations(&auto_migrations); interp.reset_test_results(); // First run the file to define all functions @@ -1046,6 +1074,16 @@ fn handle_pkg_command(args: &[String]) { std::process::exit(1); } } + "search" => { + if args.len() < 2 { + eprintln!("Usage: lux pkg search "); + std::process::exit(1); + } + search_registry(&args[1]); + } + "publish" => { + publish_package(&project_root); + } "help" | "--help" | "-h" => { 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 { + 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::().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() { println!("Lux Package Manager"); println!(); @@ -1075,6 +1270,11 @@ fn print_pkg_help() { println!(" list, ls List dependencies and their status"); println!(" update Update all dependencies"); println!(" clean Remove installed packages"); + println!(" search 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!("Examples:"); 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 local-lib --path ../lib"); println!(" lux pkg remove http"); + println!(" lux pkg search json"); + println!(" lux pkg publish"); } fn init_pkg_here(name: Option<&str>) { @@ -3073,13 +3275,12 @@ c")"#; #[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 diag = Diagnostic::error( + "Type Mismatch", + "Expected Int but got String", + Span { start: 56, end: 61 }, + ) + .with_hint("The second argument should be an Int."); let output = render_diagnostic_plain(&diag, source, Some("example.lux")); @@ -3098,8 +3299,10 @@ c")"#; }; 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"))); + // Check error code is set + assert!(diag.code.is_some()); } #[test] diff --git a/src/parser.rs b/src/parser.rs index 2cf273f..b96953a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3,7 +3,7 @@ #![allow(dead_code)] use crate::ast::*; -use crate::diagnostics::{Diagnostic, Severity}; +use crate::diagnostics::{Diagnostic, ErrorCode, Severity}; use crate::lexer::{LexError, Lexer, StringPart, Token, TokenKind}; use std::fmt; @@ -27,57 +27,79 @@ 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); + let (code, title, hints) = categorize_parse_error(&self.message); Diagnostic { severity: Severity::Error, + code, title, message: self.message.clone(), span: self.span, hints, + expected_type: None, + actual_type: None, + secondary_spans: Vec::new(), } } } -/// Categorize parse errors to provide better titles and hints -fn categorize_parse_error(message: &str) -> (String, Vec) { +/// Categorize parse errors to provide better titles, hints, and error codes +fn categorize_parse_error(message: &str) -> (Option, String, Vec) { let message_lower = message.to_lowercase(); if message_lower.contains("unexpected") && message_lower.contains("expected") { ( + Some(ErrorCode::E0101), "Unexpected Token".to_string(), vec!["Check for missing or misplaced punctuation.".to_string()], ) } else if message_lower.contains("expected") && message_lower.contains("expression") { ( + Some(ErrorCode::E0101), "Missing Expression".to_string(), vec!["An expression was expected here.".to_string()], ) } else if message_lower.contains("expected") && message_lower.contains(":") { ( + Some(ErrorCode::E0104), "Missing Type Annotation".to_string(), vec!["A type annotation is required here.".to_string()], ) } else if message_lower.contains("unclosed") || message_lower.contains("unterminated") { ( + Some(ErrorCode::E0102), "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") && 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") { ( + Some(ErrorCode::E0100), "Invalid Syntax".to_string(), vec!["Check the syntax of this construct.".to_string()], ) } else if message_lower.contains("identifier") { ( + Some(ErrorCode::E0100), "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![]) + (Some(ErrorCode::E0100), "Parse Error".to_string(), vec![]) } } diff --git a/src/typechecker.rs b/src/typechecker.rs index 49187a5..6587249 100644 --- a/src/typechecker.rs +++ b/src/typechecker.rs @@ -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) { +/// Extract expected and actual types from an error message +fn extract_types_from_message(message: &str) -> (Option, Option) { + // 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, String, Vec, Option, Option) { 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, + ) } }