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
//! Panic-mode recovery: sync on `;` or a statement-start keyword.
//!
//! [`super::Parser::parse`] / [`super::Parser::parse_all`] stay fail-fast
//! (execution must not run a partial script). This path is for IDEs, `analyze`,
//! and the REPL diagnostics surface: emit every recoverable error and keep
//! statements that parsed.
use super::{AstLowerer, MAX_STATEMENTS};
use crate::ast::Stmt;
use crate::error::{QqlError, Span};
use crate::token::TokenKind;
use alloc::vec::Vec;
/// A script parse that keeps going after syntax errors.
#[derive(Debug, Clone)]
pub struct RecoveredScript {
/// Statements that parsed successfully, in source order, with their spans.
pub statements: Vec<(Stmt, Span)>,
/// Every recoverable syntax error, in source order.
pub errors: Vec<QqlError>,
}
impl RecoveredScript {
/// True when the script parsed with no errors (execution-ready).
pub fn is_valid(&self) -> bool {
self.errors.is_empty()
}
}
impl<'a> AstLowerer<'a> {
pub(crate) fn lower_script_recovering(input: &'a str) -> RecoveredScript {
let tokens = match Self::lex(input) {
Ok(tokens) => tokens,
Err(error) => {
return RecoveredScript {
statements: Vec::new(),
errors: alloc::vec![error],
};
}
};
let mut parser = AstLowerer::new(input, tokens);
let mut statements = Vec::new();
let mut errors = Vec::new();
while parser.peek_kind() != TokenKind::Eof {
if statements.len() >= MAX_STATEMENTS {
if let Ok(tok) = parser.peek() {
errors.push(QqlError::parse(
"QQL-PARSE-STATEMENT-LIMIT",
alloc::format!("a script may contain at most {MAX_STATEMENTS} statements"),
tok.span,
));
}
break;
}
while parser.peek_kind() == TokenKind::Semicolon {
if let Ok(tok) = parser.peek() {
errors.push(QqlError::parse(
"QQL-PARSE-EMPTY-STATEMENT",
"leading or empty statements are not allowed",
tok.span,
));
}
let _ = parser.advance();
if parser.peek_kind() == TokenKind::Eof {
return RecoveredScript { statements, errors };
}
}
let Ok(start_tok) = parser.peek() else {
break;
};
if start_tok.kind == TokenKind::Eof {
break;
}
let start_pos = start_tok.span.start;
match parser.parse_stmt() {
Ok(stmt) => match parser.peek() {
Ok(next) if next.kind == TokenKind::Semicolon => {
let end = next.span.end;
let _ = parser.advance();
statements.push((stmt, Span::new(start_pos, end)));
}
Ok(next) if next.kind == TokenKind::Eof => {
let end = parser.prev_span().end;
statements.push((stmt, Span::new(start_pos, end)));
}
Ok(next) if next.kind.is_statement_start() => {
errors.push(QqlError::parse(
"QQL-PARSE-SEPARATOR",
"multiple statements must be separated by a semicolon",
next.span,
));
let end = parser.prev_span().end;
statements.push((stmt, Span::new(start_pos, end)));
}
Ok(next) => {
errors.push(QqlError::parse(
"QQL-PARSE-TRAILING",
alloc::format!("unexpected trailing token '{}'", next.text),
next.span,
));
let end = parser.prev_span().end;
statements.push((stmt, Span::new(start_pos, end)));
parser.synchronize();
}
Err(error) => {
errors.push(error);
parser.synchronize();
}
},
Err(error) => {
errors.push(error);
let stuck_at = parser.index;
parser.synchronize();
// Force progress only when we are not already sitting on
// the next statement. Skipping a recovery-sync keyword
// would drop `COUNT` / `SHOW` after a failed `QUERY`.
if parser.index == stuck_at && !parser.peek_kind().is_recovery_sync() {
let _ = parser.advance();
}
}
}
}
RecoveredScript { statements, errors }
}
fn peek_kind(&mut self) -> TokenKind {
self.peek().map(|t| t.kind).unwrap_or(TokenKind::Eof)
}
/// Skip tokens until `;` (consumed) or a statement-start keyword (left
/// unconsumed). `WITH` is not a boundary — it is also a clause.
fn synchronize(&mut self) {
loop {
let Ok(tok) = self.peek() else {
return;
};
match tok.kind {
TokenKind::Eof => return,
TokenKind::Semicolon => {
let _ = self.advance();
return;
}
kind if kind.is_recovery_sync() => return,
_ => {
let _ = self.advance();
}
}
}
}
}