use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Span {
pub start: usize,
pub end: usize,
}
impl Span {
pub fn new(start: usize, end: usize) -> Self {
Self { start, end }
}
pub fn at(position: usize) -> Self {
Self { start: position, end: position + 1 }
}
pub fn len(&self) -> usize {
self.end.saturating_sub(self.start)
}
pub fn is_empty(&self) -> bool {
self.start >= self.end
}
}
impl fmt::Display for Span {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.len() <= 1 {
write!(f, "{}", self.start)
} else {
write!(f, "{}..{}", self.start, self.end)
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum RllError {
UnexpectedChar { char: char, position: usize },
Expected { expected: &'static str, position: usize },
UnclosedBracket { position: usize },
UnclosedParen { position: usize },
EmptyInput,
MissingTerminator,
InvalidInstruction { position: usize },
UnexpectedEof,
}
impl RllError {
pub fn position(&self) -> Option<usize> {
match self {
RllError::UnexpectedChar { position, .. } => Some(*position),
RllError::Expected { position, .. } => Some(*position),
RllError::UnclosedBracket { position } => Some(*position),
RllError::UnclosedParen { position } => Some(*position),
RllError::InvalidInstruction { position } => Some(*position),
RllError::EmptyInput | RllError::MissingTerminator | RllError::UnexpectedEof => None,
}
}
pub fn span(&self) -> Option<Span> {
self.position().map(Span::at)
}
pub fn format_with_context(&self, source: &str) -> String {
let mut result = String::new();
result.push_str(&format!("error: {}\n", self));
if let Some(pos) = self.position() {
if pos < source.len() {
let (line_start, line_num) = find_line_start(source, pos);
let line_end = source[line_start..].find('\n')
.map(|i| line_start + i)
.unwrap_or(source.len());
let line = &source[line_start..line_end];
let col = pos - line_start;
result.push_str(&format!(" --> position {}:{}\n", line_num, col));
let (display_line, display_col) = if line.len() > 80 {
let window_start = col.saturating_sub(30);
let window_end = (col + 50).min(line.len());
let prefix = if window_start > 0 { "..." } else { "" };
let suffix = if window_end < line.len() { "..." } else { "" };
let window = &line[window_start..window_end];
(format!("{}{}{}", prefix, window, suffix), col - window_start + prefix.len())
} else {
(line.to_string(), col)
};
let gutter = format!("{} | ", line_num);
result.push_str(&gutter);
result.push_str(&display_line);
result.push('\n');
let caret_padding: String = " ".repeat(gutter.len() + display_col);
result.push_str(&caret_padding);
result.push_str("^ here");
}
}
result
}
}
fn find_line_start(source: &str, pos: usize) -> (usize, usize) {
let mut line_start = 0;
let mut line_num = 1;
for (i, c) in source.char_indices() {
if i >= pos {
break;
}
if c == '\n' {
line_start = i + 1;
line_num += 1;
}
}
(line_start, line_num)
}
impl fmt::Display for RllError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RllError::UnexpectedChar { char, position } => {
write!(f, "unexpected character '{}' at position {}", char, position)
}
RllError::Expected { expected, position } => {
write!(f, "expected {} at position {}", expected, position)
}
RllError::UnclosedBracket { position } => {
write!(f, "unclosed bracket '[' at position {}", position)
}
RllError::UnclosedParen { position } => {
write!(f, "unclosed parenthesis '(' at position {}", position)
}
RllError::EmptyInput => {
write!(f, "empty input")
}
RllError::MissingTerminator => {
write!(f, "missing rung terminator ';'")
}
RllError::InvalidInstruction { position } => {
write!(f, "invalid instruction at position {}", position)
}
RllError::UnexpectedEof => {
write!(f, "unexpected end of input")
}
}
}
}
impl std::error::Error for RllError {}
pub type RllResult<T> = Result<T, RllError>;
#[derive(Debug, Clone)]
pub struct ParseError {
pub error: RllError,
pub source: String,
pub context: Option<ErrorContext>,
}
#[derive(Debug, Clone)]
pub struct ErrorContext {
pub program: String,
pub routine: String,
pub rung_number: u32,
}
impl ErrorContext {
pub fn new(program: impl Into<String>, routine: impl Into<String>, rung_number: u32) -> Self {
Self {
program: program.into(),
routine: routine.into(),
rung_number,
}
}
pub fn path(&self) -> String {
format!("{}/{}/Rung#{}", self.program, self.routine, self.rung_number)
}
}
impl ParseError {
pub fn new(error: RllError, source: impl Into<String>) -> Self {
Self {
error,
source: source.into(),
context: None,
}
}
pub fn with_context(mut self, context: ErrorContext) -> Self {
self.context = Some(context);
self
}
pub fn format(&self) -> String {
let mut result = String::new();
if let Some(ctx) = &self.context {
result.push_str(&format!("in {}\n", ctx.path()));
}
result.push_str(&self.error.format_with_context(&self.source));
result
}
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.format())
}
}
impl std::error::Error for ParseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.error)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_span_display() {
let span = Span::new(10, 15);
assert_eq!(format!("{}", span), "10..15");
let single = Span::at(42);
assert_eq!(format!("{}", single), "42");
}
#[test]
fn test_error_position() {
let err = RllError::UnexpectedChar { char: 'x', position: 5 };
assert_eq!(err.position(), Some(5));
let err = RllError::EmptyInput;
assert_eq!(err.position(), None);
}
#[test]
fn test_format_with_context() {
let err = RllError::UnclosedBracket { position: 4 };
let source = "XIC([Incomplete;";
let formatted = err.format_with_context(source);
assert!(formatted.contains("unclosed bracket"));
assert!(formatted.contains("XIC([Incomplete;"));
assert!(formatted.contains("^")); }
#[test]
fn test_format_with_context_multiline() {
let err = RllError::UnexpectedChar { char: '!', position: 15 };
let source = "XIC(Tag1)\nOTE(!)MOV(A,B);";
let formatted = err.format_with_context(source);
assert!(formatted.contains("unexpected character '!'"));
assert!(formatted.contains("OTE(!)MOV")); assert!(formatted.contains("2:")); }
#[test]
fn test_parse_error_with_context() {
let err = RllError::MissingTerminator;
let parse_err = ParseError::new(err, "XIC(Tag)OTE(Out)")
.with_context(ErrorContext::new("MainProgram", "MainRoutine", 5));
let formatted = parse_err.format();
assert!(formatted.contains("MainProgram/MainRoutine/Rung#5"));
assert!(formatted.contains("missing rung terminator"));
}
#[test]
fn test_long_line_truncation() {
let long_line = format!("{}XIC(BadTag!Here){}", "A".repeat(50), "B".repeat(50));
let err = RllError::UnexpectedChar { char: '!', position: 60 };
let formatted = err.format_with_context(&long_line);
assert!(formatted.contains("...") || formatted.len() < long_line.len() + 100);
assert!(formatted.contains("^"));
}
}