Skip to main content

gram_codec/parser/
error.rs

1//! Error types for the nom-based parser
2
3use super::types::Location;
4use thiserror::Error;
5
6/// Errors that can occur during parsing
7#[derive(Debug, Error)]
8pub enum ParseError {
9    /// Syntax error with location and context
10    #[error("Syntax error at {location}: expected {expected}, found '{found}'")]
11    SyntaxError {
12        location: Location,
13        expected: String,
14        found: String,
15        context: Vec<String>,
16    },
17
18    /// Unexpected input after successful parse
19    #[error("Unexpected input at {location}: {snippet}")]
20    UnexpectedInput { location: Location, snippet: String },
21
22    /// Invalid value (number, string, identifier)
23    #[error("Invalid {kind} at {location}: {reason}")]
24    InvalidValue {
25        location: Location,
26        kind: String,
27        reason: String,
28    },
29
30    /// Unmatched delimiter (bracket, paren, brace)
31    #[error("Unmatched {delimiter} at {location}")]
32    UnmatchedDelimiter { location: Location, delimiter: char },
33
34    /// Internal parser error (should not occur in production)
35    #[error("Internal parser error: {message}")]
36    Internal { message: String },
37}
38
39impl ParseError {
40    /// Create a syntax error from nom's VerboseError
41    pub fn from_nom_error(input: &str, err: nom::Err<nom::error::VerboseError<&str>>) -> Self {
42        match err {
43            nom::Err::Error(e) | nom::Err::Failure(e) => {
44                let (error_input, kind) = e
45                    .errors
46                    .first()
47                    .map(|(i, k)| (*i, k))
48                    .unwrap_or((input, &nom::error::VerboseErrorKind::Context("unknown")));
49
50                let offset = input.len() - error_input.len();
51                let location = Location::from_offset(input, offset);
52
53                let found = error_input.chars().take(20).collect::<String>();
54                let expected = format!("{:?}", kind);
55
56                ParseError::SyntaxError {
57                    location,
58                    expected,
59                    found,
60                    context: Vec::new(),
61                }
62            }
63            nom::Err::Incomplete(_) => ParseError::Internal {
64                message: "Unexpected incomplete parse (streaming not supported)".to_string(),
65            },
66        }
67    }
68
69    /// Get the location of this error
70    pub fn location(&self) -> Option<Location> {
71        match self {
72            ParseError::SyntaxError { location, .. }
73            | ParseError::UnexpectedInput { location, .. }
74            | ParseError::InvalidValue { location, .. }
75            | ParseError::UnmatchedDelimiter { location, .. } => Some(*location),
76            ParseError::Internal { .. } => None,
77        }
78    }
79
80    /// Add context to this error
81    pub fn with_context(mut self, context: String) -> Self {
82        if let ParseError::SyntaxError {
83            context: ref mut ctx,
84            ..
85        } = self
86        {
87            ctx.push(context);
88        }
89        self
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn test_error_location() {
99        let err = ParseError::SyntaxError {
100            location: Location::new(2, 5, 10),
101            expected: "identifier".to_string(),
102            found: "123".to_string(),
103            context: vec![],
104        };
105
106        assert_eq!(err.location().unwrap().line, 2);
107        assert_eq!(err.location().unwrap().column, 5);
108    }
109
110    #[test]
111    fn test_error_with_context() {
112        let err = ParseError::SyntaxError {
113            location: Location::new(1, 1, 0),
114            expected: "value".to_string(),
115            found: "x".to_string(),
116            context: vec![],
117        };
118
119        let err = err.with_context("in record".to_string());
120
121        match err {
122            ParseError::SyntaxError { context, .. } => {
123                assert_eq!(context.len(), 1);
124                assert_eq!(context[0], "in record");
125            }
126            _ => panic!("Expected SyntaxError"),
127        }
128    }
129}