cel_parser/
error.rs

1use std::{fmt::Display, iter};
2
3use lalrpop_util::lexer::Token;
4use thiserror::Error;
5
6#[derive(Debug, Clone, PartialEq)]
7pub struct Location {
8    pub line: usize,
9    pub column: usize,
10    pub absolute: usize,
11}
12
13#[derive(Debug, Default)]
14pub struct Span {
15    pub start: Option<Location>,
16    pub end: Option<Location>,
17}
18
19impl Display for Span {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        match (&self.start, &self.end) {
22            (Some(start), Some(end)) if start == end => {
23                write!(f, "[{}:{}]", start.line, start.column)
24            }
25            (Some(location), None) | (None, Some(location)) => {
26                write!(f, "[{}:{}]", location.line, location.column)
27            }
28            (Some(start), Some(end)) => write!(
29                f,
30                "[{}:{}]->[{}:{}]",
31                start.line, start.column, end.line, end.column
32            ),
33            (None, None) => write!(f, "?",),
34        }
35    }
36}
37
38impl Span {
39    fn single_location(location: Location) -> Span {
40        Span {
41            start: Some(location.clone()),
42            end: Some(location),
43        }
44    }
45}
46
47#[derive(Error, Debug)]
48#[error("Error parsing: {msg} at {span}")]
49pub struct ParseError {
50    pub msg: String,
51    pub expected: Vec<String>,
52    pub span: Span,
53}
54
55impl ParseError {
56    pub(super) fn from_lalrpop(
57        src_str: &str,
58        err: lalrpop_util::ParseError<usize, Token<'_>, &str>,
59    ) -> Self {
60        use lalrpop_util::ParseError::*;
61
62        match err {
63            InvalidToken { location } => ParseError {
64                span: byte_pos_to_src_location(src_str, location)
65                    .map(Span::single_location)
66                    .unwrap_or_default(),
67                expected: Vec::new(),
68                msg: "invalid token".into(),
69            },
70            UnrecognizedEof { location, expected } => ParseError {
71                msg: "unrecognized eof".into(),
72                span: byte_pos_to_src_location(src_str, location)
73                    .map(Span::single_location)
74                    .unwrap_or_default(),
75                expected,
76            },
77            UnrecognizedToken {
78                token: (start, token, end),
79                expected,
80            } => ParseError {
81                msg: format!("unrecognized token: '{}'", token),
82                span: Span {
83                    start: byte_pos_to_src_location(src_str, start),
84                    end: byte_pos_to_src_location(src_str, end),
85                },
86                expected,
87            },
88            ExtraToken {
89                token: (start, token, end),
90            } => ParseError {
91                msg: format!("extra token: '{}'", token),
92                span: Span {
93                    start: byte_pos_to_src_location(src_str, start),
94                    end: byte_pos_to_src_location(src_str, end),
95                },
96                expected: Vec::new(),
97            },
98            User { error } => ParseError {
99                msg: error.into(),
100                expected: Vec::new(),
101                span: Span::default(),
102            },
103        }
104    }
105}
106
107// Slightly simplified but heavily inspired by
108// https://github.com/gluon-lang/gluon/blob/f8326d21a14b5f21d203e9c43fa5bb7f0688a74c/base/src/source.rs
109fn byte_pos_to_src_location(src_str: &str, byte_pos: usize) -> Option<Location> {
110    let src_bytes = src_str.as_bytes();
111    let total_len = src_bytes.len();
112
113    let line_indices: Vec<usize> = {
114        let input_indices = src_bytes
115            .iter()
116            .enumerate()
117            .filter(|&(_, b)| *b == b'\n')
118            .map(|(i, _)| i + 1); // index of first char in the line
119
120        iter::once(0).chain(input_indices).collect()
121    };
122
123    if byte_pos <= total_len {
124        let num_lines = line_indices.len();
125
126        let line_index = (0..num_lines)
127            .find(|&i| line_indices[i] > byte_pos)
128            .map(|i| i - 1)
129            .unwrap_or(num_lines - 1);
130
131        let line_byte_pos = line_indices[line_index];
132        Some(Location {
133            line: line_index,
134            column: byte_pos - line_byte_pos,
135            absolute: byte_pos,
136        })
137    } else {
138        None
139    }
140}