Skip to main content

inillucent_sql/
diagnostic.rs

1//! Syntax diagnostics: what was wrong, and exactly where.
2//!
3//! Invariant: every parse failure carries the byte offset of the token that
4//! caused it, and that offset is compared against the pinned release in tests.
5//! A message may be worded differently from SQLite's; an offset may not differ,
6//! because an offset is what an editor underlines and what a caller reports.
7//!
8//! The expected-token set is deliberately the smallest useful one rather than
9//! the full first-set of the production. A list of forty keywords is not a
10//! diagnostic, it is a grammar dump.
11
12use inillucent_base::{DbError, PrimaryCode};
13
14use crate::lexer::{LexError, LexErrorKind, Span};
15
16/// Why a parse failed.
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub enum ParseErrorKind {
19    /// The lexer refused a byte sequence.
20    Lex(LexErrorKind),
21    /// A token appeared where the grammar did not allow it.
22    Unexpected {
23        /// What was found, as source text.
24        found: String,
25        /// The smallest useful set of things that would have been accepted.
26        expected: Vec<&'static str>,
27    },
28    /// The statement ended before the production did.
29    UnexpectedEnd {
30        /// What would have continued it.
31        expected: Vec<&'static str>,
32    },
33    /// A construct the grammar has but this phase does not implement.
34    Unsupported(&'static str),
35    /// A statement the schema refuses, in the reference's own wording.
36    ///
37    /// It is not a syntax error and does not read as one: the statement parsed
38    /// and the schema will not have it, which is what `foreign key mismatch`
39    /// says.
40    Refused(String),
41    /// A hard limit was exceeded.
42    LimitExceeded(&'static str),
43}
44
45/// A parse failure with its location.
46#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct ParseError {
48    /// Why it failed.
49    pub kind: ParseErrorKind,
50    /// Where it failed.
51    pub span: Span,
52}
53
54impl ParseError {
55    /// Returns a failure at a span.
56    pub fn new(kind: ParseErrorKind, span: Span) -> ParseError {
57        ParseError { kind, span }
58    }
59
60    /// Returns the byte offset a caller should point at.
61    pub fn offset(&self) -> u32 {
62        self.span.start
63    }
64
65    /// Returns the one-line message.
66    pub fn message(&self) -> String {
67        match &self.kind {
68            ParseErrorKind::Lex(kind) => kind.message().to_string(),
69            ParseErrorKind::Unexpected { found, expected } => {
70                // **The expected set is not printed.** The reference never
71                // names what it wanted - every syntax failure it reports is
72                // `near "X": syntax error` and nothing more - and a message
73                // that adds `, expected ;` is a message no transcript
74                // comparison can match. The set is still carried, because it is
75                // what `expected()` answers and the parser's own tests read it;
76                // it is only the rendering that stops at the reference's words.
77                let _ = expected;
78                format!(r#"near "{found}": syntax error"#)
79            }
80            ParseErrorKind::UnexpectedEnd { expected } => {
81                if expected.is_empty() {
82                    "incomplete input".to_string()
83                } else {
84                    format!("incomplete input, expected {}", join_expected(expected))
85                }
86            }
87            ParseErrorKind::Unsupported(what) => format!("unsupported: {what}"),
88            ParseErrorKind::Refused(message) => message.clone(),
89            ParseErrorKind::LimitExceeded(what) => format!("{what} exceeded"),
90        }
91    }
92
93    /// Returns the stable result code this failure reports as.
94    ///
95    /// A syntax error is `SQLITE_ERROR`, which is what the pinned release
96    /// returns from `prepare`. A limit is `SQLITE_TOOBIG` where SQLite uses it
97    /// and `SQLITE_ERROR` where SQLite reports the limit as a parse error,
98    /// which is the case for parser depth and compound depth.
99    pub fn code(&self) -> PrimaryCode {
100        match &self.kind {
101            ParseErrorKind::LimitExceeded("string or blob too big") => PrimaryCode::TooBig,
102            _ => PrimaryCode::Error,
103        }
104    }
105}
106
107impl From<LexError> for ParseError {
108    /// Lifts a lexer failure into a parse failure at the same offset.
109    fn from(error: LexError) -> ParseError {
110        ParseError {
111            kind: ParseErrorKind::Lex(error.kind),
112            span: Span::at(error.offset as usize),
113        }
114    }
115}
116
117impl From<ParseError> for DbError {
118    /// Converts a parse failure into the engine's stable error, keeping the
119    /// offset so a caller can point at the character.
120    fn from(error: ParseError) -> DbError {
121        DbError::primary(error.code())
122            .with_message(error.message())
123            .with_sql_offset(error.offset())
124    }
125}
126
127/// Renders an expected-token set the way a diagnostic reads it.
128fn join_expected(expected: &[&'static str]) -> String {
129    match expected {
130        [] => String::new(),
131        [only] => (*only).to_string(),
132        [first, second] => format!("{first} or {second}"),
133        _ => {
134            let head: Vec<&str> = expected
135                .get(..expected.len().saturating_sub(1))
136                .unwrap_or(&[])
137                .to_vec();
138            let tail = expected.last().copied().unwrap_or("");
139            format!("{}, or {tail}", head.join(", "))
140        }
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    /// The offset survives the conversion into the engine's error type, which
149    /// is the whole point of carrying it.
150    #[test]
151    fn the_offset_reaches_the_engine_error() {
152        let error = ParseError::new(
153            ParseErrorKind::Unexpected {
154                found: "FROM".to_string(),
155                expected: vec!["an expression"],
156            },
157            Span::new(7, 11),
158        );
159        let db: DbError = error.into();
160        assert_eq!(db.sql_offset(), Some(7));
161        assert_eq!(db.code(), PrimaryCode::Error);
162    }
163
164    /// The expected set reads as a sentence at one, two, and more entries.
165    #[test]
166    fn the_expected_set_reads_as_a_sentence() {
167        assert_eq!(join_expected(&["a"]), "a");
168        assert_eq!(join_expected(&["a", "b"]), "a or b");
169        assert_eq!(join_expected(&["a", "b", "c"]), "a, b, or c");
170    }
171
172    /// A lexer failure keeps its own offset when it becomes a parse failure.
173    #[test]
174    fn a_lex_failure_keeps_its_offset() {
175        let error: ParseError = LexError {
176            kind: LexErrorKind::UnterminatedQuote,
177            offset: 12,
178        }
179        .into();
180        assert_eq!(error.offset(), 12);
181    }
182}