2025-11-12 20:45:35 +01:00
|
|
|
use crate::{
|
|
|
|
|
common::base_value::BaseValue,
|
|
|
|
|
frontend::source_registry::{SourceRegistry, SourceSlice},
|
|
|
|
|
};
|
|
|
|
|
use std::{error::Error, fmt};
|
2025-10-03 19:07:12 +02:00
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub enum LoxError {
|
|
|
|
|
LexicalError {
|
|
|
|
|
source_slice: SourceSlice,
|
|
|
|
|
message: String,
|
|
|
|
|
},
|
|
|
|
|
RuntimeError {
|
|
|
|
|
source_slice: SourceSlice,
|
|
|
|
|
message: String,
|
|
|
|
|
},
|
|
|
|
|
ParseError {
|
|
|
|
|
source_slice: SourceSlice,
|
|
|
|
|
message: String,
|
|
|
|
|
},
|
|
|
|
|
IoError {
|
|
|
|
|
message: String,
|
|
|
|
|
},
|
|
|
|
|
TypeMismatch {
|
|
|
|
|
source_slice: SourceSlice,
|
|
|
|
|
expected: String,
|
|
|
|
|
found: String,
|
|
|
|
|
},
|
2026-02-11 16:35:05 +01:00
|
|
|
Return {
|
|
|
|
|
source_slice: SourceSlice,
|
|
|
|
|
value: BaseValue,
|
|
|
|
|
return_label: String,
|
|
|
|
|
},
|
2025-10-03 19:07:12 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-04 19:02:33 +02:00
|
|
|
/// Configuration for error display formatting
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub struct ErrorDisplayConfig {
|
|
|
|
|
/// Show colored output (for terminals that support it)
|
|
|
|
|
pub colored: bool,
|
|
|
|
|
/// Number of context lines to show before and after the error
|
|
|
|
|
pub context_lines: usize,
|
|
|
|
|
/// Show line numbers
|
|
|
|
|
pub show_line_numbers: bool,
|
|
|
|
|
/// Use Unicode characters for arrows and decorations
|
|
|
|
|
pub use_unicode: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for ErrorDisplayConfig {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
colored: true,
|
|
|
|
|
context_lines: 2,
|
|
|
|
|
show_line_numbers: true,
|
|
|
|
|
use_unicode: true,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ErrorDisplayConfig {
|
|
|
|
|
pub fn simple() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
colored: false,
|
|
|
|
|
context_lines: 1,
|
|
|
|
|
show_line_numbers: false,
|
|
|
|
|
use_unicode: false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn verbose() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
colored: true,
|
|
|
|
|
context_lines: 3,
|
|
|
|
|
show_line_numbers: true,
|
|
|
|
|
use_unicode: true,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-03 19:07:12 +02:00
|
|
|
impl LoxError {
|
|
|
|
|
pub fn get_message(&self) -> String {
|
|
|
|
|
match self {
|
|
|
|
|
LoxError::LexicalError { message, .. } => message.clone(),
|
|
|
|
|
LoxError::RuntimeError { message, .. } => message.clone(),
|
|
|
|
|
LoxError::ParseError { message, .. } => message.clone(),
|
|
|
|
|
LoxError::IoError { message } => message.clone(),
|
|
|
|
|
LoxError::TypeMismatch {
|
|
|
|
|
expected, found, ..
|
|
|
|
|
} => {
|
|
|
|
|
format!("expected {}, found {}", expected, found)
|
|
|
|
|
}
|
2026-02-11 16:35:05 +01:00
|
|
|
LoxError::Return {
|
|
|
|
|
value,
|
|
|
|
|
return_label,
|
|
|
|
|
..
|
|
|
|
|
} => {
|
|
|
|
|
format!("return value: {} and label: {}", value, return_label)
|
|
|
|
|
}
|
2025-10-03 19:07:12 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn get_source_slice(&self) -> Option<SourceSlice> {
|
|
|
|
|
match self {
|
|
|
|
|
LoxError::LexicalError { source_slice, .. } => Some(source_slice.clone()),
|
|
|
|
|
LoxError::RuntimeError { source_slice, .. } => Some(source_slice.clone()),
|
|
|
|
|
LoxError::ParseError { source_slice, .. } => Some(source_slice.clone()),
|
|
|
|
|
LoxError::IoError { .. } => None,
|
|
|
|
|
LoxError::TypeMismatch { source_slice, .. } => Some(source_slice.clone()),
|
2026-02-11 16:35:05 +01:00
|
|
|
LoxError::Return {
|
|
|
|
|
value,
|
|
|
|
|
return_label,
|
|
|
|
|
..
|
|
|
|
|
} => None,
|
2025-10-03 19:07:12 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-04 19:02:33 +02:00
|
|
|
pub fn error_kind(&self) -> &'static str {
|
|
|
|
|
match self {
|
|
|
|
|
LoxError::LexicalError { .. } => "lexical error",
|
|
|
|
|
LoxError::RuntimeError { .. } => "runtime error",
|
|
|
|
|
LoxError::ParseError { .. } => "parse error",
|
|
|
|
|
LoxError::IoError { .. } => "io error",
|
|
|
|
|
LoxError::TypeMismatch { .. } => "type error",
|
2026-02-11 16:35:05 +01:00
|
|
|
LoxError::Return { .. } => "return error",
|
2025-10-04 19:02:33 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Display error with source code context using default configuration
|
|
|
|
|
pub fn display_with_source(&self, source_registry: &SourceRegistry) -> String {
|
|
|
|
|
self.display_with_source_and_config(source_registry, &ErrorDisplayConfig::default())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Display error with source code context using custom configuration
|
|
|
|
|
pub fn display_with_source_and_config(
|
|
|
|
|
&self,
|
|
|
|
|
source_registry: &SourceRegistry,
|
|
|
|
|
config: &ErrorDisplayConfig,
|
|
|
|
|
) -> String {
|
|
|
|
|
let mut output = String::new();
|
|
|
|
|
|
|
|
|
|
// Error header
|
|
|
|
|
let header = if config.colored {
|
|
|
|
|
format!(
|
|
|
|
|
"\x1b[1;31merror\x1b[0m: [{}] {}",
|
|
|
|
|
self.error_kind(),
|
|
|
|
|
self.get_message()
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
format!("error: [{}] {}", self.error_kind(), self.get_message())
|
|
|
|
|
};
|
|
|
|
|
output.push_str(&header);
|
|
|
|
|
output.push('\n');
|
|
|
|
|
|
|
|
|
|
// If we have source location, show it
|
|
|
|
|
if let Some(source_slice) = self.get_source_slice() {
|
|
|
|
|
let source_info = source_registry.get_by_id(source_slice.source_id);
|
|
|
|
|
|
|
|
|
|
// File location line
|
|
|
|
|
let location = if config.colored {
|
|
|
|
|
format!(
|
|
|
|
|
" \x1b[34m-->\x1b[0m {}:{}:{}",
|
|
|
|
|
source_info.file_name,
|
|
|
|
|
source_slice.start_position.line + 1,
|
|
|
|
|
source_slice.start_position.column + 1
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
format!(
|
|
|
|
|
" --> {}:{}:{}",
|
|
|
|
|
source_info.file_name,
|
|
|
|
|
source_slice.start_position.line + 1,
|
|
|
|
|
source_slice.start_position.column + 1
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
output.push_str(&location);
|
|
|
|
|
output.push('\n');
|
|
|
|
|
|
|
|
|
|
// Source code context
|
|
|
|
|
if let Some(source_context) =
|
|
|
|
|
self.get_source_context(source_registry, &source_slice, config)
|
|
|
|
|
{
|
|
|
|
|
output.push_str(&source_context);
|
2025-10-03 19:07:12 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-10-04 19:02:33 +02:00
|
|
|
|
|
|
|
|
output
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_source_context(
|
|
|
|
|
&self,
|
|
|
|
|
source_registry: &SourceRegistry,
|
|
|
|
|
source_slice: &SourceSlice,
|
|
|
|
|
config: &ErrorDisplayConfig,
|
|
|
|
|
) -> Option<String> {
|
|
|
|
|
let source_info = source_registry.get_by_id(source_slice.source_id);
|
|
|
|
|
let lines: Vec<&str> = source_info.content.lines().collect();
|
|
|
|
|
|
|
|
|
|
if lines.is_empty() || source_slice.start_position.line >= lines.len() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut output = String::new();
|
|
|
|
|
let error_line = source_slice.start_position.line;
|
|
|
|
|
let start_line = error_line.saturating_sub(config.context_lines);
|
|
|
|
|
let end_line = (error_line + config.context_lines + 1).min(lines.len());
|
|
|
|
|
|
|
|
|
|
// Calculate the width needed for line numbers
|
|
|
|
|
let line_number_width = if config.show_line_numbers {
|
|
|
|
|
(end_line + 1).to_string().len()
|
|
|
|
|
} else {
|
|
|
|
|
0
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Show context lines
|
|
|
|
|
for line_idx in start_line..end_line {
|
|
|
|
|
let line_content = lines[line_idx];
|
|
|
|
|
let line_number = line_idx + 1;
|
|
|
|
|
|
|
|
|
|
if config.show_line_numbers {
|
|
|
|
|
let line_num_str = if config.colored {
|
|
|
|
|
if line_idx == error_line {
|
|
|
|
|
format!(
|
|
|
|
|
"\x1b[1;34m{:width$}\x1b[0m",
|
|
|
|
|
line_number,
|
|
|
|
|
width = line_number_width
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
format!(
|
|
|
|
|
"\x1b[34m{:width$}\x1b[0m",
|
|
|
|
|
line_number,
|
|
|
|
|
width = line_number_width
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
format!("{:width$}", line_number, width = line_number_width)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let separator = if config.colored {
|
|
|
|
|
if line_idx == error_line {
|
|
|
|
|
"\x1b[1;34m |\x1b[0m "
|
|
|
|
|
} else {
|
|
|
|
|
"\x1b[34m |\x1b[0m "
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
" | "
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
output.push_str(&format!(
|
|
|
|
|
" {}{}{}\n",
|
|
|
|
|
line_num_str, separator, line_content
|
|
|
|
|
));
|
|
|
|
|
} else {
|
|
|
|
|
output.push_str(&format!(" {}\n", line_content));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add error indicator on the error line
|
|
|
|
|
if line_idx == error_line {
|
|
|
|
|
let spaces_before =
|
|
|
|
|
" ".repeat(3 + line_number_width + 3 + source_slice.start_position.column);
|
|
|
|
|
|
|
|
|
|
let error_length = if source_slice.start_position.line
|
|
|
|
|
== source_slice.end_position.line
|
|
|
|
|
{
|
|
|
|
|
(source_slice.end_position.column - source_slice.start_position.column).max(1)
|
|
|
|
|
} else {
|
|
|
|
|
line_content.len() - source_slice.start_position.column
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let indicator = if config.use_unicode {
|
|
|
|
|
"^".repeat(error_length)
|
|
|
|
|
} else {
|
|
|
|
|
"^".repeat(error_length)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let colored_indicator = if config.colored {
|
|
|
|
|
format!("\x1b[1;31m{}\x1b[0m", indicator)
|
|
|
|
|
} else {
|
|
|
|
|
indicator
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Add the indicator line
|
|
|
|
|
if config.show_line_numbers {
|
|
|
|
|
let empty_line_number = " ".repeat(line_number_width);
|
|
|
|
|
let separator = if config.colored {
|
|
|
|
|
"\x1b[34m |\x1b[0m "
|
|
|
|
|
} else {
|
|
|
|
|
" | "
|
|
|
|
|
};
|
|
|
|
|
output.push_str(&format!(
|
|
|
|
|
" {}{}{}{}\n",
|
|
|
|
|
empty_line_number,
|
|
|
|
|
separator,
|
|
|
|
|
spaces_before[3 + line_number_width + 3..].to_string(),
|
|
|
|
|
colored_indicator
|
|
|
|
|
));
|
|
|
|
|
} else {
|
|
|
|
|
output.push_str(&format!(
|
|
|
|
|
" {}{}\n",
|
|
|
|
|
spaces_before[3..].to_string(),
|
|
|
|
|
colored_indicator
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add error type and additional context
|
|
|
|
|
let error_note = match self {
|
|
|
|
|
LoxError::TypeMismatch {
|
|
|
|
|
expected, found, ..
|
|
|
|
|
} => Some(format!("expected `{}`, found `{}`", expected, found)),
|
|
|
|
|
_ => None,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Some(note) = error_note {
|
|
|
|
|
let note_prefix = if config.show_line_numbers {
|
|
|
|
|
format!(
|
|
|
|
|
" {} {} ",
|
|
|
|
|
" ".repeat(line_number_width),
|
|
|
|
|
if config.colored {
|
|
|
|
|
"\x1b[34m|\x1b[0m"
|
|
|
|
|
} else {
|
|
|
|
|
"|"
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
" ".to_string()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let colored_note = if config.colored {
|
|
|
|
|
format!("\x1b[1;36mnote\x1b[0m: {}", note)
|
|
|
|
|
} else {
|
|
|
|
|
format!("note: {}", note)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
output.push_str(&format!("{}{}\n", note_prefix, colored_note));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Some(output)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Print error with source context to stdout
|
|
|
|
|
pub fn print_with_source(&self, source_registry: &SourceRegistry) {
|
|
|
|
|
println!("{}", self.display_with_source(source_registry));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Print error with source context and custom config to stdout
|
|
|
|
|
pub fn print_with_source_and_config(
|
|
|
|
|
&self,
|
|
|
|
|
source_registry: &SourceRegistry,
|
|
|
|
|
config: &ErrorDisplayConfig,
|
|
|
|
|
) {
|
|
|
|
|
println!(
|
|
|
|
|
"{}",
|
|
|
|
|
self.display_with_source_and_config(source_registry, config)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Legacy method for backwards compatibility
|
|
|
|
|
pub fn print_with_context(&self, source_registry: &SourceRegistry) {
|
|
|
|
|
self.print_with_source(source_registry);
|
2025-10-03 19:07:12 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-04 19:02:33 +02:00
|
|
|
impl fmt::Display for LoxError {
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
2025-10-03 19:07:12 +02:00
|
|
|
match self {
|
|
|
|
|
LoxError::LexicalError {
|
|
|
|
|
source_slice,
|
|
|
|
|
message,
|
|
|
|
|
} => {
|
2025-10-04 19:02:33 +02:00
|
|
|
write!(
|
|
|
|
|
f,
|
|
|
|
|
"Lexical error at {}:{}: {}",
|
|
|
|
|
source_slice.start_position.line + 1,
|
|
|
|
|
source_slice.start_position.column + 1,
|
|
|
|
|
message
|
|
|
|
|
)
|
2025-10-03 19:07:12 +02:00
|
|
|
}
|
|
|
|
|
LoxError::RuntimeError {
|
|
|
|
|
source_slice,
|
|
|
|
|
message,
|
|
|
|
|
} => {
|
2025-10-04 19:02:33 +02:00
|
|
|
write!(
|
|
|
|
|
f,
|
|
|
|
|
"Runtime error at {}:{}: {}",
|
|
|
|
|
source_slice.start_position.line + 1,
|
|
|
|
|
source_slice.start_position.column + 1,
|
|
|
|
|
message
|
|
|
|
|
)
|
2025-10-03 19:07:12 +02:00
|
|
|
}
|
|
|
|
|
LoxError::ParseError {
|
|
|
|
|
source_slice,
|
|
|
|
|
message,
|
|
|
|
|
} => {
|
2025-10-04 19:02:33 +02:00
|
|
|
write!(
|
|
|
|
|
f,
|
|
|
|
|
"Parse error at {}:{}: {}",
|
|
|
|
|
source_slice.start_position.line + 1,
|
|
|
|
|
source_slice.start_position.column + 1,
|
|
|
|
|
message
|
|
|
|
|
)
|
2025-10-03 19:07:12 +02:00
|
|
|
}
|
|
|
|
|
LoxError::IoError { message } => write!(f, "IO error: {}", message),
|
|
|
|
|
LoxError::TypeMismatch {
|
|
|
|
|
source_slice,
|
|
|
|
|
expected,
|
|
|
|
|
found,
|
|
|
|
|
} => {
|
|
|
|
|
write!(
|
|
|
|
|
f,
|
2025-10-04 19:02:33 +02:00
|
|
|
"Type mismatch at {}:{}: expected `{}`, found `{}`",
|
|
|
|
|
source_slice.start_position.line + 1,
|
|
|
|
|
source_slice.start_position.column + 1,
|
|
|
|
|
expected,
|
|
|
|
|
found
|
2025-10-03 19:07:12 +02:00
|
|
|
)
|
|
|
|
|
}
|
2026-02-11 16:35:05 +01:00
|
|
|
LoxError::Return {
|
|
|
|
|
value,
|
|
|
|
|
return_label,
|
|
|
|
|
source_slice,
|
|
|
|
|
} => {
|
|
|
|
|
write!(
|
|
|
|
|
f,
|
|
|
|
|
"Return error at {}:{}: value `{}`, label `{}`",
|
|
|
|
|
source_slice.start_position.line + 1,
|
|
|
|
|
source_slice.start_position.column + 1,
|
|
|
|
|
value,
|
|
|
|
|
return_label
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-10-03 19:07:12 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-12 20:45:35 +01:00
|
|
|
impl Error for LoxError {}
|
2025-10-03 19:07:12 +02:00
|
|
|
|
|
|
|
|
pub type LoxResult<T> = Result<T, LoxError>;
|
2025-10-04 19:02:33 +02:00
|
|
|
|
|
|
|
|
/// Helper function to create a lexical error
|
2025-10-06 18:52:32 +02:00
|
|
|
#[allow(dead_code)]
|
2025-10-04 21:05:00 +02:00
|
|
|
pub fn lexical_error<T>(source_slice: SourceSlice, message: impl Into<String>) -> LoxResult<T> {
|
|
|
|
|
Err(LoxError::LexicalError {
|
2025-10-04 19:02:33 +02:00
|
|
|
source_slice,
|
|
|
|
|
message: message.into(),
|
2025-10-04 21:05:00 +02:00
|
|
|
})
|
2025-10-04 19:02:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Helper function to create a parse error
|
2025-10-06 18:52:32 +02:00
|
|
|
#[allow(dead_code)]
|
2025-10-04 21:05:00 +02:00
|
|
|
pub fn parse_error<T>(source_slice: SourceSlice, message: impl Into<String>) -> LoxResult<T> {
|
|
|
|
|
Err(LoxError::ParseError {
|
2025-10-04 19:02:33 +02:00
|
|
|
source_slice,
|
|
|
|
|
message: message.into(),
|
2025-10-04 21:05:00 +02:00
|
|
|
})
|
2025-10-04 19:02:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Helper function to create a runtime error
|
2025-10-04 21:05:00 +02:00
|
|
|
pub fn runtime_error<T>(source_slice: SourceSlice, message: impl Into<String>) -> LoxResult<T> {
|
|
|
|
|
Err(LoxError::RuntimeError {
|
2025-10-04 19:02:33 +02:00
|
|
|
source_slice,
|
|
|
|
|
message: message.into(),
|
2025-10-04 21:05:00 +02:00
|
|
|
})
|
2025-10-04 19:02:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Helper function to create a type mismatch error
|
2025-10-04 21:05:00 +02:00
|
|
|
#[allow(dead_code)]
|
|
|
|
|
pub fn type_mismatch_error<T>(
|
2025-10-04 19:02:33 +02:00
|
|
|
source_slice: SourceSlice,
|
|
|
|
|
expected: impl Into<String>,
|
|
|
|
|
found: impl Into<String>,
|
2025-10-04 21:05:00 +02:00
|
|
|
) -> LoxResult<T> {
|
|
|
|
|
Err(LoxError::TypeMismatch {
|
2025-10-04 19:02:33 +02:00
|
|
|
source_slice,
|
|
|
|
|
expected: expected.into(),
|
|
|
|
|
found: found.into(),
|
2025-10-04 21:05:00 +02:00
|
|
|
})
|
2025-10-04 19:02:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Helper function to create an IO error
|
2025-10-04 21:05:00 +02:00
|
|
|
#[allow(dead_code)]
|
|
|
|
|
pub fn io_error<T>(message: impl Into<String>) -> LoxResult<T> {
|
|
|
|
|
Err(LoxError::IoError {
|
2025-10-04 19:02:33 +02:00
|
|
|
message: message.into(),
|
2025-10-04 21:05:00 +02:00
|
|
|
})
|
2025-10-04 19:02:33 +02:00
|
|
|
}
|