2026-02-26 11:50:51 +01:00

212 lines
7.1 KiB
Rust

//! Error formatting for Geolog
//!
//! Provides user-friendly error messages using ariadne for nice formatting.
use ariadne::{Color, Label, Report, ReportKind, Source};
use chumsky::prelude::Simple;
use std::ops::Range;
use crate::lexer::Token;
/// Format lexer errors into a user-friendly string
pub fn format_lexer_errors(source: &str, errors: Vec<Simple<char>>) -> String {
let mut output = Vec::new();
for error in errors {
let span = error.span();
let report = Report::build(ReportKind::Error, (), span.start)
.with_message("Lexical error")
.with_label(
Label::new(span.clone())
.with_message(format_lexer_error(&error))
.with_color(Color::Red),
);
report
.finish()
.write(Source::from(source), &mut output)
.expect("Failed to write error report");
}
String::from_utf8(output).unwrap_or_else(|_| "Error formatting failed".to_string())
}
/// Format a single lexer error into a readable message
fn format_lexer_error(error: &Simple<char>) -> String {
let found = error
.found()
.map(|c| format!("'{}'", c))
.unwrap_or_else(|| "end of input".to_string());
if let Some(_expected) = error.expected().next() {
format!(
"Unexpected {}, expected {}",
found,
format_char_set(error.expected())
)
} else {
format!("Unexpected character {}", found)
}
}
/// Format parser errors into a user-friendly string
pub fn format_parser_errors(
source: &str,
errors: Vec<Simple<Token>>,
token_spans: &[(Token, Range<usize>)],
) -> String {
let mut output = Vec::new();
for error in errors {
let span = error.span();
// Map token span to character span
// The span could be either:
// 1. A token index (0, 1, 2, ..., n-1 for n tokens) - look up in token_spans
// 2. Already a character position (from custom errors that captured spans)
//
// Best heuristic: check if the span matches a token's character range.
// If so, it's a character position. Otherwise, treat as token index.
let is_char_position = token_spans
.iter()
.any(|(_, char_range)| char_range.start == span.start && char_range.end == span.end);
let char_span = if is_char_position {
// Span exactly matches a token's character range - use as-is
span.clone()
} else if span.start < token_spans.len() {
// Span.start is a valid token index - use token's character range
token_spans[span.start].1.clone()
} else if span.start == token_spans.len() {
// End of input marker - use the end of the last token
if let Some((_, last_range)) = token_spans.last() {
last_range.end..last_range.end
} else {
0..0
}
} else {
// Fallback: treat as character position
let start = span.start.min(source.len());
let end = span.end.min(source.len());
start..end
};
let report = Report::build(ReportKind::Error, (), char_span.start)
.with_message("Parse error")
.with_label(
Label::new(char_span.clone())
.with_message(format_parser_error(&error))
.with_color(Color::Red),
);
report
.finish()
.write(Source::from(source), &mut output)
.expect("Failed to write error report");
}
String::from_utf8(output).unwrap_or_else(|_| "Error formatting failed".to_string())
}
/// Format a single parser error into a readable message
fn format_parser_error(error: &Simple<Token>) -> String {
use chumsky::error::SimpleReason;
let found = error
.found()
.map(|t| format!("'{}'", format_token(t)))
.unwrap_or_else(|| "end of input".to_string());
// Check for custom error messages first (from Simple::custom())
if let SimpleReason::Custom(msg) = error.reason() {
return msg.clone();
}
let expected = format_token_set(error.expected());
if !expected.is_empty() {
// Check for common patterns and provide helpful messages
let expected_str = expected.join(", ");
// Detect common mistakes
if expected.contains(&"';'".to_string()) && error.found() == Some(&Token::Colon) {
return format!(
"Expected semicolon ';' to end declaration, found '{}'",
format_token(error.found().unwrap())
);
}
if expected.contains(&"':'".to_string()) && error.found() == Some(&Token::Semicolon) {
return format!(
"Expected colon ':' before type, found '{}'",
format_token(error.found().unwrap())
);
}
format!("Unexpected {}, expected one of: {}", found, expected_str)
} else if let Some(label) = error.label() {
label.to_string()
} else {
format!("Unexpected token {}", found)
}
}
/// Format a token for display
fn format_token(token: &Token) -> String {
match token {
Token::Namespace => "namespace".to_string(),
Token::Theory => "theory".to_string(),
Token::Instance => "instance".to_string(),
Token::Query => "query".to_string(),
Token::Sort => "Sort".to_string(),
Token::Prop => "Prop".to_string(),
Token::Forall => "forall".to_string(),
Token::Exists => "exists".to_string(),
Token::True => "true".to_string(),
Token::False => "false".to_string(),
Token::Ident(s) => s.clone(),
Token::LBrace => "{".to_string(),
Token::RBrace => "}".to_string(),
Token::LParen => "(".to_string(),
Token::RParen => ")".to_string(),
Token::LBracket => "[".to_string(),
Token::RBracket => "]".to_string(),
Token::Colon => ":".to_string(),
Token::Semicolon => ";".to_string(),
Token::Comma => ",".to_string(),
Token::Dot => ".".to_string(),
Token::Slash => "/".to_string(),
Token::Arrow => "->".to_string(),
Token::Eq => "=".to_string(),
Token::Turnstile => "|-".to_string(),
Token::And => r"/\".to_string(),
Token::Or => r"\/".to_string(),
Token::Question => "?".to_string(),
Token::Chase => "chase".to_string(),
}
}
/// Format a set of expected tokens
fn format_token_set<'a>(expected: impl Iterator<Item = &'a Option<Token>>) -> Vec<String> {
expected
.filter_map(|opt| opt.as_ref())
.map(|t| format!("'{}'", format_token(t)))
.collect()
}
/// Format a set of expected characters
fn format_char_set<'a>(expected: impl Iterator<Item = &'a Option<char>>) -> String {
let chars: Vec<String> = expected
.filter_map(|opt| opt.as_ref())
.map(|c| format!("'{}'", c))
.collect();
if chars.is_empty() {
"valid character".to_string()
} else if chars.len() == 1 {
chars[0].clone()
} else {
chars.join(" or ")
}
}