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}