Skip to main content

pln_parse/
error.rs

1use std::fmt;
2
3use crate::ast::Span;
4
5/// A parse or validation error with source location.
6#[derive(Debug, Clone)]
7pub struct ParseError {
8    pub message: String,
9    pub span: Span,
10    pub kind: ErrorKind,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum ErrorKind {
15    UnexpectedChar(char),
16    UnexpectedEof,
17    MixedOperators,
18    UnclosedGroup,
19    UnclosedString,
20    InvalidUnit(String),
21    InvalidNumber,
22    EmptyGroup,
23    TrailingInput,
24    InvalidUnitForSplit { unit: String, split: String },
25}
26
27impl ParseError {
28    pub fn format_with_source(&self, source: &str) -> String {
29        let (line, column) = byte_offset_to_position(source, self.span.start);
30        let source_line = source.lines().nth(line - 1).unwrap_or("");
31
32        let mut output = format!("error: {}\n", self.message);
33        output.push_str(&format!("  --> input:{}:{}\n", line, column));
34        output.push_str("   |\n");
35        output.push_str(&format!("{:>3} | {}\n", line, source_line));
36        output.push_str("   | ");
37
38        // Caret pointing at the error position
39        let line_start = source[..self.span.start]
40            .rfind('\n')
41            .map(|position| position + 1)
42            .unwrap_or(0);
43        let offset_in_line = self.span.start - line_start;
44        for _ in 0..offset_in_line {
45            output.push(' ');
46        }
47        let caret_len = (self.span.end - self.span.start).max(1);
48        for _ in 0..caret_len {
49            output.push('^');
50        }
51        output.push('\n');
52        output
53    }
54}
55
56impl fmt::Display for ParseError {
57    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
58        write!(formatter, "{}", self.message)
59    }
60}
61
62impl std::error::Error for ParseError {}
63
64fn byte_offset_to_position(source: &str, offset: usize) -> (usize, usize) {
65    let before = &source[..offset.min(source.len())];
66    let line = before
67        .chars()
68        .filter(|&character| character == '\n')
69        .count()
70        + 1;
71    let column = before
72        .rfind('\n')
73        .map(|position| offset - position)
74        .unwrap_or(offset + 1);
75    (line, column)
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn byte_offset_single_line() {
84        assert_eq!(byte_offset_to_position("(a|b/c)", 4), (1, 5));
85    }
86
87    #[test]
88    fn byte_offset_multiline() {
89        assert_eq!(byte_offset_to_position("abc\ndef\nghi", 5), (2, 2));
90    }
91}