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}