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    /// SQLite's own sentence for a statement that SQLite refuses in this form but
42    /// accepts in another one this build cannot run.
43    ///
44    /// **The words are SQLite's and the status is `unsupported`.** A write to a view is
45    /// `cannot modify v because it is a view` when the view has no `INSTEAD OF`
46    /// trigger, and runs the trigger when it has one. A view that was reopened has lost
47    /// its triggers in this build, and the writes the triggers would have run are ones
48    /// the matrix in `tests/matrix` still marks as not built, so the sentence is
49    /// SQLite's and the caller is still told the construct is not built.
50    RefusedNotBuilt {
51        /// What the message says.
52        said: String,
53        /// The construct, for the `unsupported` status.
54        feature: &'static str,
55    },
56    /// A hard limit was exceeded.
57    LimitExceeded(&'static str),
58}
59
60/// A parse failure with its location.
61#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct ParseError {
63    /// Why it failed.
64    pub kind: ParseErrorKind,
65    /// Where it failed.
66    pub span: Span,
67}
68
69impl ParseError {
70    /// Returns a failure at a span.
71    pub fn new(kind: ParseErrorKind, span: Span) -> ParseError {
72        ParseError { kind, span }
73    }
74
75    /// Returns the byte offset a caller should point at.
76    pub fn offset(&self) -> u32 {
77        self.span.start
78    }
79
80    /// Returns the one-line message.
81    pub fn message(&self) -> String {
82        match &self.kind {
83            ParseErrorKind::Lex(kind) => kind.message().to_string(),
84            ParseErrorKind::Unexpected { found, expected } => {
85                // **The expected set is not printed.** The reference never
86                // names what it wanted - every syntax failure it reports is
87                // `near "X": syntax error` and nothing more - and a message
88                // that adds `, expected ;` is a message no transcript
89                // comparison can match. The set is still carried, because it is
90                // what `expected()` answers and the parser's own tests read it;
91                // it is only the rendering that stops at the reference's words.
92                let _ = expected;
93                format!(r#"near "{found}": syntax error"#)
94            }
95            ParseErrorKind::UnexpectedEnd { expected } => {
96                if expected.is_empty() {
97                    "incomplete input".to_string()
98                } else {
99                    format!("incomplete input, expected {}", join_expected(expected))
100                }
101            }
102            ParseErrorKind::Unsupported(what) => format!("unsupported: {what}"),
103            ParseErrorKind::Refused(message) => message.clone(),
104            ParseErrorKind::RefusedNotBuilt { said, .. } => said.clone(),
105            // SQLite words these limits as the limit itself, with no `exceeded`:
106            // `too many terms in compound SELECT`, `too many columns in result set`
107            // and `string or blob too big`. The others keep their wording.
108            ParseErrorKind::LimitExceeded(
109                what @ ("too many terms in compound SELECT"
110                | "too many columns in result set"
111                | "string or blob too big"),
112            ) => (*what).to_string(),
113            ParseErrorKind::LimitExceeded(what) => format!("{what} exceeded"),
114        }
115    }
116
117    /// Returns the stable result code this failure reports as.
118    ///
119    /// A syntax error is `SQLITE_ERROR`, which is what the pinned release
120    /// returns from `prepare`. A limit is `SQLITE_TOOBIG` where SQLite uses it
121    /// and `SQLITE_ERROR` where SQLite reports the limit as a parse error,
122    /// which is the case for parser depth and compound depth.
123    pub fn code(&self) -> PrimaryCode {
124        match &self.kind {
125            ParseErrorKind::LimitExceeded("string or blob too big") => PrimaryCode::TooBig,
126            _ => PrimaryCode::Error,
127        }
128    }
129}
130
131impl From<LexError> for ParseError {
132    /// Lifts a lexer failure into a parse failure at the same offset.
133    fn from(error: LexError) -> ParseError {
134        ParseError {
135            kind: ParseErrorKind::Lex(error.kind),
136            span: Span::at(error.offset as usize),
137        }
138    }
139}
140
141/// Turns a lexing failure into the parse failure SQLite words it as.
142///
143/// SQLite says `unrecognized token: "<the bytes it consumed>"`, and an application
144/// that matches on that message needs the token in it. The lexer does not hold the
145/// SQL text in its error, so the caller that has the text supplies it here.
146///
147/// @param source - the SQL text that was being lexed
148/// @param error - what the lexer reported
149pub fn lex_failure(source: &[u8], error: LexError) -> ParseError {
150    if error.kind == LexErrorKind::UnterminatedComment {
151        return ParseError::from(error);
152    }
153    let text = crate::lexer::illegal_token_text(source, error);
154    ParseError::new(
155        ParseErrorKind::Refused(format!(
156            "unrecognized token: \"{}\"",
157            String::from_utf8_lossy(text)
158        )),
159        Span::at(error.offset as usize),
160    )
161}
162
163impl From<ParseError> for DbError {
164    /// Converts a parse failure into the engine's stable error, keeping the
165    /// offset so a caller can point at the character.
166    fn from(error: ParseError) -> DbError {
167        DbError::primary(error.code())
168            .with_message(error.message())
169            .with_sql_offset(error.offset())
170    }
171}
172
173/// Renders an expected-token set the way a diagnostic reads it.
174fn join_expected(expected: &[&'static str]) -> String {
175    match expected {
176        [] => String::new(),
177        [only] => (*only).to_string(),
178        [first, second] => format!("{first} or {second}"),
179        _ => {
180            let head: Vec<&str> = expected
181                .get(..expected.len().saturating_sub(1))
182                .unwrap_or(&[])
183                .to_vec();
184            let tail = expected.last().copied().unwrap_or("");
185            format!("{}, or {tail}", head.join(", "))
186        }
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    /// The offset survives the conversion into the engine's error type, which
195    /// is the whole point of carrying it.
196    #[test]
197    fn the_offset_reaches_the_engine_error() {
198        let error = ParseError::new(
199            ParseErrorKind::Unexpected {
200                found: "FROM".to_string(),
201                expected: vec!["an expression"],
202            },
203            Span::new(7, 11),
204        );
205        let db: DbError = error.into();
206        assert_eq!(db.sql_offset(), Some(7));
207        assert_eq!(db.code(), PrimaryCode::Error);
208    }
209
210    /// The expected set reads as a sentence at one, two, and more entries.
211    #[test]
212    fn the_expected_set_reads_as_a_sentence() {
213        assert_eq!(join_expected(&["a"]), "a");
214        assert_eq!(join_expected(&["a", "b"]), "a or b");
215        assert_eq!(join_expected(&["a", "b", "c"]), "a, b, or c");
216    }
217
218    /// A lexer failure keeps its own offset when it becomes a parse failure.
219    #[test]
220    fn a_lex_failure_keeps_its_offset() {
221        let error: ParseError = LexError {
222            kind: LexErrorKind::UnterminatedQuote,
223            offset: 12,
224        }
225        .into();
226        assert_eq!(error.offset(), 12);
227    }
228}