use thiserror::Error;
pub use crate::lex::span::SourcePos;
#[derive(Debug, Clone, Error, PartialEq)]
pub enum LexError {
#[error("line {}, column {}: invalid indentation: {} spaces (must be multiple of 2)", .pos.line(), .pos.column(), .spaces)]
InvalidIndentation {
spaces: usize,
pos: SourcePos,
},
#[error("line {}, column {}: tab character not allowed for indentation", .pos.line(), .pos.column())]
TabInIndentation {
pos: SourcePos,
},
#[error("line {}, column {}: indent depth {} exceeds maximum {}", .pos.line(), .pos.column(), .depth, .max)]
IndentTooDeep {
depth: usize,
max: usize,
pos: SourcePos,
},
#[error("line {}, column {}: unclosed quoted string", .pos.line(), .pos.column())]
UnclosedQuote {
pos: SourcePos,
},
#[error("line {}, column {}: unclosed expression", .pos.line(), .pos.column())]
UnclosedExpression {
pos: SourcePos,
},
#[error("line {}, column {}: invalid escape sequence '{}'", .pos.line(), .pos.column(), .sequence)]
InvalidEscape {
sequence: String,
pos: SourcePos,
},
#[error("line {}, column {}: invalid reference format: {}", .pos.line(), .pos.column(), .message)]
InvalidReference {
message: String,
pos: SourcePos,
},
#[error("line {}, column {}: invalid token: {}", .pos.line(), .pos.column(), .message)]
InvalidToken {
message: String,
pos: SourcePos,
},
#[error("line {}, column {}: string length {} exceeds maximum {}", .pos.line(), .pos.column(), .length, .max)]
StringTooLong {
length: usize,
max: usize,
pos: SourcePos,
},
#[error("line {}, column {}: recursion depth {} exceeds maximum {}", .pos.line(), .pos.column(), .depth, .max)]
RecursionTooDeep {
depth: usize,
max: usize,
pos: SourcePos,
},
#[error("line {}, column {}: field count {} exceeds maximum {}", .pos.line(), .pos.column(), .count, .max)]
TooManyFields {
count: usize,
max: usize,
pos: SourcePos,
},
#[error("line {}, column {}: parenthesis depth {} exceeds maximum {}", .pos.line(), .pos.column(), .depth, .max)]
ParenthesisDepthExceeded {
depth: usize,
max: usize,
pos: SourcePos,
},
#[error("trailing comma not allowed in matrix row")]
TrailingComma,
#[error("expected comma after closing quote, got '{0}'")]
ExpectedCommaAfterQuote(char),
#[error("quote character found in unquoted CSV field: '{0}'")]
QuoteInUnquotedField(String),
#[error("unbalanced brackets in tensor literal")]
UnbalancedBrackets,
#[error("invalid number in tensor: {0}")]
InvalidNumber(String),
#[error("empty tensor not allowed")]
EmptyTensor,
#[error("mixed types in tensor (expected number, got '{0}')")]
MixedTypes(String),
#[error("inconsistent dimensions in tensor")]
InconsistentDimensions,
#[error("unexpected character in tensor: '{0}'")]
UnexpectedChar(char),
#[error("invalid tensor structure: {0}")]
InvalidStructure(String),
}
impl LexError {
#[inline]
pub fn position(&self) -> Option<SourcePos> {
match self {
LexError::InvalidIndentation { pos, .. } => Some(*pos),
LexError::TabInIndentation { pos } => Some(*pos),
LexError::IndentTooDeep { pos, .. } => Some(*pos),
LexError::UnclosedQuote { pos } => Some(*pos),
LexError::UnclosedExpression { pos } => Some(*pos),
LexError::InvalidEscape { pos, .. } => Some(*pos),
LexError::InvalidReference { pos, .. } => Some(*pos),
LexError::InvalidToken { pos, .. } => Some(*pos),
LexError::StringTooLong { pos, .. } => Some(*pos),
LexError::RecursionTooDeep { pos, .. } => Some(*pos),
LexError::TooManyFields { pos, .. } => Some(*pos),
LexError::ParenthesisDepthExceeded { pos, .. } => Some(*pos),
LexError::TrailingComma
| LexError::ExpectedCommaAfterQuote(_)
| LexError::QuoteInUnquotedField(_)
| LexError::UnbalancedBrackets
| LexError::InvalidNumber(_)
| LexError::EmptyTensor
| LexError::MixedTypes(_)
| LexError::InconsistentDimensions
| LexError::UnexpectedChar(_)
| LexError::InvalidStructure(_) => None,
}
}
#[inline]
pub fn is_resource_limit(&self) -> bool {
matches!(
self,
LexError::StringTooLong { .. }
| LexError::RecursionTooDeep { .. }
| LexError::TooManyFields { .. }
| LexError::ParenthesisDepthExceeded { .. }
| LexError::IndentTooDeep { .. }
)
}
#[inline]
pub fn is_tensor_error(&self) -> bool {
matches!(
self,
LexError::UnbalancedBrackets
| LexError::InvalidNumber(_)
| LexError::EmptyTensor
| LexError::MixedTypes(_)
| LexError::InconsistentDimensions
| LexError::UnexpectedChar(_)
| LexError::InvalidStructure(_)
)
}
#[inline]
pub fn is_csv_error(&self) -> bool {
matches!(
self,
LexError::TrailingComma
| LexError::ExpectedCommaAfterQuote(_)
| LexError::QuoteInUnquotedField(_)
| LexError::UnclosedQuote { .. }
| LexError::UnclosedExpression { .. }
| LexError::InvalidEscape { .. }
)
}
}
pub type LexResult<T> = Result<T, LexError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_source_pos_new() {
let pos = SourcePos::new(10, 5);
assert_eq!(pos.line(), 10);
assert_eq!(pos.column(), 5);
}
#[test]
fn test_source_pos_display() {
let pos = SourcePos::new(42, 15);
assert_eq!(format!("{}", pos), "line 42, column 15");
}
#[test]
fn test_source_pos_default() {
let pos = SourcePos::default();
assert_eq!(pos.line(), 0);
assert_eq!(pos.column(), 0);
}
#[test]
fn test_error_position_extraction() {
let pos = SourcePos::new(10, 20);
assert_eq!(
LexError::InvalidIndentation { spaces: 3, pos }.position(),
Some(pos)
);
assert_eq!(LexError::TabInIndentation { pos }.position(), Some(pos));
assert_eq!(LexError::UnclosedQuote { pos }.position(), Some(pos));
assert_eq!(LexError::TrailingComma.position(), None);
assert_eq!(LexError::EmptyTensor.position(), None);
assert_eq!(LexError::UnbalancedBrackets.position(), None);
}
#[test]
fn test_is_resource_limit() {
let pos = SourcePos::new(1, 1);
assert!(LexError::StringTooLong {
length: 100,
max: 50,
pos
}
.is_resource_limit());
assert!(LexError::RecursionTooDeep {
depth: 10,
max: 5,
pos
}
.is_resource_limit());
assert!(LexError::TooManyFields {
count: 100,
max: 50,
pos
}
.is_resource_limit());
assert!(LexError::IndentTooDeep {
depth: 10,
max: 5,
pos
}
.is_resource_limit());
assert!(!LexError::TrailingComma.is_resource_limit());
assert!(!LexError::EmptyTensor.is_resource_limit());
}
#[test]
fn test_is_tensor_error() {
assert!(LexError::UnbalancedBrackets.is_tensor_error());
assert!(LexError::EmptyTensor.is_tensor_error());
assert!(LexError::InconsistentDimensions.is_tensor_error());
assert!(LexError::InvalidNumber("abc".to_string()).is_tensor_error());
assert!(LexError::UnexpectedChar('x').is_tensor_error());
assert!(!LexError::TrailingComma.is_tensor_error());
assert!(!LexError::UnclosedQuote {
pos: SourcePos::new(1, 1)
}
.is_tensor_error());
}
#[test]
fn test_is_csv_error() {
assert!(LexError::TrailingComma.is_csv_error());
assert!(LexError::ExpectedCommaAfterQuote('x').is_csv_error());
assert!(LexError::QuoteInUnquotedField("test".to_string()).is_csv_error());
assert!(LexError::UnclosedQuote {
pos: SourcePos::new(1, 1)
}
.is_csv_error());
assert!(LexError::UnclosedExpression {
pos: SourcePos::new(1, 1)
}
.is_csv_error());
assert!(!LexError::EmptyTensor.is_csv_error());
assert!(!LexError::UnbalancedBrackets.is_csv_error());
}
#[test]
fn test_error_display() {
let pos = SourcePos::new(5, 10);
let err = LexError::InvalidIndentation { spaces: 3, pos };
let msg = format!("{}", err);
assert!(msg.contains("line 5"));
assert!(msg.contains("column 10"));
assert!(msg.contains("3 spaces"));
let err = LexError::TrailingComma;
assert_eq!(
format!("{}", err),
"trailing comma not allowed in matrix row"
);
let err = LexError::EmptyTensor;
assert_eq!(format!("{}", err), "empty tensor not allowed");
}
#[test]
fn test_error_equality() {
let pos = SourcePos::new(1, 1);
assert_eq!(
LexError::UnclosedQuote { pos },
LexError::UnclosedQuote { pos }
);
assert_ne!(
LexError::UnclosedQuote { pos },
LexError::UnclosedExpression { pos }
);
assert_eq!(LexError::TrailingComma, LexError::TrailingComma);
assert_eq!(LexError::EmptyTensor, LexError::EmptyTensor);
}
#[test]
fn test_error_clone() {
let pos = SourcePos::new(5, 10);
let original = LexError::InvalidToken {
message: "test".to_string(),
pos,
};
let cloned = original.clone();
assert_eq!(original, cloned);
}
#[test]
fn test_error_is_std_error() {
fn accepts_error<E: std::error::Error>(_: E) {}
accepts_error(LexError::TrailingComma);
accepts_error(LexError::EmptyTensor);
accepts_error(LexError::UnclosedQuote {
pos: SourcePos::new(1, 1),
});
}
}