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}