Skip to main content

qql_core/parser/
recover.rs

1//! Panic-mode recovery: sync on `;` or a statement-start keyword.
2//!
3//! [`super::Parser::parse`] / [`super::Parser::parse_all`] stay fail-fast
4//! (execution must not run a partial script). This path is for IDEs, `analyze`,
5//! and the REPL diagnostics surface: emit every recoverable error and keep
6//! statements that parsed.
7
8use super::{AstLowerer, MAX_STATEMENTS};
9use crate::ast::Stmt;
10use crate::error::{QqlError, Span};
11use crate::token::TokenKind;
12use alloc::vec::Vec;
13
14/// A script parse that keeps going after syntax errors.
15#[derive(Debug, Clone)]
16pub struct RecoveredScript {
17    /// Statements that parsed successfully, in source order, with their spans.
18    pub statements: Vec<(Stmt, Span)>,
19    /// Every recoverable syntax error, in source order.
20    pub errors: Vec<QqlError>,
21}
22
23impl RecoveredScript {
24    /// True when the script parsed with no errors (execution-ready).
25    pub fn is_valid(&self) -> bool {
26        self.errors.is_empty()
27    }
28}
29
30impl<'a> AstLowerer<'a> {
31    pub(crate) fn lower_script_recovering(input: &'a str) -> RecoveredScript {
32        let tokens = match Self::lex(input) {
33            Ok(tokens) => tokens,
34            Err(error) => {
35                return RecoveredScript {
36                    statements: Vec::new(),
37                    errors: alloc::vec![error],
38                };
39            }
40        };
41        let mut parser = AstLowerer::new(input, tokens);
42        let mut statements = Vec::new();
43        let mut errors = Vec::new();
44
45        while parser.peek_kind() != TokenKind::Eof {
46            if statements.len() >= MAX_STATEMENTS {
47                if let Ok(tok) = parser.peek() {
48                    errors.push(QqlError::parse(
49                        "QQL-PARSE-STATEMENT-LIMIT",
50                        alloc::format!("a script may contain at most {MAX_STATEMENTS} statements"),
51                        tok.span,
52                    ));
53                }
54                break;
55            }
56
57            while parser.peek_kind() == TokenKind::Semicolon {
58                if let Ok(tok) = parser.peek() {
59                    errors.push(QqlError::parse(
60                        "QQL-PARSE-EMPTY-STATEMENT",
61                        "leading or empty statements are not allowed",
62                        tok.span,
63                    ));
64                }
65                let _ = parser.advance();
66                if parser.peek_kind() == TokenKind::Eof {
67                    return RecoveredScript { statements, errors };
68                }
69            }
70
71            let Ok(start_tok) = parser.peek() else {
72                break;
73            };
74            if start_tok.kind == TokenKind::Eof {
75                break;
76            }
77            let start_pos = start_tok.span.start;
78            match parser.parse_stmt() {
79                Ok(stmt) => match parser.peek() {
80                    Ok(next) if next.kind == TokenKind::Semicolon => {
81                        let end = next.span.end;
82                        let _ = parser.advance();
83                        statements.push((stmt, Span::new(start_pos, end)));
84                    }
85                    Ok(next) if next.kind == TokenKind::Eof => {
86                        let end = parser.prev_span().end;
87                        statements.push((stmt, Span::new(start_pos, end)));
88                    }
89                    Ok(next) if next.kind.is_statement_start() => {
90                        errors.push(QqlError::parse(
91                            "QQL-PARSE-SEPARATOR",
92                            "multiple statements must be separated by a semicolon",
93                            next.span,
94                        ));
95                        let end = parser.prev_span().end;
96                        statements.push((stmt, Span::new(start_pos, end)));
97                    }
98                    Ok(next) => {
99                        errors.push(QqlError::parse(
100                            "QQL-PARSE-TRAILING",
101                            alloc::format!("unexpected trailing token '{}'", next.text),
102                            next.span,
103                        ));
104                        let end = parser.prev_span().end;
105                        statements.push((stmt, Span::new(start_pos, end)));
106                        parser.synchronize();
107                    }
108                    Err(error) => {
109                        errors.push(error);
110                        parser.synchronize();
111                    }
112                },
113                Err(error) => {
114                    errors.push(error);
115                    let stuck_at = parser.index;
116                    parser.synchronize();
117                    // Force progress only when we are not already sitting on
118                    // the next statement. Skipping a recovery-sync keyword
119                    // would drop `COUNT` / `SHOW` after a failed `QUERY`.
120                    if parser.index == stuck_at && !parser.peek_kind().is_recovery_sync() {
121                        let _ = parser.advance();
122                    }
123                }
124            }
125        }
126
127        RecoveredScript { statements, errors }
128    }
129
130    fn peek_kind(&mut self) -> TokenKind {
131        self.peek().map(|t| t.kind).unwrap_or(TokenKind::Eof)
132    }
133
134    /// Skip tokens until `;` (consumed) or a statement-start keyword (left
135    /// unconsumed). `WITH` is not a boundary — it is also a clause.
136    fn synchronize(&mut self) {
137        loop {
138            let Ok(tok) = self.peek() else {
139                return;
140            };
141            match tok.kind {
142                TokenKind::Eof => return,
143                TokenKind::Semicolon => {
144                    let _ = self.advance();
145                    return;
146                }
147                kind if kind.is_recovery_sync() => return,
148                _ => {
149                    let _ = self.advance();
150                }
151            }
152        }
153    }
154}