1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
//! Engine error type.
use std::fmt;
/// What went wrong, by phase.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Kind {
/// A character or token could not be lexed.
Lex,
/// The token stream is not a valid query.
Parse,
/// A construct that is valid Quarb but not yet implemented.
Unsupported,
/// A failure in the macro-expansion phase — the body ran or
/// its output re-parsed and something went wrong there, as
/// distinct from reading the query text itself. The inner
/// error keeps its own banner ([`QuarbError::source`]); this
/// kind names the phase.
Expansion,
/// A query the engine refuses to answer because the result
/// would be silently wrong or incomplete — the alternative to
/// returning a truncated answer (the same doctrine as
/// pushdown's refuse-or-fall-back, but with nothing to fall
/// back to).
Refused,
}
impl Kind {
/// The banner the message prints under.
fn banner(self) -> &'static str {
match self {
Kind::Lex => "lex error",
Kind::Parse => "parse error",
Kind::Unsupported => "not yet supported",
Kind::Expansion => "expansion error",
Kind::Refused => "refused",
}
}
}
/// An error raised while lexing, parsing, or executing a query:
/// its kind, the message, and — when the engine knows them — the
/// way out, where in the query text it was found, and the error
/// inside (an expansion's).
///
/// Displayed as `BANNER: MESSAGE[HINT][: SOURCE][ (at character N)]`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QuarbError {
kind: Kind,
message: String,
hint: Option<String>,
at: Option<usize>,
source: Option<Box<QuarbError>>,
}
impl QuarbError {
fn new(kind: Kind, message: String) -> QuarbError {
QuarbError {
kind,
message,
hint: None,
at: None,
source: None,
}
}
/// A lexer error.
pub fn lex(message: impl Into<String>) -> QuarbError {
QuarbError::new(Kind::Lex, message.into())
}
/// A parse error.
pub fn parse(message: impl Into<String>) -> QuarbError {
QuarbError::new(Kind::Parse, message.into())
}
/// A construct the engine does not implement yet.
pub fn unsupported(message: impl Into<String>) -> QuarbError {
QuarbError::new(Kind::Unsupported, message.into())
}
/// An expansion-phase error; the error inside goes on with
/// [`QuarbError::wrapping`].
pub fn expansion(message: impl Into<String>) -> QuarbError {
QuarbError::new(Kind::Expansion, message.into())
}
/// A refusal.
pub fn refused(message: impl Into<String>) -> QuarbError {
QuarbError::new(Kind::Refused, message.into())
}
/// With the way out, printed right after the message.
pub fn with_hint(mut self, hint: impl Into<String>) -> QuarbError {
self.hint = Some(hint.into());
self
}
/// With the character offset (zero-based) in the query text
/// where the engine stopped.
pub fn at(mut self, offset: usize) -> QuarbError {
self.at = Some(offset);
self
}
/// With the error inside (an expansion's re-parse or run).
pub fn wrapping(mut self, inner: QuarbError) -> QuarbError {
self.source = Some(Box::new(inner));
self
}
/// The phase.
pub fn kind(&self) -> Kind {
self.kind
}
/// The message, without the banner, hint, position or source.
pub fn message(&self) -> &str {
&self.message
}
/// The way out, if the engine named one.
pub fn hint(&self) -> Option<&str> {
self.hint.as_deref()
}
/// The character offset (zero-based) in the query text where
/// the engine stopped, when it knows.
pub fn offset(&self) -> Option<usize> {
self.at
}
/// The error inside, for an expansion.
pub fn inner(&self) -> Option<&QuarbError> {
self.source.as_deref()
}
}
impl fmt::Display for QuarbError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.kind.banner(), self.message)?;
if let Some(hint) = &self.hint {
write!(f, "{hint}")?;
}
if let Some(inner) = &self.source {
write!(f, ": {inner}")?;
}
if let Some(at) = self.at {
write!(f, " (at character {})", at + 1)?;
}
Ok(())
}
}
impl std::error::Error for QuarbError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source.as_deref().map(|e| e as _)
}
}
/// Convenience alias for engine results.
pub type Result<T> = std::result::Result<T, QuarbError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_reproduces_the_banners() {
assert_eq!(QuarbError::parse("x").to_string(), "parse error: x");
assert_eq!(
QuarbError::parse("x").at(4).to_string(),
"parse error: x (at character 5)"
);
assert_eq!(
QuarbError::unsupported("pipeline command 'f'")
.with_hint(" ('f' uses '@|')")
.to_string(),
"not yet supported: pipeline command 'f' ('f' uses '@|')"
);
let e =
QuarbError::expansion("in expansion of '&m' ('/a')").wrapping(QuarbError::lex("bad"));
assert_eq!(
e.to_string(),
"expansion error: in expansion of '&m' ('/a'): lex error: bad"
);
assert_eq!(e.inner().map(|i| i.kind()), Some(Kind::Lex));
assert_eq!(QuarbError::refused("r").to_string(), "refused: r");
}
}