Skip to main content

graphforge_ast/
parse_error.rs

1//! Structured parse error type for the GraphForge Cypher parser.
2
3use graphforge_core::Span;
4use serde::{Deserialize, Serialize};
5
6/// The kind of parse error.
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8#[non_exhaustive]
9pub enum ParseErrorKind {
10    /// The lexer encountered a byte sequence it could not tokenize.
11    UnexpectedChar,
12    /// The parser encountered a token that does not fit the grammar at this
13    /// position.
14    UnexpectedToken {
15        /// What was found.
16        found: String,
17        /// What the parser expected (human-readable).
18        expected: Vec<String>,
19    },
20    /// A string literal was opened but never closed.
21    UnterminatedString,
22    /// A block comment was opened with `/*` but never closed with `*/`.
23    UnterminatedBlockComment,
24    /// An integer or float literal could not be parsed.
25    InvalidNumericLiteral,
26    /// A `$` parameter prefix was not followed by a valid name or index.
27    InvalidParameter,
28    /// A query ended before it was syntactically complete.
29    UnexpectedEof {
30        /// What the parser expected at end of input.
31        expected: Vec<String>,
32    },
33}
34
35/// A structured parse error produced by `graphforge-cypher`.
36///
37/// `ParseError` is the error type in the `Result` returned by
38/// `graphforge_cypher::parse`. The differential test harness uses the `span` and
39/// `kind` fields to assert that both parsers flag the same source location.
40/// A structured parse error produced by `graphforge-cypher`.
41///
42/// `ParseError` is the error type in the `Result` returned by
43/// `graphforge_cypher::parse`. The differential test harness uses the `span` and
44/// `kind` fields to assert that both parsers flag the same source location.
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46pub struct ParseError {
47    /// Structured classification of the failure.
48    pub kind: ParseErrorKind,
49    /// Source location of the offending token or character.
50    pub span: Span,
51    /// Human-readable explanation (may include context beyond `kind`).
52    pub message: String,
53}
54
55impl std::fmt::Display for ParseError {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        let kind_str = match &self.kind {
58            ParseErrorKind::UnexpectedChar => "unexpected character".to_owned(),
59            ParseErrorKind::UnexpectedToken { found, .. } => {
60                format!("unexpected token '{found}'")
61            }
62            ParseErrorKind::UnterminatedString => "unterminated string".to_owned(),
63            ParseErrorKind::UnterminatedBlockComment => "unterminated block comment".to_owned(),
64            ParseErrorKind::InvalidNumericLiteral => "invalid numeric literal".to_owned(),
65            ParseErrorKind::InvalidParameter => "invalid parameter".to_owned(),
66            ParseErrorKind::UnexpectedEof { .. } => "unexpected end of input".to_owned(),
67        };
68        write!(f, "{kind_str} at {}", self.span)
69    }
70}
71
72impl std::error::Error for ParseError {}
73
74impl ParseError {
75    /// Create a new `ParseError`.
76    #[must_use]
77    pub fn new(kind: ParseErrorKind, span: Span, message: impl Into<String>) -> Self {
78        Self {
79            kind,
80            span,
81            message: message.into(),
82        }
83    }
84}