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:
@@ -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<ErrorCode>,
|
||||
pub title: String,
|
||||
pub message: String,
|
||||
pub span: Span,
|
||||
pub hints: Vec<String>,
|
||||
pub expected_type: Option<String>,
|
||||
pub actual_type: Option<String>,
|
||||
pub secondary_spans: Vec<(Span, String)>,
|
||||
}
|
||||
|
||||
impl Diagnostic {
|
||||
pub fn error(title: impl Into<String>, message: impl Into<String>, 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<String>, message: impl Into<String>, 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<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
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -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("<input>");
|
||||
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("<input>");
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>) {
|
||||
/// Categorize runtime errors to provide better titles, hints, and error codes
|
||||
fn categorize_runtime_error(message: &str) -> (Option<ErrorCode>, String, Vec<String>) {
|
||||
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<String>) {
|
||||
)
|
||||
} 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<String>) {
|
||||
)
|
||||
} 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<String>) {
|
||||
],
|
||||
)
|
||||
} 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>>>,
|
||||
/// Test results for the Test effect
|
||||
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
|
||||
@@ -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<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
|
||||
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<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 {
|
||||
message: format!(
|
||||
"Unhandled effect operation: {}.{}",
|
||||
|
||||
219
src/main.rs
219
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 <file.lux> Start interactive debugger");
|
||||
println!(" lux init [name] Initialize a new project");
|
||||
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 --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 <query>");
|
||||
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<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() {
|
||||
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 <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!("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]
|
||||
|
||||
@@ -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<String>) {
|
||||
/// Categorize parse errors to provide better titles, hints, and error codes
|
||||
fn categorize_parse_error(message: &str) -> (Option<ErrorCode>, String, Vec<String>) {
|
||||
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![])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::ast::{
|
||||
ImportDecl, LetDecl, Literal, LiteralKind, MatchArm, Parameter, Pattern, Program, Span,
|
||||
Statement, TraitDecl, TypeDecl, TypeExpr, UnaryOp, VariantFields,
|
||||
};
|
||||
use crate::diagnostics::{find_similar_names, format_did_you_mean, Diagnostic, Severity};
|
||||
use crate::diagnostics::{find_similar_names, format_did_you_mean, Diagnostic, ErrorCode, Severity};
|
||||
use crate::exhaustiveness::{check_exhaustiveness, missing_patterns_hint};
|
||||
use crate::modules::ModuleLoader;
|
||||
use crate::schema::{SchemaRegistry, Compatibility, BreakingChange, AutoMigration};
|
||||
@@ -39,92 +39,239 @@ impl std::fmt::Display for TypeError {
|
||||
impl TypeError {
|
||||
/// Convert to a rich diagnostic for Elm-style error display
|
||||
pub fn to_diagnostic(&self) -> Diagnostic {
|
||||
// Categorize the error and extract hints
|
||||
let (title, hints) = categorize_type_error(&self.message);
|
||||
// Categorize the error and extract hints, error code, and type info
|
||||
let (code, title, hints, expected, actual) = categorize_type_error(&self.message);
|
||||
|
||||
Diagnostic {
|
||||
severity: Severity::Error,
|
||||
code,
|
||||
title,
|
||||
message: self.message.clone(),
|
||||
span: self.span,
|
||||
hints,
|
||||
expected_type: expected,
|
||||
actual_type: actual,
|
||||
secondary_spans: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Categorize a type error message to provide better titles and hints
|
||||
fn categorize_type_error(message: &str) -> (String, Vec<String>) {
|
||||
/// Extract expected and actual types from an error message
|
||||
fn extract_types_from_message(message: &str) -> (Option<String>, Option<String>) {
|
||||
// Try to extract "expected X, got Y" patterns
|
||||
let patterns = [
|
||||
("expected ", ", got "),
|
||||
("expected ", " but got "),
|
||||
("expected ", " found "),
|
||||
];
|
||||
|
||||
for (exp_prefix, got_prefix) in patterns {
|
||||
if let Some(exp_start) = message.to_lowercase().find(exp_prefix) {
|
||||
let after_expected = &message[exp_start + exp_prefix.len()..];
|
||||
if let Some(got_pos) = after_expected.to_lowercase().find(got_prefix) {
|
||||
let expected = after_expected[..got_pos].trim().to_string();
|
||||
let after_got = &after_expected[got_pos + got_prefix.len()..];
|
||||
// Take until end of word or punctuation
|
||||
let actual = after_got
|
||||
.split(|c: char| c == ',' || c == '.' || c == ';' || c == ')' || c.is_whitespace())
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string();
|
||||
if !expected.is_empty() && !actual.is_empty() {
|
||||
return (Some(expected), Some(actual));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try "cannot unify X with Y" pattern
|
||||
if let Some(unify_pos) = message.to_lowercase().find("cannot unify ") {
|
||||
let after_unify = &message[unify_pos + 13..];
|
||||
if let Some(with_pos) = after_unify.to_lowercase().find(" with ") {
|
||||
let type1 = after_unify[..with_pos].trim().to_string();
|
||||
let type2 = after_unify[with_pos + 6..]
|
||||
.split(|c: char| c == ',' || c == '.' || c == ';' || c == ')' || c.is_whitespace())
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string();
|
||||
if !type1.is_empty() && !type2.is_empty() {
|
||||
return (Some(type1), Some(type2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(None, None)
|
||||
}
|
||||
|
||||
/// Categorize a type error message to provide better titles, hints, and error codes
|
||||
fn categorize_type_error(message: &str) -> (Option<ErrorCode>, String, Vec<String>, Option<String>, Option<String>) {
|
||||
let message_lower = message.to_lowercase();
|
||||
let (expected, actual) = extract_types_from_message(message);
|
||||
|
||||
if message_lower.contains("type mismatch") {
|
||||
(
|
||||
Some(ErrorCode::E0201),
|
||||
"Type Mismatch".to_string(),
|
||||
vec!["Check that the types on both sides of the expression are compatible.".to_string()],
|
||||
expected,
|
||||
actual,
|
||||
)
|
||||
} else if message_lower.contains("undefined variable") || message_lower.contains("not found") {
|
||||
} else if message_lower.contains("undefined variable") {
|
||||
(
|
||||
Some(ErrorCode::E0301),
|
||||
"Undefined Variable".to_string(),
|
||||
vec![
|
||||
"Check the spelling of the variable name.".to_string(),
|
||||
"Make sure the variable is defined before use.".to_string(),
|
||||
],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
} else if message_lower.contains("undefined function") || message_lower.contains("function") && message_lower.contains("not found") {
|
||||
(
|
||||
Some(ErrorCode::E0302),
|
||||
"Undefined Function".to_string(),
|
||||
vec![
|
||||
"Check the spelling of the function name.".to_string(),
|
||||
"Make sure the function is imported or defined.".to_string(),
|
||||
],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
} else if message_lower.contains("not found") || message_lower.contains("unknown") && message_lower.contains("name") {
|
||||
(
|
||||
Some(ErrorCode::E0301),
|
||||
"Unknown Name".to_string(),
|
||||
vec![
|
||||
"Check the spelling of the name.".to_string(),
|
||||
"Make sure the variable is defined before use.".to_string(),
|
||||
],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
} else if message_lower.contains("cannot unify") {
|
||||
(
|
||||
"Type Mismatch".to_string(),
|
||||
Some(ErrorCode::E0202),
|
||||
"Cannot Unify Types".to_string(),
|
||||
vec!["The types are not compatible. Check your function arguments and return types.".to_string()],
|
||||
expected,
|
||||
actual,
|
||||
)
|
||||
} else if message_lower.contains("expected") && message_lower.contains("argument") {
|
||||
(
|
||||
Some(ErrorCode::E0209),
|
||||
"Wrong Number of Arguments".to_string(),
|
||||
vec!["Check the function signature and provide the correct number of arguments.".to_string()],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
} else if message_lower.contains("pure") && message_lower.contains("effect") {
|
||||
(
|
||||
Some(ErrorCode::E0701),
|
||||
"Purity Violation".to_string(),
|
||||
vec![
|
||||
"Functions marked 'is pure' cannot perform effects.".to_string(),
|
||||
"Remove the 'is pure' annotation or handle the effects.".to_string(),
|
||||
],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
} else if message_lower.contains("effect") && message_lower.contains("unhandled") {
|
||||
(
|
||||
Some(ErrorCode::E0401),
|
||||
"Unhandled Effect".to_string(),
|
||||
vec!["Use a 'handle' expression to provide an implementation for this effect.".to_string()],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
} else if message_lower.contains("recursive") {
|
||||
(
|
||||
Some(ErrorCode::E0205),
|
||||
"Invalid Recursion".to_string(),
|
||||
vec!["Check that recursive calls have proper base cases.".to_string()],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
} else if message_lower.contains("unknown effect") {
|
||||
(
|
||||
Some(ErrorCode::E0402),
|
||||
"Unknown Effect".to_string(),
|
||||
vec![
|
||||
"Make sure the effect is spelled correctly.".to_string(),
|
||||
"Built-in effects: Console, File, Process, Http, Random, Time, Sql.".to_string(),
|
||||
],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
} else if message_lower.contains("record has no field") {
|
||||
} else if message_lower.contains("record has no field") || message_lower.contains("missing field") {
|
||||
(
|
||||
"Missing Field".to_string(),
|
||||
Some(ErrorCode::E0207),
|
||||
"Missing Record Field".to_string(),
|
||||
vec!["Check the field name spelling or review the record definition.".to_string()],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
} else if message_lower.contains("undefined field") {
|
||||
(
|
||||
Some(ErrorCode::E0308),
|
||||
"Undefined Field".to_string(),
|
||||
vec!["Check the field name spelling or review the record definition.".to_string()],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
} else if message_lower.contains("cannot access field") {
|
||||
(
|
||||
Some(ErrorCode::E0308),
|
||||
"Invalid Field Access".to_string(),
|
||||
vec!["Field access is only valid on record types.".to_string()],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
} else if message_lower.contains("effect") && (message_lower.contains("not available") || message_lower.contains("missing")) {
|
||||
(
|
||||
"Missing Effect".to_string(),
|
||||
Some(ErrorCode::E0403),
|
||||
"Missing Effect Handler".to_string(),
|
||||
vec![
|
||||
"Add the effect to your function's effect list.".to_string(),
|
||||
"Example: fn myFn(): Int with {Console, Sql} = ...".to_string(),
|
||||
],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
} else if message_lower.contains("non-exhaustive") || message_lower.contains("exhaustive") {
|
||||
(
|
||||
Some(ErrorCode::E0501),
|
||||
"Non-Exhaustive Patterns".to_string(),
|
||||
vec!["Add patterns for the missing cases or use a wildcard pattern '_'.".to_string()],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
} else if message_lower.contains("duplicate") {
|
||||
(
|
||||
Some(ErrorCode::E0305),
|
||||
"Duplicate Definition".to_string(),
|
||||
vec!["Each name can only be defined once in the same scope.".to_string()],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
} else if message_lower.contains("return type") {
|
||||
(
|
||||
Some(ErrorCode::E0211),
|
||||
"Return Type Mismatch".to_string(),
|
||||
vec!["The function body's type doesn't match the declared return type.".to_string()],
|
||||
expected,
|
||||
actual,
|
||||
)
|
||||
} else {
|
||||
("Type Error".to_string(), vec![])
|
||||
(
|
||||
Some(ErrorCode::E0200),
|
||||
"Type Error".to_string(),
|
||||
vec![],
|
||||
expected,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user