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
|
//! Elm-style diagnostic messages for beautiful error reporting
|
||||||
|
//!
|
||||||
|
//! This module provides Elm-quality error messages with:
|
||||||
|
//! - Error codes (E0001, E0002, etc.) for easy searchability
|
||||||
|
//! - Visual type diffs showing expected vs actual
|
||||||
|
//! - "Did you mean?" suggestions using Levenshtein distance
|
||||||
|
//! - Context-aware hints based on error type
|
||||||
|
//! - Colored output with syntax highlighting
|
||||||
|
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use crate::ast::Span;
|
use crate::ast::Span;
|
||||||
|
|
||||||
|
/// Error codes for all Lux compiler errors
|
||||||
|
/// These follow the pattern E{category}{number}:
|
||||||
|
/// - E01xx: Parse errors
|
||||||
|
/// - E02xx: Type errors
|
||||||
|
/// - E03xx: Name resolution errors
|
||||||
|
/// - E04xx: Effect errors
|
||||||
|
/// - E05xx: Pattern matching errors
|
||||||
|
/// - E06xx: Import/module errors
|
||||||
|
/// - E07xx: Behavioral type errors
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ErrorCode {
|
||||||
|
// Parse errors (E01xx)
|
||||||
|
E0100, // Generic parse error
|
||||||
|
E0101, // Unexpected token
|
||||||
|
E0102, // Unclosed delimiter
|
||||||
|
E0103, // Invalid literal
|
||||||
|
E0104, // Missing semicolon or delimiter
|
||||||
|
E0105, // Invalid operator
|
||||||
|
|
||||||
|
// Type errors (E02xx)
|
||||||
|
E0200, // Generic type error
|
||||||
|
E0201, // Type mismatch
|
||||||
|
E0202, // Cannot unify types
|
||||||
|
E0203, // Missing type annotation
|
||||||
|
E0204, // Invalid type application
|
||||||
|
E0205, // Recursive type without indirection
|
||||||
|
E0206, // Field type mismatch
|
||||||
|
E0207, // Missing record field
|
||||||
|
E0208, // Extra record field
|
||||||
|
E0209, // Wrong number of type arguments
|
||||||
|
E0210, // Type not found
|
||||||
|
E0211, // Return type mismatch
|
||||||
|
|
||||||
|
// Name resolution errors (E03xx)
|
||||||
|
E0300, // Generic name error
|
||||||
|
E0301, // Undefined variable
|
||||||
|
E0302, // Undefined function
|
||||||
|
E0303, // Undefined type
|
||||||
|
E0304, // Undefined module
|
||||||
|
E0305, // Duplicate definition
|
||||||
|
E0306, // Shadowing warning
|
||||||
|
E0307, // Unused variable warning
|
||||||
|
E0308, // Undefined field
|
||||||
|
|
||||||
|
// Effect errors (E04xx)
|
||||||
|
E0400, // Generic effect error
|
||||||
|
E0401, // Unhandled effect
|
||||||
|
E0402, // Effect not in scope
|
||||||
|
E0403, // Missing effect handler
|
||||||
|
E0404, // Invalid effect operation
|
||||||
|
E0405, // Effect mismatch
|
||||||
|
|
||||||
|
// Pattern matching errors (E05xx)
|
||||||
|
E0500, // Generic pattern error
|
||||||
|
E0501, // Non-exhaustive patterns
|
||||||
|
E0502, // Unreachable pattern
|
||||||
|
E0503, // Invalid pattern
|
||||||
|
E0504, // Duplicate pattern binding
|
||||||
|
|
||||||
|
// Import/module errors (E06xx)
|
||||||
|
E0600, // Generic module error
|
||||||
|
E0601, // Module not found
|
||||||
|
E0602, // Circular import
|
||||||
|
E0603, // Export not found
|
||||||
|
E0604, // Private item accessed
|
||||||
|
|
||||||
|
// Behavioral type errors (E07xx)
|
||||||
|
E0700, // Generic behavioral error
|
||||||
|
E0701, // Purity violation
|
||||||
|
E0702, // Totality violation
|
||||||
|
E0703, // Idempotency violation
|
||||||
|
E0704, // Commutativity violation
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorCode {
|
||||||
|
pub fn code(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
// Parse errors
|
||||||
|
ErrorCode::E0100 => "E0100",
|
||||||
|
ErrorCode::E0101 => "E0101",
|
||||||
|
ErrorCode::E0102 => "E0102",
|
||||||
|
ErrorCode::E0103 => "E0103",
|
||||||
|
ErrorCode::E0104 => "E0104",
|
||||||
|
ErrorCode::E0105 => "E0105",
|
||||||
|
// Type errors
|
||||||
|
ErrorCode::E0200 => "E0200",
|
||||||
|
ErrorCode::E0201 => "E0201",
|
||||||
|
ErrorCode::E0202 => "E0202",
|
||||||
|
ErrorCode::E0203 => "E0203",
|
||||||
|
ErrorCode::E0204 => "E0204",
|
||||||
|
ErrorCode::E0205 => "E0205",
|
||||||
|
ErrorCode::E0206 => "E0206",
|
||||||
|
ErrorCode::E0207 => "E0207",
|
||||||
|
ErrorCode::E0208 => "E0208",
|
||||||
|
ErrorCode::E0209 => "E0209",
|
||||||
|
ErrorCode::E0210 => "E0210",
|
||||||
|
ErrorCode::E0211 => "E0211",
|
||||||
|
// Name errors
|
||||||
|
ErrorCode::E0300 => "E0300",
|
||||||
|
ErrorCode::E0301 => "E0301",
|
||||||
|
ErrorCode::E0302 => "E0302",
|
||||||
|
ErrorCode::E0303 => "E0303",
|
||||||
|
ErrorCode::E0304 => "E0304",
|
||||||
|
ErrorCode::E0305 => "E0305",
|
||||||
|
ErrorCode::E0306 => "E0306",
|
||||||
|
ErrorCode::E0307 => "E0307",
|
||||||
|
ErrorCode::E0308 => "E0308",
|
||||||
|
// Effect errors
|
||||||
|
ErrorCode::E0400 => "E0400",
|
||||||
|
ErrorCode::E0401 => "E0401",
|
||||||
|
ErrorCode::E0402 => "E0402",
|
||||||
|
ErrorCode::E0403 => "E0403",
|
||||||
|
ErrorCode::E0404 => "E0404",
|
||||||
|
ErrorCode::E0405 => "E0405",
|
||||||
|
// Pattern errors
|
||||||
|
ErrorCode::E0500 => "E0500",
|
||||||
|
ErrorCode::E0501 => "E0501",
|
||||||
|
ErrorCode::E0502 => "E0502",
|
||||||
|
ErrorCode::E0503 => "E0503",
|
||||||
|
ErrorCode::E0504 => "E0504",
|
||||||
|
// Module errors
|
||||||
|
ErrorCode::E0600 => "E0600",
|
||||||
|
ErrorCode::E0601 => "E0601",
|
||||||
|
ErrorCode::E0602 => "E0602",
|
||||||
|
ErrorCode::E0603 => "E0603",
|
||||||
|
ErrorCode::E0604 => "E0604",
|
||||||
|
// Behavioral errors
|
||||||
|
ErrorCode::E0700 => "E0700",
|
||||||
|
ErrorCode::E0701 => "E0701",
|
||||||
|
ErrorCode::E0702 => "E0702",
|
||||||
|
ErrorCode::E0703 => "E0703",
|
||||||
|
ErrorCode::E0704 => "E0704",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
// Parse errors
|
||||||
|
ErrorCode::E0100 => "Parse Error",
|
||||||
|
ErrorCode::E0101 => "Unexpected Token",
|
||||||
|
ErrorCode::E0102 => "Unclosed Delimiter",
|
||||||
|
ErrorCode::E0103 => "Invalid Literal",
|
||||||
|
ErrorCode::E0104 => "Missing Delimiter",
|
||||||
|
ErrorCode::E0105 => "Invalid Operator",
|
||||||
|
// Type errors
|
||||||
|
ErrorCode::E0200 => "Type Error",
|
||||||
|
ErrorCode::E0201 => "Type Mismatch",
|
||||||
|
ErrorCode::E0202 => "Cannot Unify Types",
|
||||||
|
ErrorCode::E0203 => "Missing Type Annotation",
|
||||||
|
ErrorCode::E0204 => "Invalid Type Application",
|
||||||
|
ErrorCode::E0205 => "Recursive Type Error",
|
||||||
|
ErrorCode::E0206 => "Field Type Mismatch",
|
||||||
|
ErrorCode::E0207 => "Missing Record Field",
|
||||||
|
ErrorCode::E0208 => "Extra Record Field",
|
||||||
|
ErrorCode::E0209 => "Wrong Type Arity",
|
||||||
|
ErrorCode::E0210 => "Type Not Found",
|
||||||
|
ErrorCode::E0211 => "Return Type Mismatch",
|
||||||
|
// Name errors
|
||||||
|
ErrorCode::E0300 => "Name Error",
|
||||||
|
ErrorCode::E0301 => "Undefined Variable",
|
||||||
|
ErrorCode::E0302 => "Undefined Function",
|
||||||
|
ErrorCode::E0303 => "Undefined Type",
|
||||||
|
ErrorCode::E0304 => "Undefined Module",
|
||||||
|
ErrorCode::E0305 => "Duplicate Definition",
|
||||||
|
ErrorCode::E0306 => "Variable Shadowing",
|
||||||
|
ErrorCode::E0307 => "Unused Variable",
|
||||||
|
ErrorCode::E0308 => "Undefined Field",
|
||||||
|
// Effect errors
|
||||||
|
ErrorCode::E0400 => "Effect Error",
|
||||||
|
ErrorCode::E0401 => "Unhandled Effect",
|
||||||
|
ErrorCode::E0402 => "Effect Not In Scope",
|
||||||
|
ErrorCode::E0403 => "Missing Effect Handler",
|
||||||
|
ErrorCode::E0404 => "Invalid Effect Operation",
|
||||||
|
ErrorCode::E0405 => "Effect Mismatch",
|
||||||
|
// Pattern errors
|
||||||
|
ErrorCode::E0500 => "Pattern Error",
|
||||||
|
ErrorCode::E0501 => "Non-Exhaustive Patterns",
|
||||||
|
ErrorCode::E0502 => "Unreachable Pattern",
|
||||||
|
ErrorCode::E0503 => "Invalid Pattern",
|
||||||
|
ErrorCode::E0504 => "Duplicate Pattern Binding",
|
||||||
|
// Module errors
|
||||||
|
ErrorCode::E0600 => "Module Error",
|
||||||
|
ErrorCode::E0601 => "Module Not Found",
|
||||||
|
ErrorCode::E0602 => "Circular Import",
|
||||||
|
ErrorCode::E0603 => "Export Not Found",
|
||||||
|
ErrorCode::E0604 => "Private Item Access",
|
||||||
|
// Behavioral errors
|
||||||
|
ErrorCode::E0700 => "Behavioral Type Error",
|
||||||
|
ErrorCode::E0701 => "Purity Violation",
|
||||||
|
ErrorCode::E0702 => "Totality Violation",
|
||||||
|
ErrorCode::E0703 => "Idempotency Violation",
|
||||||
|
ErrorCode::E0704 => "Commutativity Violation",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a URL to documentation about this error
|
||||||
|
pub fn help_url(&self) -> String {
|
||||||
|
format!("https://lux-lang.dev/errors/{}", self.code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ErrorCode {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// ANSI color codes for terminal output
|
/// ANSI color codes for terminal output
|
||||||
pub mod colors {
|
pub mod colors {
|
||||||
pub const RESET: &str = "\x1b[0m";
|
pub const RESET: &str = "\x1b[0m";
|
||||||
@@ -46,30 +260,57 @@ impl Severity {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Diagnostic {
|
pub struct Diagnostic {
|
||||||
pub severity: Severity,
|
pub severity: Severity,
|
||||||
|
pub code: Option<ErrorCode>,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub span: Span,
|
pub span: Span,
|
||||||
pub hints: Vec<String>,
|
pub hints: Vec<String>,
|
||||||
|
pub expected_type: Option<String>,
|
||||||
|
pub actual_type: Option<String>,
|
||||||
|
pub secondary_spans: Vec<(Span, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Diagnostic {
|
impl Diagnostic {
|
||||||
pub fn error(title: impl Into<String>, message: impl Into<String>, span: Span) -> Self {
|
pub fn error(title: impl Into<String>, message: impl Into<String>, span: Span) -> Self {
|
||||||
Self {
|
Self {
|
||||||
severity: Severity::Error,
|
severity: Severity::Error,
|
||||||
|
code: None,
|
||||||
title: title.into(),
|
title: title.into(),
|
||||||
message: message.into(),
|
message: message.into(),
|
||||||
span,
|
span,
|
||||||
hints: Vec::new(),
|
hints: Vec::new(),
|
||||||
|
expected_type: None,
|
||||||
|
actual_type: None,
|
||||||
|
secondary_spans: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn warning(title: impl Into<String>, message: impl Into<String>, span: Span) -> Self {
|
pub fn warning(title: impl Into<String>, message: impl Into<String>, span: Span) -> Self {
|
||||||
Self {
|
Self {
|
||||||
severity: Severity::Warning,
|
severity: Severity::Warning,
|
||||||
|
code: None,
|
||||||
title: title.into(),
|
title: title.into(),
|
||||||
message: message.into(),
|
message: message.into(),
|
||||||
span,
|
span,
|
||||||
hints: Vec::new(),
|
hints: Vec::new(),
|
||||||
|
expected_type: None,
|
||||||
|
actual_type: None,
|
||||||
|
secondary_spans: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an error with a specific error code
|
||||||
|
pub fn with_code(code: ErrorCode, message: impl Into<String>, span: Span) -> Self {
|
||||||
|
Self {
|
||||||
|
severity: Severity::Error,
|
||||||
|
code: Some(code),
|
||||||
|
title: code.title().to_string(),
|
||||||
|
message: message.into(),
|
||||||
|
span,
|
||||||
|
hints: Vec::new(),
|
||||||
|
expected_type: None,
|
||||||
|
actual_type: None,
|
||||||
|
secondary_spans: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +323,111 @@ impl Diagnostic {
|
|||||||
self.hints.extend(hints);
|
self.hints.extend(hints);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add expected and actual types for type mismatch visualization
|
||||||
|
pub fn with_types(mut self, expected: impl Into<String>, actual: impl Into<String>) -> Self {
|
||||||
|
self.expected_type = Some(expected.into());
|
||||||
|
self.actual_type = Some(actual.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a secondary span with a label (e.g., "defined here")
|
||||||
|
pub fn with_secondary(mut self, span: Span, label: impl Into<String>) -> Self {
|
||||||
|
self.secondary_spans.push((span, label.into()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the error code
|
||||||
|
pub fn code(mut self, code: ErrorCode) -> Self {
|
||||||
|
self.code = Some(code);
|
||||||
|
self.title = code.title().to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a visual type diff between expected and actual types
|
||||||
|
/// Highlights the differences in red
|
||||||
|
pub fn format_type_diff(expected: &str, actual: &str) -> String {
|
||||||
|
let mut output = String::new();
|
||||||
|
|
||||||
|
output.push_str(&format!(
|
||||||
|
"\n {}{}Expected:{} {}{}{}\n",
|
||||||
|
colors::BOLD,
|
||||||
|
colors::CYAN,
|
||||||
|
colors::RESET,
|
||||||
|
colors::BOLD,
|
||||||
|
expected,
|
||||||
|
colors::RESET
|
||||||
|
));
|
||||||
|
output.push_str(&format!(
|
||||||
|
" {}{}Found: {} {}{}{}\n",
|
||||||
|
colors::BOLD,
|
||||||
|
colors::RED,
|
||||||
|
colors::RESET,
|
||||||
|
colors::BOLD,
|
||||||
|
actual,
|
||||||
|
colors::RESET
|
||||||
|
));
|
||||||
|
|
||||||
|
// If types are structurally similar, show where they differ
|
||||||
|
if let Some(diff) = compute_type_diff(expected, actual) {
|
||||||
|
output.push_str(&format!("\n {}\n", diff));
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute a simple diff between two type strings
|
||||||
|
fn compute_type_diff(expected: &str, actual: &str) -> Option<String> {
|
||||||
|
// Simple case: find the first difference
|
||||||
|
let exp_chars: Vec<char> = expected.chars().collect();
|
||||||
|
let act_chars: Vec<char> = actual.chars().collect();
|
||||||
|
|
||||||
|
let mut diff_start = None;
|
||||||
|
|
||||||
|
for (i, (e, a)) in exp_chars.iter().zip(act_chars.iter()).enumerate() {
|
||||||
|
if e != a && diff_start.is_none() {
|
||||||
|
diff_start = Some(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the types are identical, no diff needed
|
||||||
|
if diff_start.is_none() && exp_chars.len() == act_chars.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the difference location with an arrow
|
||||||
|
if let Some(start) = diff_start {
|
||||||
|
let pointer = format!(
|
||||||
|
"{}{}^-- difference starts here{}",
|
||||||
|
" ".repeat(start + 9), // Account for "Found: " prefix
|
||||||
|
colors::YELLOW,
|
||||||
|
colors::RESET
|
||||||
|
);
|
||||||
|
return Some(pointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Length difference
|
||||||
|
if exp_chars.len() != act_chars.len() {
|
||||||
|
return Some(format!(
|
||||||
|
"{}Note:{} Types have different lengths ({} vs {} characters)",
|
||||||
|
colors::YELLOW,
|
||||||
|
colors::RESET,
|
||||||
|
exp_chars.len(),
|
||||||
|
act_chars.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a plain type diff (no colors)
|
||||||
|
pub fn format_type_diff_plain(expected: &str, actual: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"\n Expected: {}\n Found: {}\n",
|
||||||
|
expected, actual
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate the Levenshtein edit distance between two strings
|
/// Calculate the Levenshtein edit distance between two strings
|
||||||
@@ -211,18 +557,23 @@ pub fn render_diagnostic(
|
|||||||
let severity_color = diagnostic.severity.color();
|
let severity_color = diagnostic.severity.color();
|
||||||
let severity_label = diagnostic.severity.label();
|
let severity_label = diagnostic.severity.label();
|
||||||
|
|
||||||
// Header: -- ERROR ----------- filename.lux
|
// Header with error code: ── ERROR[E0201] ── filename.lux
|
||||||
let filename_str = filename.unwrap_or("<input>");
|
let filename_str = filename.unwrap_or("<input>");
|
||||||
|
let code_str = diagnostic
|
||||||
|
.code
|
||||||
|
.map(|c| format!("[{}]", c.code()))
|
||||||
|
.unwrap_or_default();
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"{}{}{} ── {} ──────────────────────────────────\n",
|
"{}{}── {}{} ──────────────────── {}{}\n",
|
||||||
colors::BOLD,
|
colors::BOLD,
|
||||||
severity_color,
|
severity_color,
|
||||||
severity_label,
|
severity_label,
|
||||||
filename_str
|
code_str,
|
||||||
|
filename_str,
|
||||||
|
colors::RESET
|
||||||
));
|
));
|
||||||
output.push_str(colors::RESET);
|
|
||||||
|
|
||||||
// Title
|
// Location
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"\n{}{}{}:{}{}\n\n",
|
"\n{}{}{}:{}{}\n\n",
|
||||||
colors::BOLD,
|
colors::BOLD,
|
||||||
@@ -309,6 +660,11 @@ pub fn render_diagnostic(
|
|||||||
// Error message
|
// Error message
|
||||||
output.push_str(&format!("\n{}\n", diagnostic.message));
|
output.push_str(&format!("\n{}\n", diagnostic.message));
|
||||||
|
|
||||||
|
// Type diff visualization (if present)
|
||||||
|
if let (Some(expected), Some(actual)) = (&diagnostic.expected_type, &diagnostic.actual_type) {
|
||||||
|
output.push_str(&format_type_diff(expected, actual));
|
||||||
|
}
|
||||||
|
|
||||||
// Hints
|
// Hints
|
||||||
if !diagnostic.hints.is_empty() {
|
if !diagnostic.hints.is_empty() {
|
||||||
output.push('\n');
|
output.push('\n');
|
||||||
@@ -323,6 +679,17 @@ pub fn render_diagnostic(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Help URL for error codes
|
||||||
|
if let Some(code) = diagnostic.code {
|
||||||
|
output.push_str(&format!(
|
||||||
|
"\n{}{}Learn more:{} {}\n",
|
||||||
|
colors::DIM,
|
||||||
|
colors::BLUE,
|
||||||
|
colors::RESET,
|
||||||
|
code.help_url()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
output.push('\n');
|
output.push('\n');
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
@@ -338,9 +705,14 @@ pub fn render_diagnostic_plain(
|
|||||||
let (end_line, end_col) = offset_to_line_col(source, diagnostic.span.end);
|
let (end_line, end_col) = offset_to_line_col(source, diagnostic.span.end);
|
||||||
|
|
||||||
let filename_str = filename.unwrap_or("<input>");
|
let filename_str = filename.unwrap_or("<input>");
|
||||||
|
let code_str = diagnostic
|
||||||
|
.code
|
||||||
|
.map(|c| format!("[{}]", c.code()))
|
||||||
|
.unwrap_or_default();
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"-- {} ── {} ──────────────────────────────────\n",
|
"-- {}{} ── {} ──────────────────────────────────\n",
|
||||||
diagnostic.severity.label(),
|
diagnostic.severity.label(),
|
||||||
|
code_str,
|
||||||
filename_str
|
filename_str
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -381,6 +753,11 @@ pub fn render_diagnostic_plain(
|
|||||||
|
|
||||||
output.push_str(&format!("\n{}\n", diagnostic.message));
|
output.push_str(&format!("\n{}\n", diagnostic.message));
|
||||||
|
|
||||||
|
// Type diff visualization (plain)
|
||||||
|
if let (Some(expected), Some(actual)) = (&diagnostic.expected_type, &diagnostic.actual_type) {
|
||||||
|
output.push_str(&format_type_diff_plain(expected, actual));
|
||||||
|
}
|
||||||
|
|
||||||
if !diagnostic.hints.is_empty() {
|
if !diagnostic.hints.is_empty() {
|
||||||
output.push('\n');
|
output.push('\n');
|
||||||
for hint in &diagnostic.hints {
|
for hint in &diagnostic.hints {
|
||||||
@@ -388,6 +765,11 @@ pub fn render_diagnostic_plain(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Help URL for error codes
|
||||||
|
if let Some(code) = diagnostic.code {
|
||||||
|
output.push_str(&format!("\nLearn more: {}\n", code.help_url()));
|
||||||
|
}
|
||||||
|
|
||||||
output.push('\n');
|
output.push('\n');
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
@@ -478,25 +860,28 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_type_error_categorization() {
|
fn test_type_error_categorization() {
|
||||||
use super::Diagnostic;
|
use super::{Diagnostic, ErrorCode};
|
||||||
|
|
||||||
// Test that errors are properly categorized
|
// Test that errors are properly categorized
|
||||||
let source = "let x: Int = \"hello\"";
|
let source = "let x: Int = \"hello\"";
|
||||||
|
|
||||||
// Simulate a type mismatch diagnostic
|
// Simulate a type mismatch diagnostic with error code
|
||||||
let diag = Diagnostic {
|
let diag = Diagnostic::with_code(
|
||||||
severity: Severity::Error,
|
ErrorCode::E0201,
|
||||||
title: "Type Mismatch".to_string(),
|
"Type mismatch: expected Int, got String",
|
||||||
message: "Type mismatch: expected Int, got String".to_string(),
|
Span { start: 13, end: 20 },
|
||||||
span: Span { start: 13, end: 20 },
|
)
|
||||||
hints: vec!["Check that the types on both sides of the expression are compatible.".to_string()],
|
.with_types("Int", "String")
|
||||||
};
|
.with_hint("Check that the types on both sides of the expression are compatible.");
|
||||||
|
|
||||||
let output = render_diagnostic_plain(&diag, source, Some("test.lux"));
|
let output = render_diagnostic_plain(&diag, source, Some("test.lux"));
|
||||||
|
|
||||||
assert!(output.contains("Type Mismatch"));
|
assert!(output.contains("Type Mismatch"));
|
||||||
assert!(output.contains("\"hello\""));
|
assert!(output.contains("\"hello\""));
|
||||||
assert!(output.contains("Hint:"));
|
assert!(output.contains("Hint:"));
|
||||||
|
assert!(output.contains("[E0201]"));
|
||||||
|
assert!(output.contains("Expected: Int"));
|
||||||
|
assert!(output.contains("Found: String"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -589,4 +974,60 @@ mod tests {
|
|||||||
let hint = super::format_did_you_mean(&["a".to_string(), "b".to_string(), "c".to_string()]);
|
let hint = super::format_did_you_mean(&["a".to_string(), "b".to_string(), "c".to_string()]);
|
||||||
assert_eq!(hint, Some("Did you mean 'a', 'b', or 'c'?".to_string()));
|
assert_eq!(hint, Some("Did you mean 'a', 'b', or 'c'?".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_error_code_display() {
|
||||||
|
assert_eq!(super::ErrorCode::E0201.code(), "E0201");
|
||||||
|
assert_eq!(super::ErrorCode::E0201.title(), "Type Mismatch");
|
||||||
|
assert!(super::ErrorCode::E0201.help_url().contains("E0201"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_diagnostic_with_code() {
|
||||||
|
let diag = super::Diagnostic::with_code(
|
||||||
|
super::ErrorCode::E0301,
|
||||||
|
"Variable 'x' not found",
|
||||||
|
Span { start: 0, end: 1 },
|
||||||
|
);
|
||||||
|
assert_eq!(diag.code, Some(super::ErrorCode::E0301));
|
||||||
|
assert_eq!(diag.title, "Undefined Variable");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_diagnostic_with_types() {
|
||||||
|
let diag = super::Diagnostic::error("Test", "Msg", Span { start: 0, end: 1 })
|
||||||
|
.with_types("Int", "String");
|
||||||
|
assert_eq!(diag.expected_type, Some("Int".to_string()));
|
||||||
|
assert_eq!(diag.actual_type, Some("String".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_type_diff_plain() {
|
||||||
|
let diff = super::format_type_diff_plain("Int", "String");
|
||||||
|
assert!(diff.contains("Expected: Int"));
|
||||||
|
assert!(diff.contains("Found: String"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_diagnostic_render_with_all_features() {
|
||||||
|
let source = "let x: Int = \"hello\"";
|
||||||
|
let diag = super::Diagnostic::with_code(
|
||||||
|
super::ErrorCode::E0201,
|
||||||
|
"This value should be an Int but is a String",
|
||||||
|
Span { start: 13, end: 20 },
|
||||||
|
)
|
||||||
|
.with_types("Int", "String")
|
||||||
|
.with_hint("Try using String.parseInt to convert the string to an integer");
|
||||||
|
|
||||||
|
let output = super::render_diagnostic_plain(&diag, source, Some("test.lux"));
|
||||||
|
|
||||||
|
// Check all features are present
|
||||||
|
assert!(output.contains("[E0201]"));
|
||||||
|
assert!(output.contains("Type Mismatch"));
|
||||||
|
assert!(output.contains("Expected: Int"));
|
||||||
|
assert!(output.contains("Found: String"));
|
||||||
|
assert!(output.contains("Hint:"));
|
||||||
|
assert!(output.contains("parseInt"));
|
||||||
|
assert!(output.contains("lux-lang.dev/errors/E0201"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
#![allow(dead_code, unused_variables)]
|
#![allow(dead_code, unused_variables)]
|
||||||
|
|
||||||
use crate::ast::*;
|
use crate::ast::*;
|
||||||
use crate::diagnostics::{Diagnostic, Severity};
|
use crate::diagnostics::{Diagnostic, ErrorCode, Severity};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
use rusqlite::Connection;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@@ -426,29 +427,47 @@ impl std::error::Error for RuntimeError {}
|
|||||||
impl RuntimeError {
|
impl RuntimeError {
|
||||||
/// Convert to a rich diagnostic for Elm-style error display
|
/// Convert to a rich diagnostic for Elm-style error display
|
||||||
pub fn to_diagnostic(&self) -> Diagnostic {
|
pub fn to_diagnostic(&self) -> Diagnostic {
|
||||||
let (title, hints) = categorize_runtime_error(&self.message);
|
let (code, title, hints) = categorize_runtime_error(&self.message);
|
||||||
|
|
||||||
Diagnostic {
|
Diagnostic {
|
||||||
severity: Severity::Error,
|
severity: Severity::Error,
|
||||||
|
code,
|
||||||
title,
|
title,
|
||||||
message: self.message.clone(),
|
message: self.message.clone(),
|
||||||
span: self.span.unwrap_or_default(),
|
span: self.span.unwrap_or_default(),
|
||||||
hints,
|
hints,
|
||||||
|
expected_type: None,
|
||||||
|
actual_type: None,
|
||||||
|
secondary_spans: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Categorize runtime errors to provide better titles and hints
|
/// Categorize runtime errors to provide better titles, hints, and error codes
|
||||||
fn categorize_runtime_error(message: &str) -> (String, Vec<String>) {
|
fn categorize_runtime_error(message: &str) -> (Option<ErrorCode>, String, Vec<String>) {
|
||||||
let message_lower = message.to_lowercase();
|
let message_lower = message.to_lowercase();
|
||||||
|
|
||||||
if message_lower.contains("undefined") || message_lower.contains("not found") {
|
if message_lower.contains("undefined variable") {
|
||||||
(
|
(
|
||||||
|
Some(ErrorCode::E0301),
|
||||||
|
"Undefined Variable".to_string(),
|
||||||
|
vec!["Make sure the variable is defined and in scope.".to_string()],
|
||||||
|
)
|
||||||
|
} else if message_lower.contains("undefined function") || message_lower.contains("function not found") {
|
||||||
|
(
|
||||||
|
Some(ErrorCode::E0302),
|
||||||
|
"Undefined Function".to_string(),
|
||||||
|
vec!["Make sure the function is defined and in scope.".to_string()],
|
||||||
|
)
|
||||||
|
} else if message_lower.contains("undefined") || message_lower.contains("not found") {
|
||||||
|
(
|
||||||
|
Some(ErrorCode::E0301),
|
||||||
"Undefined Reference".to_string(),
|
"Undefined Reference".to_string(),
|
||||||
vec!["Make sure the name is defined and in scope.".to_string()],
|
vec!["Make sure the name is defined and in scope.".to_string()],
|
||||||
)
|
)
|
||||||
} else if message_lower.contains("division by zero") || message_lower.contains("divide by zero") {
|
} else if message_lower.contains("division by zero") || message_lower.contains("divide by zero") {
|
||||||
(
|
(
|
||||||
|
None, // Runtime error, not a type error
|
||||||
"Division by Zero".to_string(),
|
"Division by Zero".to_string(),
|
||||||
vec![
|
vec![
|
||||||
"Check that the divisor is not zero before dividing.".to_string(),
|
"Check that the divisor is not zero before dividing.".to_string(),
|
||||||
@@ -457,11 +476,13 @@ fn categorize_runtime_error(message: &str) -> (String, Vec<String>) {
|
|||||||
)
|
)
|
||||||
} else if message_lower.contains("type") && message_lower.contains("mismatch") {
|
} else if message_lower.contains("type") && message_lower.contains("mismatch") {
|
||||||
(
|
(
|
||||||
|
Some(ErrorCode::E0201),
|
||||||
"Type Mismatch".to_string(),
|
"Type Mismatch".to_string(),
|
||||||
vec!["The value has a different type than expected.".to_string()],
|
vec!["The value has a different type than expected.".to_string()],
|
||||||
)
|
)
|
||||||
} else if message_lower.contains("effect") && message_lower.contains("unhandled") {
|
} else if message_lower.contains("effect") && message_lower.contains("unhandled") {
|
||||||
(
|
(
|
||||||
|
Some(ErrorCode::E0401),
|
||||||
"Unhandled Effect".to_string(),
|
"Unhandled Effect".to_string(),
|
||||||
vec![
|
vec![
|
||||||
"This effect must be handled before the program can continue.".to_string(),
|
"This effect must be handled before the program can continue.".to_string(),
|
||||||
@@ -470,16 +491,19 @@ fn categorize_runtime_error(message: &str) -> (String, Vec<String>) {
|
|||||||
)
|
)
|
||||||
} else if message_lower.contains("pattern") && message_lower.contains("match") {
|
} else if message_lower.contains("pattern") && message_lower.contains("match") {
|
||||||
(
|
(
|
||||||
|
Some(ErrorCode::E0501),
|
||||||
"Non-exhaustive Pattern".to_string(),
|
"Non-exhaustive Pattern".to_string(),
|
||||||
vec!["Add more patterns to cover all possible cases.".to_string()],
|
vec!["Add more patterns to cover all possible cases.".to_string()],
|
||||||
)
|
)
|
||||||
} else if message_lower.contains("argument") {
|
} else if message_lower.contains("argument") {
|
||||||
(
|
(
|
||||||
|
Some(ErrorCode::E0209),
|
||||||
"Wrong Arguments".to_string(),
|
"Wrong Arguments".to_string(),
|
||||||
vec!["Check the number and types of arguments provided.".to_string()],
|
vec!["Check the number and types of arguments provided.".to_string()],
|
||||||
)
|
)
|
||||||
} else if message_lower.contains("index") || message_lower.contains("bounds") {
|
} else if message_lower.contains("index") || message_lower.contains("bounds") {
|
||||||
(
|
(
|
||||||
|
None, // Runtime error
|
||||||
"Index Out of Bounds".to_string(),
|
"Index Out of Bounds".to_string(),
|
||||||
vec![
|
vec![
|
||||||
"The index is outside the valid range.".to_string(),
|
"The index is outside the valid range.".to_string(),
|
||||||
@@ -487,7 +511,7 @@ fn categorize_runtime_error(message: &str) -> (String, Vec<String>) {
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
("Runtime Error".to_string(), vec![])
|
(None, "Runtime Error".to_string(), vec![])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,6 +611,10 @@ pub struct Interpreter {
|
|||||||
current_http_request: Arc<Mutex<Option<tiny_http::Request>>>,
|
current_http_request: Arc<Mutex<Option<tiny_http::Request>>>,
|
||||||
/// Test results for the Test effect
|
/// Test results for the Test effect
|
||||||
test_results: RefCell<TestResults>,
|
test_results: RefCell<TestResults>,
|
||||||
|
/// SQL database connections (connection ID -> Connection)
|
||||||
|
sql_connections: RefCell<HashMap<i64, Connection>>,
|
||||||
|
/// Next SQL connection ID
|
||||||
|
next_sql_conn_id: RefCell<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Results from running tests
|
/// Results from running tests
|
||||||
@@ -627,6 +655,8 @@ impl Interpreter {
|
|||||||
http_server: Arc::new(Mutex::new(None)),
|
http_server: Arc::new(Mutex::new(None)),
|
||||||
current_http_request: Arc::new(Mutex::new(None)),
|
current_http_request: Arc::new(Mutex::new(None)),
|
||||||
test_results: RefCell::new(TestResults::default()),
|
test_results: RefCell::new(TestResults::default()),
|
||||||
|
sql_connections: RefCell::new(HashMap::new()),
|
||||||
|
next_sql_conn_id: RefCell::new(1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -708,6 +738,32 @@ impl Interpreter {
|
|||||||
.insert(from_version, migration);
|
.insert(from_version, migration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register auto-generated migrations from the typechecker
|
||||||
|
/// These are migrations that were automatically generated for auto-migratable schema changes
|
||||||
|
pub fn register_auto_migrations(
|
||||||
|
&mut self,
|
||||||
|
auto_migrations: &std::collections::HashMap<String, std::collections::HashMap<u32, Expr>>,
|
||||||
|
) {
|
||||||
|
for (type_name, version_migrations) in auto_migrations {
|
||||||
|
for (from_version, body) in version_migrations {
|
||||||
|
// Only register if no migration already exists
|
||||||
|
let already_has = self
|
||||||
|
.migrations
|
||||||
|
.get(type_name)
|
||||||
|
.map(|m| m.contains_key(from_version))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if !already_has {
|
||||||
|
let stored = StoredMigration {
|
||||||
|
body: body.clone(),
|
||||||
|
env: self.global_env.clone(),
|
||||||
|
};
|
||||||
|
self.register_migration(type_name, *from_version, stored);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a versioned value
|
/// Create a versioned value
|
||||||
pub fn create_versioned(&self, type_name: &str, version: u32, value: Value) -> Value {
|
pub fn create_versioned(&self, type_name: &str, version: u32, value: Value) -> Value {
|
||||||
Value::Versioned {
|
Value::Versioned {
|
||||||
@@ -3712,6 +3768,292 @@ impl Interpreter {
|
|||||||
Ok(Value::Unit)
|
Ok(Value::Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Sql Effect =====
|
||||||
|
("Sql", "open") => {
|
||||||
|
let path = match request.args.first() {
|
||||||
|
Some(Value::String(s)) => s.clone(),
|
||||||
|
_ => return Err(RuntimeError {
|
||||||
|
message: "Sql.open requires a path string".to_string(),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
match Connection::open(&path) {
|
||||||
|
Ok(conn) => {
|
||||||
|
let id = *self.next_sql_conn_id.borrow();
|
||||||
|
*self.next_sql_conn_id.borrow_mut() += 1;
|
||||||
|
self.sql_connections.borrow_mut().insert(id, conn);
|
||||||
|
Ok(Value::Int(id))
|
||||||
|
}
|
||||||
|
Err(e) => Err(RuntimeError {
|
||||||
|
message: format!("Sql.open failed: {}", e),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
("Sql", "openMemory") => {
|
||||||
|
match Connection::open_in_memory() {
|
||||||
|
Ok(conn) => {
|
||||||
|
let id = *self.next_sql_conn_id.borrow();
|
||||||
|
*self.next_sql_conn_id.borrow_mut() += 1;
|
||||||
|
self.sql_connections.borrow_mut().insert(id, conn);
|
||||||
|
Ok(Value::Int(id))
|
||||||
|
}
|
||||||
|
Err(e) => Err(RuntimeError {
|
||||||
|
message: format!("Sql.openMemory failed: {}", e),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
("Sql", "close") => {
|
||||||
|
let conn_id = match request.args.first() {
|
||||||
|
Some(Value::Int(id)) => *id,
|
||||||
|
_ => return Err(RuntimeError {
|
||||||
|
message: "Sql.close requires a connection ID".to_string(),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.sql_connections.borrow_mut().remove(&conn_id).is_some() {
|
||||||
|
Ok(Value::Unit)
|
||||||
|
} else {
|
||||||
|
Err(RuntimeError {
|
||||||
|
message: format!("Sql.close: invalid connection ID {}", conn_id),
|
||||||
|
span: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
("Sql", "execute") => {
|
||||||
|
let (conn_id, sql) = match (request.args.get(0), request.args.get(1)) {
|
||||||
|
(Some(Value::Int(id)), Some(Value::String(s))) => (*id, s.clone()),
|
||||||
|
_ => return Err(RuntimeError {
|
||||||
|
message: "Sql.execute requires connection ID and SQL string".to_string(),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let conns = self.sql_connections.borrow();
|
||||||
|
let conn = match conns.get(&conn_id) {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return Err(RuntimeError {
|
||||||
|
message: format!("Sql.execute: invalid connection ID {}", conn_id),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
match conn.execute(&sql, []) {
|
||||||
|
Ok(rows_affected) => Ok(Value::Int(rows_affected as i64)),
|
||||||
|
Err(e) => Err(RuntimeError {
|
||||||
|
message: format!("Sql.execute failed: {}", e),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
("Sql", "query") => {
|
||||||
|
let (conn_id, sql) = match (request.args.get(0), request.args.get(1)) {
|
||||||
|
(Some(Value::Int(id)), Some(Value::String(s))) => (*id, s.clone()),
|
||||||
|
_ => return Err(RuntimeError {
|
||||||
|
message: "Sql.query requires connection ID and SQL string".to_string(),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let conns = self.sql_connections.borrow();
|
||||||
|
let conn = match conns.get(&conn_id) {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return Err(RuntimeError {
|
||||||
|
message: format!("Sql.query: invalid connection ID {}", conn_id),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut stmt = match conn.prepare(&sql) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => return Err(RuntimeError {
|
||||||
|
message: format!("Sql.query prepare failed: {}", e),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let column_names: Vec<String> = stmt.column_names().iter().map(|s| s.to_string()).collect();
|
||||||
|
let column_count = column_names.len();
|
||||||
|
|
||||||
|
let rows: Result<Vec<Value>, _> = stmt.query_map([], |row| {
|
||||||
|
let mut record = HashMap::new();
|
||||||
|
for (i, col_name) in column_names.iter().enumerate() {
|
||||||
|
let value = match row.get_ref(i) {
|
||||||
|
Ok(rusqlite::types::ValueRef::Null) => Value::Constructor {
|
||||||
|
name: "None".to_string(),
|
||||||
|
fields: vec![],
|
||||||
|
},
|
||||||
|
Ok(rusqlite::types::ValueRef::Integer(n)) => Value::Int(n),
|
||||||
|
Ok(rusqlite::types::ValueRef::Real(f)) => Value::Float(f),
|
||||||
|
Ok(rusqlite::types::ValueRef::Text(s)) => {
|
||||||
|
Value::String(String::from_utf8_lossy(s).to_string())
|
||||||
|
}
|
||||||
|
Ok(rusqlite::types::ValueRef::Blob(b)) => {
|
||||||
|
Value::String(format!("<blob {} bytes>", b.len()))
|
||||||
|
}
|
||||||
|
Err(_) => Value::String("<error>".to_string()),
|
||||||
|
};
|
||||||
|
record.insert(col_name.clone(), value);
|
||||||
|
}
|
||||||
|
Ok(Value::Record(record))
|
||||||
|
}).and_then(|rows| rows.collect());
|
||||||
|
|
||||||
|
match rows {
|
||||||
|
Ok(r) => Ok(Value::List(r)),
|
||||||
|
Err(e) => Err(RuntimeError {
|
||||||
|
message: format!("Sql.query failed: {}", e),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
("Sql", "queryOne") => {
|
||||||
|
let (conn_id, sql) = match (request.args.get(0), request.args.get(1)) {
|
||||||
|
(Some(Value::Int(id)), Some(Value::String(s))) => (*id, s.clone()),
|
||||||
|
_ => return Err(RuntimeError {
|
||||||
|
message: "Sql.queryOne requires connection ID and SQL string".to_string(),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let conns = self.sql_connections.borrow();
|
||||||
|
let conn = match conns.get(&conn_id) {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return Err(RuntimeError {
|
||||||
|
message: format!("Sql.queryOne: invalid connection ID {}", conn_id),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut stmt = match conn.prepare(&sql) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => return Err(RuntimeError {
|
||||||
|
message: format!("Sql.queryOne prepare failed: {}", e),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let column_names: Vec<String> = stmt.column_names().iter().map(|s| s.to_string()).collect();
|
||||||
|
|
||||||
|
let result = stmt.query_row([], |row| {
|
||||||
|
let mut record = HashMap::new();
|
||||||
|
for (i, col_name) in column_names.iter().enumerate() {
|
||||||
|
let value = match row.get_ref(i) {
|
||||||
|
Ok(rusqlite::types::ValueRef::Null) => Value::Constructor {
|
||||||
|
name: "None".to_string(),
|
||||||
|
fields: vec![],
|
||||||
|
},
|
||||||
|
Ok(rusqlite::types::ValueRef::Integer(n)) => Value::Int(n),
|
||||||
|
Ok(rusqlite::types::ValueRef::Real(f)) => Value::Float(f),
|
||||||
|
Ok(rusqlite::types::ValueRef::Text(s)) => {
|
||||||
|
Value::String(String::from_utf8_lossy(s).to_string())
|
||||||
|
}
|
||||||
|
Ok(rusqlite::types::ValueRef::Blob(b)) => {
|
||||||
|
Value::String(format!("<blob {} bytes>", b.len()))
|
||||||
|
}
|
||||||
|
Err(_) => Value::String("<error>".to_string()),
|
||||||
|
};
|
||||||
|
record.insert(col_name.clone(), value);
|
||||||
|
}
|
||||||
|
Ok(Value::Record(record))
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => Ok(Value::Constructor {
|
||||||
|
name: "Some".to_string(),
|
||||||
|
fields: vec![row],
|
||||||
|
}),
|
||||||
|
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(Value::Constructor {
|
||||||
|
name: "None".to_string(),
|
||||||
|
fields: vec![],
|
||||||
|
}),
|
||||||
|
Err(e) => Err(RuntimeError {
|
||||||
|
message: format!("Sql.queryOne failed: {}", e),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
("Sql", "beginTx") => {
|
||||||
|
let conn_id = match request.args.first() {
|
||||||
|
Some(Value::Int(id)) => *id,
|
||||||
|
_ => return Err(RuntimeError {
|
||||||
|
message: "Sql.beginTx requires a connection ID".to_string(),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let conns = self.sql_connections.borrow();
|
||||||
|
let conn = match conns.get(&conn_id) {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return Err(RuntimeError {
|
||||||
|
message: format!("Sql.beginTx: invalid connection ID {}", conn_id),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
match conn.execute("BEGIN TRANSACTION", []) {
|
||||||
|
Ok(_) => Ok(Value::Unit),
|
||||||
|
Err(e) => Err(RuntimeError {
|
||||||
|
message: format!("Sql.beginTx failed: {}", e),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
("Sql", "commit") => {
|
||||||
|
let conn_id = match request.args.first() {
|
||||||
|
Some(Value::Int(id)) => *id,
|
||||||
|
_ => return Err(RuntimeError {
|
||||||
|
message: "Sql.commit requires a connection ID".to_string(),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let conns = self.sql_connections.borrow();
|
||||||
|
let conn = match conns.get(&conn_id) {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return Err(RuntimeError {
|
||||||
|
message: format!("Sql.commit: invalid connection ID {}", conn_id),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
match conn.execute("COMMIT", []) {
|
||||||
|
Ok(_) => Ok(Value::Unit),
|
||||||
|
Err(e) => Err(RuntimeError {
|
||||||
|
message: format!("Sql.commit failed: {}", e),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
("Sql", "rollback") => {
|
||||||
|
let conn_id = match request.args.first() {
|
||||||
|
Some(Value::Int(id)) => *id,
|
||||||
|
_ => return Err(RuntimeError {
|
||||||
|
message: "Sql.rollback requires a connection ID".to_string(),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let conns = self.sql_connections.borrow();
|
||||||
|
let conn = match conns.get(&conn_id) {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return Err(RuntimeError {
|
||||||
|
message: format!("Sql.rollback: invalid connection ID {}", conn_id),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
match conn.execute("ROLLBACK", []) {
|
||||||
|
Ok(_) => Ok(Value::Unit),
|
||||||
|
Err(e) => Err(RuntimeError {
|
||||||
|
message: format!("Sql.rollback failed: {}", e),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ => Err(RuntimeError {
|
_ => Err(RuntimeError {
|
||||||
message: format!(
|
message: format!(
|
||||||
"Unhandled effect operation: {}.{}",
|
"Unhandled effect operation: {}.{}",
|
||||||
|
|||||||
219
src/main.rs
219
src/main.rs
@@ -13,6 +13,7 @@ mod lsp;
|
|||||||
mod modules;
|
mod modules;
|
||||||
mod package;
|
mod package;
|
||||||
mod parser;
|
mod parser;
|
||||||
|
mod registry;
|
||||||
mod schema;
|
mod schema;
|
||||||
mod typechecker;
|
mod typechecker;
|
||||||
mod types;
|
mod types;
|
||||||
@@ -126,6 +127,25 @@ fn main() {
|
|||||||
// Package manager
|
// Package manager
|
||||||
handle_pkg_command(&args[2..]);
|
handle_pkg_command(&args[2..]);
|
||||||
}
|
}
|
||||||
|
"registry" => {
|
||||||
|
// Run package registry server
|
||||||
|
let storage = args.iter()
|
||||||
|
.position(|a| a == "--storage" || a == "-s")
|
||||||
|
.and_then(|i| args.get(i + 1))
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.unwrap_or("./lux-registry");
|
||||||
|
|
||||||
|
let bind = args.iter()
|
||||||
|
.position(|a| a == "--bind" || a == "-b")
|
||||||
|
.and_then(|i| args.get(i + 1))
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.unwrap_or("127.0.0.1:8080");
|
||||||
|
|
||||||
|
if let Err(e) = registry::run_registry_server(storage, bind) {
|
||||||
|
eprintln!("Registry server error: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
"compile" => {
|
"compile" => {
|
||||||
// Compile to native binary or JavaScript
|
// Compile to native binary or JavaScript
|
||||||
if args.len() < 3 {
|
if args.len() < 3 {
|
||||||
@@ -182,6 +202,9 @@ fn print_help() {
|
|||||||
println!(" lux debug <file.lux> Start interactive debugger");
|
println!(" lux debug <file.lux> Start interactive debugger");
|
||||||
println!(" lux init [name] Initialize a new project");
|
println!(" lux init [name] Initialize a new project");
|
||||||
println!(" lux pkg <command> Package manager (install, add, remove, list, update)");
|
println!(" lux pkg <command> Package manager (install, add, remove, list, update)");
|
||||||
|
println!(" lux registry Start package registry server");
|
||||||
|
println!(" -s, --storage <dir> Storage directory (default: ./lux-registry)");
|
||||||
|
println!(" -b, --bind <addr> Bind address (default: 127.0.0.1:8080)");
|
||||||
println!(" lux --lsp Start LSP server (for IDE integration)");
|
println!(" lux --lsp Start LSP server (for IDE integration)");
|
||||||
println!(" lux --help Show this help");
|
println!(" lux --help Show this help");
|
||||||
println!(" lux --version Show version");
|
println!(" lux --version Show version");
|
||||||
@@ -710,6 +733,9 @@ fn run_tests(args: &[String]) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get auto-generated migrations from typechecker
|
||||||
|
let auto_migrations = checker.get_auto_migrations().clone();
|
||||||
|
|
||||||
// Find test functions (functions starting with test_)
|
// Find test functions (functions starting with test_)
|
||||||
let test_funcs: Vec<_> = program.declarations.iter().filter_map(|d| {
|
let test_funcs: Vec<_> = program.declarations.iter().filter_map(|d| {
|
||||||
if let ast::Declaration::Function(f) = d {
|
if let ast::Declaration::Function(f) = d {
|
||||||
@@ -723,6 +749,7 @@ fn run_tests(args: &[String]) {
|
|||||||
if test_funcs.is_empty() {
|
if test_funcs.is_empty() {
|
||||||
// No test functions, run the whole file
|
// No test functions, run the whole file
|
||||||
let mut interp = Interpreter::new();
|
let mut interp = Interpreter::new();
|
||||||
|
interp.register_auto_migrations(&auto_migrations);
|
||||||
interp.reset_test_results();
|
interp.reset_test_results();
|
||||||
|
|
||||||
match interp.run(&program) {
|
match interp.run(&program) {
|
||||||
@@ -755,6 +782,7 @@ fn run_tests(args: &[String]) {
|
|||||||
|
|
||||||
for test_name in &test_funcs {
|
for test_name in &test_funcs {
|
||||||
let mut interp = Interpreter::new();
|
let mut interp = Interpreter::new();
|
||||||
|
interp.register_auto_migrations(&auto_migrations);
|
||||||
interp.reset_test_results();
|
interp.reset_test_results();
|
||||||
|
|
||||||
// First run the file to define all functions
|
// First run the file to define all functions
|
||||||
@@ -1046,6 +1074,16 @@ fn handle_pkg_command(args: &[String]) {
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"search" => {
|
||||||
|
if args.len() < 2 {
|
||||||
|
eprintln!("Usage: lux pkg search <query>");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
search_registry(&args[1]);
|
||||||
|
}
|
||||||
|
"publish" => {
|
||||||
|
publish_package(&project_root);
|
||||||
|
}
|
||||||
"help" | "--help" | "-h" => {
|
"help" | "--help" | "-h" => {
|
||||||
print_pkg_help();
|
print_pkg_help();
|
||||||
}
|
}
|
||||||
@@ -1057,6 +1095,163 @@ fn handle_pkg_command(args: &[String]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn search_registry(query: &str) {
|
||||||
|
let registry_url = std::env::var("LUX_REGISTRY_URL")
|
||||||
|
.unwrap_or_else(|_| "https://pkgs.lux-lang.org".to_string());
|
||||||
|
|
||||||
|
println!("Searching for '{}' in {}...", query, registry_url);
|
||||||
|
|
||||||
|
// Make HTTP request to registry
|
||||||
|
let url = format!("{}/api/v1/search?q={}", registry_url, query);
|
||||||
|
|
||||||
|
// Use a simple HTTP client (could use reqwest in production)
|
||||||
|
match simple_http_get(&url) {
|
||||||
|
Ok(response) => {
|
||||||
|
// Parse JSON response and display results
|
||||||
|
if response.contains("\"packages\":[]") {
|
||||||
|
println!("No packages found matching '{}'", query);
|
||||||
|
} else {
|
||||||
|
println!("\nFound packages:");
|
||||||
|
// Simple parsing of package names from JSON
|
||||||
|
for line in response.lines() {
|
||||||
|
if line.contains("\"name\":") {
|
||||||
|
if let Some(start) = line.find("\"name\":") {
|
||||||
|
let rest = &line[start + 8..];
|
||||||
|
if let Some(end) = rest.find('"') {
|
||||||
|
let name = &rest[..end];
|
||||||
|
println!(" {}", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to connect to registry: {}", e);
|
||||||
|
eprintln!("Make sure the registry server is running or check LUX_REGISTRY_URL");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn publish_package(project_root: &std::path::Path) {
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
let registry_url = std::env::var("LUX_REGISTRY_URL")
|
||||||
|
.unwrap_or_else(|_| "https://pkgs.lux-lang.org".to_string());
|
||||||
|
|
||||||
|
// Load manifest
|
||||||
|
let manifest_path = project_root.join("lux.toml");
|
||||||
|
if !manifest_path.exists() {
|
||||||
|
eprintln!("No lux.toml found");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let manifest_content = fs::read_to_string(&manifest_path).unwrap();
|
||||||
|
|
||||||
|
// Extract project info
|
||||||
|
let mut name = String::new();
|
||||||
|
let mut version = String::new();
|
||||||
|
|
||||||
|
for line in manifest_content.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.starts_with("name") {
|
||||||
|
if let Some(eq) = line.find('=') {
|
||||||
|
name = line[eq+1..].trim().trim_matches('"').to_string();
|
||||||
|
}
|
||||||
|
} else if line.starts_with("version") {
|
||||||
|
if let Some(eq) = line.find('=') {
|
||||||
|
version = line[eq+1..].trim().trim_matches('"').to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if name.is_empty() || version.is_empty() {
|
||||||
|
eprintln!("Invalid lux.toml: missing name or version");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Publishing {} v{} to {}...", name, version, registry_url);
|
||||||
|
|
||||||
|
// Create tarball of the package
|
||||||
|
let tarball_name = format!("{}-{}.tar.gz", name, version);
|
||||||
|
let tarball_path = project_root.join(&tarball_name);
|
||||||
|
|
||||||
|
// Use tar command to create tarball
|
||||||
|
let status = std::process::Command::new("tar")
|
||||||
|
.arg("-czf")
|
||||||
|
.arg(&tarball_path)
|
||||||
|
.arg("--exclude=.lux_packages")
|
||||||
|
.arg("--exclude=.git")
|
||||||
|
.arg("--exclude=target")
|
||||||
|
.arg("-C")
|
||||||
|
.arg(project_root)
|
||||||
|
.arg(".")
|
||||||
|
.status();
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Ok(s) if s.success() => {
|
||||||
|
println!("Created package tarball: {}", tarball_name);
|
||||||
|
println!();
|
||||||
|
println!("To publish, upload the tarball to the registry:");
|
||||||
|
println!(" curl -X POST {}/api/v1/publish -F 'package=@{}'",
|
||||||
|
registry_url, tarball_name);
|
||||||
|
|
||||||
|
// Clean up tarball
|
||||||
|
// fs::remove_file(&tarball_path).ok();
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
eprintln!("Failed to create package tarball");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to run tar: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn simple_http_get(url: &str) -> Result<String, String> {
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::net::TcpStream;
|
||||||
|
|
||||||
|
// Parse URL
|
||||||
|
let url = url.strip_prefix("http://").or_else(|| url.strip_prefix("https://")).unwrap_or(url);
|
||||||
|
let (host_port, path) = if let Some(slash) = url.find('/') {
|
||||||
|
(&url[..slash], &url[slash..])
|
||||||
|
} else {
|
||||||
|
(url, "/")
|
||||||
|
};
|
||||||
|
|
||||||
|
let (host, port) = if let Some(colon) = host_port.find(':') {
|
||||||
|
(&host_port[..colon], host_port[colon+1..].parse::<u16>().unwrap_or(80))
|
||||||
|
} else {
|
||||||
|
(host_port, 80)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut stream = TcpStream::connect((host, port))
|
||||||
|
.map_err(|e| format!("Connection failed: {}", e))?;
|
||||||
|
|
||||||
|
let request = format!(
|
||||||
|
"GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n",
|
||||||
|
path, host
|
||||||
|
);
|
||||||
|
|
||||||
|
stream.write_all(request.as_bytes())
|
||||||
|
.map_err(|e| format!("Write failed: {}", e))?;
|
||||||
|
|
||||||
|
let mut response = String::new();
|
||||||
|
stream.read_to_string(&mut response)
|
||||||
|
.map_err(|e| format!("Read failed: {}", e))?;
|
||||||
|
|
||||||
|
// Extract body (after \r\n\r\n)
|
||||||
|
if let Some(body_start) = response.find("\r\n\r\n") {
|
||||||
|
Ok(response[body_start + 4..].to_string())
|
||||||
|
} else {
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn print_pkg_help() {
|
fn print_pkg_help() {
|
||||||
println!("Lux Package Manager");
|
println!("Lux Package Manager");
|
||||||
println!();
|
println!();
|
||||||
@@ -1075,6 +1270,11 @@ fn print_pkg_help() {
|
|||||||
println!(" list, ls List dependencies and their status");
|
println!(" list, ls List dependencies and their status");
|
||||||
println!(" update Update all dependencies");
|
println!(" update Update all dependencies");
|
||||||
println!(" clean Remove installed packages");
|
println!(" clean Remove installed packages");
|
||||||
|
println!(" search <query> Search for packages in the registry");
|
||||||
|
println!(" publish Publish package to the registry");
|
||||||
|
println!();
|
||||||
|
println!("Registry Configuration:");
|
||||||
|
println!(" Set LUX_REGISTRY_URL to use a custom registry (default: https://pkgs.lux-lang.org)");
|
||||||
println!();
|
println!();
|
||||||
println!("Examples:");
|
println!("Examples:");
|
||||||
println!(" lux pkg init");
|
println!(" lux pkg init");
|
||||||
@@ -1084,6 +1284,8 @@ fn print_pkg_help() {
|
|||||||
println!(" lux pkg add mylib --git https://github.com/user/mylib");
|
println!(" lux pkg add mylib --git https://github.com/user/mylib");
|
||||||
println!(" lux pkg add local-lib --path ../lib");
|
println!(" lux pkg add local-lib --path ../lib");
|
||||||
println!(" lux pkg remove http");
|
println!(" lux pkg remove http");
|
||||||
|
println!(" lux pkg search json");
|
||||||
|
println!(" lux pkg publish");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_pkg_here(name: Option<&str>) {
|
fn init_pkg_here(name: Option<&str>) {
|
||||||
@@ -3073,13 +3275,12 @@ c")"#;
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_diagnostic_render_with_real_code() {
|
fn test_diagnostic_render_with_real_code() {
|
||||||
let source = "fn add(a: Int, b: Int): Int = a + b\nlet result = add(1, \"two\")";
|
let source = "fn add(a: Int, b: Int): Int = a + b\nlet result = add(1, \"two\")";
|
||||||
let diag = Diagnostic {
|
let diag = Diagnostic::error(
|
||||||
severity: Severity::Error,
|
"Type Mismatch",
|
||||||
title: "Type Mismatch".to_string(),
|
"Expected Int but got String",
|
||||||
message: "Expected Int but got String".to_string(),
|
Span { start: 56, end: 61 },
|
||||||
span: Span { start: 56, end: 61 },
|
)
|
||||||
hints: vec!["The second argument should be an Int.".to_string()],
|
.with_hint("The second argument should be an Int.");
|
||||||
};
|
|
||||||
|
|
||||||
let output = render_diagnostic_plain(&diag, source, Some("example.lux"));
|
let output = render_diagnostic_plain(&diag, source, Some("example.lux"));
|
||||||
|
|
||||||
@@ -3098,8 +3299,10 @@ c")"#;
|
|||||||
};
|
};
|
||||||
let diag = error.to_diagnostic();
|
let diag = error.to_diagnostic();
|
||||||
|
|
||||||
assert_eq!(diag.title, "Unknown Name");
|
assert_eq!(diag.title, "Undefined Variable");
|
||||||
assert!(diag.hints.iter().any(|h| h.contains("spelling")));
|
assert!(diag.hints.iter().any(|h| h.contains("spelling")));
|
||||||
|
// Check error code is set
|
||||||
|
assert!(diag.code.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use crate::ast::*;
|
use crate::ast::*;
|
||||||
use crate::diagnostics::{Diagnostic, Severity};
|
use crate::diagnostics::{Diagnostic, ErrorCode, Severity};
|
||||||
use crate::lexer::{LexError, Lexer, StringPart, Token, TokenKind};
|
use crate::lexer::{LexError, Lexer, StringPart, Token, TokenKind};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
@@ -27,57 +27,79 @@ impl fmt::Display for ParseError {
|
|||||||
impl ParseError {
|
impl ParseError {
|
||||||
/// Convert to a rich diagnostic for Elm-style error display
|
/// Convert to a rich diagnostic for Elm-style error display
|
||||||
pub fn to_diagnostic(&self) -> Diagnostic {
|
pub fn to_diagnostic(&self) -> Diagnostic {
|
||||||
let (title, hints) = categorize_parse_error(&self.message);
|
let (code, title, hints) = categorize_parse_error(&self.message);
|
||||||
|
|
||||||
Diagnostic {
|
Diagnostic {
|
||||||
severity: Severity::Error,
|
severity: Severity::Error,
|
||||||
|
code,
|
||||||
title,
|
title,
|
||||||
message: self.message.clone(),
|
message: self.message.clone(),
|
||||||
span: self.span,
|
span: self.span,
|
||||||
hints,
|
hints,
|
||||||
|
expected_type: None,
|
||||||
|
actual_type: None,
|
||||||
|
secondary_spans: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Categorize parse errors to provide better titles and hints
|
/// Categorize parse errors to provide better titles, hints, and error codes
|
||||||
fn categorize_parse_error(message: &str) -> (String, Vec<String>) {
|
fn categorize_parse_error(message: &str) -> (Option<ErrorCode>, String, Vec<String>) {
|
||||||
let message_lower = message.to_lowercase();
|
let message_lower = message.to_lowercase();
|
||||||
|
|
||||||
if message_lower.contains("unexpected") && message_lower.contains("expected") {
|
if message_lower.contains("unexpected") && message_lower.contains("expected") {
|
||||||
(
|
(
|
||||||
|
Some(ErrorCode::E0101),
|
||||||
"Unexpected Token".to_string(),
|
"Unexpected Token".to_string(),
|
||||||
vec!["Check for missing or misplaced punctuation.".to_string()],
|
vec!["Check for missing or misplaced punctuation.".to_string()],
|
||||||
)
|
)
|
||||||
} else if message_lower.contains("expected") && message_lower.contains("expression") {
|
} else if message_lower.contains("expected") && message_lower.contains("expression") {
|
||||||
(
|
(
|
||||||
|
Some(ErrorCode::E0101),
|
||||||
"Missing Expression".to_string(),
|
"Missing Expression".to_string(),
|
||||||
vec!["An expression was expected here.".to_string()],
|
vec!["An expression was expected here.".to_string()],
|
||||||
)
|
)
|
||||||
} else if message_lower.contains("expected") && message_lower.contains(":") {
|
} else if message_lower.contains("expected") && message_lower.contains(":") {
|
||||||
(
|
(
|
||||||
|
Some(ErrorCode::E0104),
|
||||||
"Missing Type Annotation".to_string(),
|
"Missing Type Annotation".to_string(),
|
||||||
vec!["A type annotation is required here.".to_string()],
|
vec!["A type annotation is required here.".to_string()],
|
||||||
)
|
)
|
||||||
} else if message_lower.contains("unclosed") || message_lower.contains("unterminated") {
|
} else if message_lower.contains("unclosed") || message_lower.contains("unterminated") {
|
||||||
(
|
(
|
||||||
|
Some(ErrorCode::E0102),
|
||||||
"Unclosed Delimiter".to_string(),
|
"Unclosed Delimiter".to_string(),
|
||||||
vec![
|
vec![
|
||||||
"Check for matching opening and closing brackets.".to_string(),
|
"Check for matching opening and closing brackets.".to_string(),
|
||||||
"Make sure all strings are properly closed with quotes.".to_string(),
|
"Make sure all strings are properly closed with quotes.".to_string(),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
} else if message_lower.contains("invalid") && message_lower.contains("literal") {
|
||||||
|
(
|
||||||
|
Some(ErrorCode::E0103),
|
||||||
|
"Invalid Literal".to_string(),
|
||||||
|
vec!["Check the format of the literal value.".to_string()],
|
||||||
|
)
|
||||||
|
} else if message_lower.contains("invalid") && message_lower.contains("operator") {
|
||||||
|
(
|
||||||
|
Some(ErrorCode::E0105),
|
||||||
|
"Invalid Operator".to_string(),
|
||||||
|
vec!["Check the operator syntax.".to_string()],
|
||||||
|
)
|
||||||
} else if message_lower.contains("invalid") {
|
} else if message_lower.contains("invalid") {
|
||||||
(
|
(
|
||||||
|
Some(ErrorCode::E0100),
|
||||||
"Invalid Syntax".to_string(),
|
"Invalid Syntax".to_string(),
|
||||||
vec!["Check the syntax of this construct.".to_string()],
|
vec!["Check the syntax of this construct.".to_string()],
|
||||||
)
|
)
|
||||||
} else if message_lower.contains("identifier") {
|
} else if message_lower.contains("identifier") {
|
||||||
(
|
(
|
||||||
|
Some(ErrorCode::E0100),
|
||||||
"Invalid Identifier".to_string(),
|
"Invalid Identifier".to_string(),
|
||||||
vec!["Identifiers must start with a letter and contain only letters, numbers, and underscores.".to_string()],
|
vec!["Identifiers must start with a letter and contain only letters, numbers, and underscores.".to_string()],
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
("Parse Error".to_string(), vec![])
|
(Some(ErrorCode::E0100), "Parse Error".to_string(), vec![])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::ast::{
|
|||||||
ImportDecl, LetDecl, Literal, LiteralKind, MatchArm, Parameter, Pattern, Program, Span,
|
ImportDecl, LetDecl, Literal, LiteralKind, MatchArm, Parameter, Pattern, Program, Span,
|
||||||
Statement, TraitDecl, TypeDecl, TypeExpr, UnaryOp, VariantFields,
|
Statement, TraitDecl, TypeDecl, TypeExpr, UnaryOp, VariantFields,
|
||||||
};
|
};
|
||||||
use crate::diagnostics::{find_similar_names, format_did_you_mean, Diagnostic, Severity};
|
use crate::diagnostics::{find_similar_names, format_did_you_mean, Diagnostic, ErrorCode, Severity};
|
||||||
use crate::exhaustiveness::{check_exhaustiveness, missing_patterns_hint};
|
use crate::exhaustiveness::{check_exhaustiveness, missing_patterns_hint};
|
||||||
use crate::modules::ModuleLoader;
|
use crate::modules::ModuleLoader;
|
||||||
use crate::schema::{SchemaRegistry, Compatibility, BreakingChange, AutoMigration};
|
use crate::schema::{SchemaRegistry, Compatibility, BreakingChange, AutoMigration};
|
||||||
@@ -39,92 +39,239 @@ impl std::fmt::Display for TypeError {
|
|||||||
impl TypeError {
|
impl TypeError {
|
||||||
/// Convert to a rich diagnostic for Elm-style error display
|
/// Convert to a rich diagnostic for Elm-style error display
|
||||||
pub fn to_diagnostic(&self) -> Diagnostic {
|
pub fn to_diagnostic(&self) -> Diagnostic {
|
||||||
// Categorize the error and extract hints
|
// Categorize the error and extract hints, error code, and type info
|
||||||
let (title, hints) = categorize_type_error(&self.message);
|
let (code, title, hints, expected, actual) = categorize_type_error(&self.message);
|
||||||
|
|
||||||
Diagnostic {
|
Diagnostic {
|
||||||
severity: Severity::Error,
|
severity: Severity::Error,
|
||||||
|
code,
|
||||||
title,
|
title,
|
||||||
message: self.message.clone(),
|
message: self.message.clone(),
|
||||||
span: self.span,
|
span: self.span,
|
||||||
hints,
|
hints,
|
||||||
|
expected_type: expected,
|
||||||
|
actual_type: actual,
|
||||||
|
secondary_spans: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Categorize a type error message to provide better titles and hints
|
/// Extract expected and actual types from an error message
|
||||||
fn categorize_type_error(message: &str) -> (String, Vec<String>) {
|
fn extract_types_from_message(message: &str) -> (Option<String>, Option<String>) {
|
||||||
|
// Try to extract "expected X, got Y" patterns
|
||||||
|
let patterns = [
|
||||||
|
("expected ", ", got "),
|
||||||
|
("expected ", " but got "),
|
||||||
|
("expected ", " found "),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (exp_prefix, got_prefix) in patterns {
|
||||||
|
if let Some(exp_start) = message.to_lowercase().find(exp_prefix) {
|
||||||
|
let after_expected = &message[exp_start + exp_prefix.len()..];
|
||||||
|
if let Some(got_pos) = after_expected.to_lowercase().find(got_prefix) {
|
||||||
|
let expected = after_expected[..got_pos].trim().to_string();
|
||||||
|
let after_got = &after_expected[got_pos + got_prefix.len()..];
|
||||||
|
// Take until end of word or punctuation
|
||||||
|
let actual = after_got
|
||||||
|
.split(|c: char| c == ',' || c == '.' || c == ';' || c == ')' || c.is_whitespace())
|
||||||
|
.next()
|
||||||
|
.unwrap_or("")
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
if !expected.is_empty() && !actual.is_empty() {
|
||||||
|
return (Some(expected), Some(actual));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try "cannot unify X with Y" pattern
|
||||||
|
if let Some(unify_pos) = message.to_lowercase().find("cannot unify ") {
|
||||||
|
let after_unify = &message[unify_pos + 13..];
|
||||||
|
if let Some(with_pos) = after_unify.to_lowercase().find(" with ") {
|
||||||
|
let type1 = after_unify[..with_pos].trim().to_string();
|
||||||
|
let type2 = after_unify[with_pos + 6..]
|
||||||
|
.split(|c: char| c == ',' || c == '.' || c == ';' || c == ')' || c.is_whitespace())
|
||||||
|
.next()
|
||||||
|
.unwrap_or("")
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
if !type1.is_empty() && !type2.is_empty() {
|
||||||
|
return (Some(type1), Some(type2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Categorize a type error message to provide better titles, hints, and error codes
|
||||||
|
fn categorize_type_error(message: &str) -> (Option<ErrorCode>, String, Vec<String>, Option<String>, Option<String>) {
|
||||||
let message_lower = message.to_lowercase();
|
let message_lower = message.to_lowercase();
|
||||||
|
let (expected, actual) = extract_types_from_message(message);
|
||||||
|
|
||||||
if message_lower.contains("type mismatch") {
|
if message_lower.contains("type mismatch") {
|
||||||
(
|
(
|
||||||
|
Some(ErrorCode::E0201),
|
||||||
"Type Mismatch".to_string(),
|
"Type Mismatch".to_string(),
|
||||||
vec!["Check that the types on both sides of the expression are compatible.".to_string()],
|
vec!["Check that the types on both sides of the expression are compatible.".to_string()],
|
||||||
|
expected,
|
||||||
|
actual,
|
||||||
)
|
)
|
||||||
} else if message_lower.contains("undefined variable") || message_lower.contains("not found") {
|
} else if message_lower.contains("undefined variable") {
|
||||||
(
|
(
|
||||||
|
Some(ErrorCode::E0301),
|
||||||
|
"Undefined Variable".to_string(),
|
||||||
|
vec![
|
||||||
|
"Check the spelling of the variable name.".to_string(),
|
||||||
|
"Make sure the variable is defined before use.".to_string(),
|
||||||
|
],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
} else if message_lower.contains("undefined function") || message_lower.contains("function") && message_lower.contains("not found") {
|
||||||
|
(
|
||||||
|
Some(ErrorCode::E0302),
|
||||||
|
"Undefined Function".to_string(),
|
||||||
|
vec![
|
||||||
|
"Check the spelling of the function name.".to_string(),
|
||||||
|
"Make sure the function is imported or defined.".to_string(),
|
||||||
|
],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
} else if message_lower.contains("not found") || message_lower.contains("unknown") && message_lower.contains("name") {
|
||||||
|
(
|
||||||
|
Some(ErrorCode::E0301),
|
||||||
"Unknown Name".to_string(),
|
"Unknown Name".to_string(),
|
||||||
vec![
|
vec![
|
||||||
"Check the spelling of the name.".to_string(),
|
"Check the spelling of the name.".to_string(),
|
||||||
"Make sure the variable is defined before use.".to_string(),
|
"Make sure the variable is defined before use.".to_string(),
|
||||||
],
|
],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
} else if message_lower.contains("cannot unify") {
|
} else if message_lower.contains("cannot unify") {
|
||||||
(
|
(
|
||||||
"Type Mismatch".to_string(),
|
Some(ErrorCode::E0202),
|
||||||
|
"Cannot Unify Types".to_string(),
|
||||||
vec!["The types are not compatible. Check your function arguments and return types.".to_string()],
|
vec!["The types are not compatible. Check your function arguments and return types.".to_string()],
|
||||||
|
expected,
|
||||||
|
actual,
|
||||||
)
|
)
|
||||||
} else if message_lower.contains("expected") && message_lower.contains("argument") {
|
} else if message_lower.contains("expected") && message_lower.contains("argument") {
|
||||||
(
|
(
|
||||||
|
Some(ErrorCode::E0209),
|
||||||
"Wrong Number of Arguments".to_string(),
|
"Wrong Number of Arguments".to_string(),
|
||||||
vec!["Check the function signature and provide the correct number of arguments.".to_string()],
|
vec!["Check the function signature and provide the correct number of arguments.".to_string()],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
} else if message_lower.contains("pure") && message_lower.contains("effect") {
|
} else if message_lower.contains("pure") && message_lower.contains("effect") {
|
||||||
(
|
(
|
||||||
|
Some(ErrorCode::E0701),
|
||||||
"Purity Violation".to_string(),
|
"Purity Violation".to_string(),
|
||||||
vec![
|
vec![
|
||||||
"Functions marked 'is pure' cannot perform effects.".to_string(),
|
"Functions marked 'is pure' cannot perform effects.".to_string(),
|
||||||
"Remove the 'is pure' annotation or handle the effects.".to_string(),
|
"Remove the 'is pure' annotation or handle the effects.".to_string(),
|
||||||
],
|
],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
} else if message_lower.contains("effect") && message_lower.contains("unhandled") {
|
} else if message_lower.contains("effect") && message_lower.contains("unhandled") {
|
||||||
(
|
(
|
||||||
|
Some(ErrorCode::E0401),
|
||||||
"Unhandled Effect".to_string(),
|
"Unhandled Effect".to_string(),
|
||||||
vec!["Use a 'handle' expression to provide an implementation for this effect.".to_string()],
|
vec!["Use a 'handle' expression to provide an implementation for this effect.".to_string()],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
} else if message_lower.contains("recursive") {
|
} else if message_lower.contains("recursive") {
|
||||||
(
|
(
|
||||||
|
Some(ErrorCode::E0205),
|
||||||
"Invalid Recursion".to_string(),
|
"Invalid Recursion".to_string(),
|
||||||
vec!["Check that recursive calls have proper base cases.".to_string()],
|
vec!["Check that recursive calls have proper base cases.".to_string()],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
} else if message_lower.contains("unknown effect") {
|
} else if message_lower.contains("unknown effect") {
|
||||||
(
|
(
|
||||||
|
Some(ErrorCode::E0402),
|
||||||
"Unknown Effect".to_string(),
|
"Unknown Effect".to_string(),
|
||||||
vec![
|
vec![
|
||||||
"Make sure the effect is spelled correctly.".to_string(),
|
"Make sure the effect is spelled correctly.".to_string(),
|
||||||
"Built-in effects: Console, File, Process, Http, Random, Time, Sql.".to_string(),
|
"Built-in effects: Console, File, Process, Http, Random, Time, Sql.".to_string(),
|
||||||
],
|
],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
} else if message_lower.contains("record has no field") {
|
} else if message_lower.contains("record has no field") || message_lower.contains("missing field") {
|
||||||
(
|
(
|
||||||
"Missing Field".to_string(),
|
Some(ErrorCode::E0207),
|
||||||
|
"Missing Record Field".to_string(),
|
||||||
vec!["Check the field name spelling or review the record definition.".to_string()],
|
vec!["Check the field name spelling or review the record definition.".to_string()],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
} else if message_lower.contains("undefined field") {
|
||||||
|
(
|
||||||
|
Some(ErrorCode::E0308),
|
||||||
|
"Undefined Field".to_string(),
|
||||||
|
vec!["Check the field name spelling or review the record definition.".to_string()],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
} else if message_lower.contains("cannot access field") {
|
} else if message_lower.contains("cannot access field") {
|
||||||
(
|
(
|
||||||
|
Some(ErrorCode::E0308),
|
||||||
"Invalid Field Access".to_string(),
|
"Invalid Field Access".to_string(),
|
||||||
vec!["Field access is only valid on record types.".to_string()],
|
vec!["Field access is only valid on record types.".to_string()],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
} else if message_lower.contains("effect") && (message_lower.contains("not available") || message_lower.contains("missing")) {
|
} else if message_lower.contains("effect") && (message_lower.contains("not available") || message_lower.contains("missing")) {
|
||||||
(
|
(
|
||||||
"Missing Effect".to_string(),
|
Some(ErrorCode::E0403),
|
||||||
|
"Missing Effect Handler".to_string(),
|
||||||
vec![
|
vec![
|
||||||
"Add the effect to your function's effect list.".to_string(),
|
"Add the effect to your function's effect list.".to_string(),
|
||||||
"Example: fn myFn(): Int with {Console, Sql} = ...".to_string(),
|
"Example: fn myFn(): Int with {Console, Sql} = ...".to_string(),
|
||||||
],
|
],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
} else if message_lower.contains("non-exhaustive") || message_lower.contains("exhaustive") {
|
||||||
|
(
|
||||||
|
Some(ErrorCode::E0501),
|
||||||
|
"Non-Exhaustive Patterns".to_string(),
|
||||||
|
vec!["Add patterns for the missing cases or use a wildcard pattern '_'.".to_string()],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
} else if message_lower.contains("duplicate") {
|
||||||
|
(
|
||||||
|
Some(ErrorCode::E0305),
|
||||||
|
"Duplicate Definition".to_string(),
|
||||||
|
vec!["Each name can only be defined once in the same scope.".to_string()],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
} else if message_lower.contains("return type") {
|
||||||
|
(
|
||||||
|
Some(ErrorCode::E0211),
|
||||||
|
"Return Type Mismatch".to_string(),
|
||||||
|
vec!["The function body's type doesn't match the declared return type.".to_string()],
|
||||||
|
expected,
|
||||||
|
actual,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
("Type Error".to_string(), vec![])
|
(
|
||||||
|
Some(ErrorCode::E0200),
|
||||||
|
"Type Error".to_string(),
|
||||||
|
vec![],
|
||||||
|
expected,
|
||||||
|
actual,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user