use crate::lexer::Token;
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub struct ParseError {
pub message: String,
pub token: Option<Token>,
pub position: usize,
pub category: ErrorCategory,
pub suggestion: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErrorCategory {
UnexpectedToken,
MissingToken,
InvalidSyntax,
TypeError,
NameError,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Parse error at position {}: {}",
self.position, self.message
)?;
if let Some(ref suggestion) = self.suggestion {
write!(f, "\n Suggestion: {}", suggestion)?;
}
Ok(())
}
}
impl ParseError {
pub fn new(message: String, position: usize) -> Self {
ParseError {
message,
token: None,
position,
category: ErrorCategory::InvalidSyntax,
suggestion: None,
}
}
pub fn with_token(mut self, token: Token) -> Self {
self.token = Some(token);
self
}
pub fn with_category(mut self, category: ErrorCategory) -> Self {
self.category = category;
self
}
pub fn with_suggestion(mut self, suggestion: String) -> Self {
self.suggestion = Some(suggestion);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RecoveryPoint {
Semicolon,
CloseBrace,
TopLevelKeyword,
Any,
}
pub fn is_recovery_point(token: &Token, point: RecoveryPoint) -> bool {
match point {
RecoveryPoint::Semicolon => matches!(token, Token::Semicolon),
RecoveryPoint::CloseBrace => matches!(token, Token::RBrace),
RecoveryPoint::TopLevelKeyword => matches!(
token,
Token::Fn
| Token::Struct
| Token::Enum
| Token::Trait
| Token::Impl
| Token::Const
| Token::Static
| Token::Use
),
RecoveryPoint::Any => {
is_recovery_point(token, RecoveryPoint::Semicolon)
|| is_recovery_point(token, RecoveryPoint::CloseBrace)
|| is_recovery_point(token, RecoveryPoint::TopLevelKeyword)
}
}
}
pub fn unexpected_token_message(expected: &str, found: &Token) -> String {
let found_str = format!("{:?}", found);
format!(
"expected `{}`, found `{}`",
expected,
found_str.trim_matches('"')
)
}
pub fn missing_token_message(expected: &str) -> String {
format!("expected `{}`, but reached end of input", expected)
}
pub fn delimiter_context(opening: &str) -> Option<&'static str> {
match opening {
"(" => Some("to close function call or grouping"),
"[" => Some("to close array or index"),
"{" => Some("to close block or struct literal"),
_ => None,
}
}
pub fn suggest_fix(error_context: &str) -> Option<String> {
match error_context {
"missing_type" => Some("Add a type annotation (e.g., ': int', ': string')".to_string()),
"missing_value" => Some("Add an expression after '='".to_string()),
"missing_semicolon" => Some("Add a semicolon ';' at the end of the statement".to_string()),
"missing_brace" => Some("Add a closing brace '}'".to_string()),
"missing_paren" => Some("Add a closing parenthesis ')'".to_string()),
"missing_bracket" => Some("Add a closing bracket ']'".to_string()),
_ => None,
}
}
pub type ParseResult<T> = Result<T, Vec<ParseError>>;
#[derive(Debug, Clone)]
pub struct PartialResult<T> {
pub value: T,
pub errors: Vec<ParseError>,
}
impl<T> PartialResult<T> {
pub fn ok(value: T) -> Self {
PartialResult {
value,
errors: Vec::new(),
}
}
pub fn with_error(value: T, error: ParseError) -> Self {
PartialResult {
value,
errors: vec![error],
}
}
pub fn with_errors(value: T, errors: Vec<ParseError>) -> Self {
PartialResult { value, errors }
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn into_result(self) -> Result<T, Vec<ParseError>> {
if self.errors.is_empty() {
Ok(self.value)
} else {
Err(self.errors)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_error_display() {
let error =
ParseError::new("test error".to_string(), 10).with_suggestion("try this".to_string());
let display = format!("{}", error);
assert!(display.contains("test error"));
assert!(display.contains("try this"));
}
#[test]
fn test_recovery_points() {
assert!(is_recovery_point(
&Token::Semicolon,
RecoveryPoint::Semicolon
));
assert!(is_recovery_point(&Token::RBrace, RecoveryPoint::CloseBrace));
assert!(is_recovery_point(
&Token::Fn,
RecoveryPoint::TopLevelKeyword
));
assert!(is_recovery_point(&Token::Semicolon, RecoveryPoint::Any));
}
#[test]
fn test_unexpected_token_message_format() {
let msg = unexpected_token_message(")", &Token::RBracket);
assert!(msg.contains("expected"));
assert!(msg.contains(")"));
assert!(msg.contains("RBracket") || msg.contains("]"));
}
#[test]
fn test_delimiter_context() {
assert_eq!(
delimiter_context("("),
Some("to close function call or grouping")
);
assert_eq!(delimiter_context("["), Some("to close array or index"));
assert_eq!(
delimiter_context("{"),
Some("to close block or struct literal")
);
assert_eq!(delimiter_context("x"), None);
}
#[test]
fn test_partial_result() {
let ok_result = PartialResult::ok(42);
assert!(!ok_result.has_errors());
let err_result = PartialResult::with_error(42, ParseError::new("error".to_string(), 0));
assert!(err_result.has_errors());
assert_eq!(err_result.errors.len(), 1);
}
}