Skip to main content

rucc_parse/
recover.rs

1//! Error recovery: where to resume, and what keeps one error from becoming twenty.
2//!
3//! Design: `spec/06-lexer-and-parser.md` section 6.8.
4//!
5//! The goal is many useful errors from one run without cascades, and the strategy is per
6//! construct rather than global. A statement that will not parse skips to the next `;` or `}`
7//! at the current bracket depth. A declaration skips past its `;`, or past the `}` of the
8//! function body it turned out to be. An expression skips nothing at all and puts an error node
9//! in the tree where the operand should have been, because expressions are short and skipping
10//! one costs the rest of the statement.
11//!
12//! # Why counting errors is not the mechanism
13//!
14//! Every recovery leaves a poisoned node behind, and a diagnostic about a poisoned node is not
15//! reported. That is what actually stops the cascade. A flag saying "something already went
16//! wrong here" is close enough to work on small inputs and wrong on real ones, because it
17//! either suppresses errors in code that was fine or fails to suppress the third message about
18//! the same broken subexpression.
19//!
20//! The sink that holds the diagnostics is [`Errors`], in `rucc-diag`, because
21//! semantic analysis reports through the same one and the limit is a number for the compiler
22//! rather than one per pass. What is here is the half of it that needs a tree: which nodes are
23//! poisoned, and therefore which messages are held back.
24//!
25//! # What is not here yet
26//!
27//! The unclosed brace heuristic. Section 6.8 asks for the opening location plus a guess at the
28//! intended closing point taken from the indentation, which is how a compiler avoids five
29//! hundred errors at end of file. It needs the source map rather than the token stream, so it
30//! lands with the diagnostic rendering rather than here.
31
32use rucc_ast::{Ast, Decl, DeclId, Expr, ExprId, Stmt, StmtId};
33use rucc_diag::{Diagnostic, Errors};
34use rucc_lex::Punct;
35
36use crate::cursor::Cursor;
37
38/// Records a diagnostic unless `about` is a node the parser already poisoned.
39///
40/// A free function rather than a method, because the sink is in `rucc-diag` and what makes a
41/// node poisoned is a fact about this tree. [`Errors::push_unless`] is the half that does not
42/// need to know that.
43pub fn push_about<P: Poison>(errors: &mut Errors, ast: &Ast, about: P, diagnostic: Diagnostic) {
44    errors.push_unless(about.is_poisoned(ast), diagnostic);
45}
46
47/// A node that recovery may have poisoned.
48///
49/// Implemented for the three node kinds that have an error variant, so that a diagnostic can be
50/// held back by the node it is about whatever kind of node that is.
51pub trait Poison: Copy {
52    /// Whether this is a node recovery put in the tree rather than one the source asked for.
53    fn is_poisoned(self, ast: &Ast) -> bool;
54}
55
56impl Poison for ExprId {
57    #[inline]
58    fn is_poisoned(self, ast: &Ast) -> bool {
59        matches!(ast[self], Expr::Error)
60    }
61}
62
63impl Poison for StmtId {
64    #[inline]
65    fn is_poisoned(self, ast: &Ast) -> bool {
66        matches!(ast[self], Stmt::Error)
67    }
68}
69
70impl Poison for DeclId {
71    #[inline]
72    fn is_poisoned(self, ast: &Ast) -> bool {
73        matches!(ast[self], Decl::Error)
74    }
75}
76
77/// Skips to the end of the statement the parser gave up on.
78///
79/// Stops just after the next `;` at the current bracket depth, or just before the `}` that
80/// closes the block, whichever comes first. A `;` inside brackets is skipped over, because the
81/// two in a `for` header do not end anything, and a `}` inside them belongs to a compound
82/// literal or a statement expression and not to the enclosing block.
83pub fn skip_to_statement_end(cursor: &mut Cursor<'_>) {
84    let mut depth = 0u32;
85    while !cursor.is_eof() {
86        match cursor.current().punct() {
87            Some(Punct::Semi) if depth == 0 => {
88                cursor.bump();
89                return;
90            }
91            Some(Punct::RBrace) if depth == 0 => return,
92            Some(Punct::LBrace | Punct::LParen | Punct::LBracket) => depth += 1,
93            Some(Punct::RBrace | Punct::RParen | Punct::RBracket) if depth > 0 => depth -= 1,
94            _ => {}
95        }
96        cursor.bump();
97    }
98}
99
100/// Skips past the declaration the parser gave up on.
101///
102/// Stops just after the `;` that ends it, or just after the `}` that ends the function body it
103/// turned out to be. The second case is what keeps a broken function signature from costing the
104/// next declaration as well: skipping to a `;` alone would run through the whole body and
105/// swallow whatever followed it.
106pub fn skip_past_declaration(cursor: &mut Cursor<'_>) {
107    let mut depth = 0u32;
108    while !cursor.is_eof() {
109        match cursor.current().punct() {
110            Some(Punct::Semi) if depth == 0 => {
111                cursor.bump();
112                return;
113            }
114            Some(Punct::LBrace | Punct::LParen | Punct::LBracket) => depth += 1,
115            Some(Punct::RBrace | Punct::RParen | Punct::RBracket) if depth > 0 => {
116                depth -= 1;
117                if depth == 0 && cursor.at_punct(Punct::RBrace) {
118                    cursor.bump();
119                    // A `}` that ends a body ends the declaration, and a `}` that ends a record
120                    // has a `;` after it that is part of the same declaration. Taking the `;`
121                    // when it is there costs nothing and saves a second error on it.
122                    cursor.eat_punct(Punct::Semi);
123                    return;
124                }
125            }
126            _ => {}
127        }
128        cursor.bump();
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use rucc_diag::Span;
135    use rucc_lex::{Token, TokenFlags, TokenKind};
136
137    use super::*;
138
139    fn stream(puncts: &[Punct]) -> Vec<Token> {
140        let mut tokens: Vec<Token> = puncts
141            .iter()
142            .enumerate()
143            .map(|(i, &punct)| Token {
144                kind: TokenKind::Punct(punct),
145                flags: TokenFlags::EMPTY,
146                value: 0,
147                span: Span::new(i as u32, i as u32 + 1),
148            })
149            .collect();
150        let end = puncts.len() as u32;
151        tokens.push(Token {
152            kind: TokenKind::Eof,
153            flags: TokenFlags::EMPTY,
154            value: 0,
155            span: Span::empty_at(end),
156        });
157        tokens
158    }
159
160    #[test]
161    fn a_statement_resumes_after_its_semicolon() {
162        // ( ; ) ; ,
163        let tokens =
164            stream(&[Punct::LParen, Punct::Semi, Punct::RParen, Punct::Semi, Punct::Comma]);
165        let mut cursor = Cursor::new(&tokens);
166        skip_to_statement_end(&mut cursor);
167        assert!(cursor.at_punct(Punct::Comma));
168    }
169
170    #[test]
171    fn a_statement_stops_at_the_brace_that_closes_its_block() {
172        let tokens = stream(&[Punct::Comma, Punct::RBrace, Punct::Semi]);
173        let mut cursor = Cursor::new(&tokens);
174        skip_to_statement_end(&mut cursor);
175        assert!(cursor.at_punct(Punct::RBrace));
176    }
177
178    #[test]
179    fn a_nested_block_does_not_end_the_statement() {
180        // , { ; } ; ,
181        let tokens = stream(&[
182            Punct::Comma,
183            Punct::LBrace,
184            Punct::Semi,
185            Punct::RBrace,
186            Punct::Semi,
187            Punct::Comma,
188        ]);
189        let mut cursor = Cursor::new(&tokens);
190        skip_to_statement_end(&mut cursor);
191        assert!(cursor.at_punct(Punct::Comma));
192        assert_eq!(cursor.index(), 5);
193    }
194
195    #[test]
196    fn a_declaration_resumes_after_the_body_it_turned_out_to_have() {
197        // ( ) { ; } ,
198        let tokens = stream(&[
199            Punct::LParen,
200            Punct::RParen,
201            Punct::LBrace,
202            Punct::Semi,
203            Punct::RBrace,
204            Punct::Comma,
205        ]);
206        let mut cursor = Cursor::new(&tokens);
207        skip_past_declaration(&mut cursor);
208        assert!(cursor.at_punct(Punct::Comma));
209    }
210
211    #[test]
212    fn a_record_takes_the_semicolon_after_its_brace_with_it() {
213        // { ; } ; ,
214        let tokens =
215            stream(&[Punct::LBrace, Punct::Semi, Punct::RBrace, Punct::Semi, Punct::Comma]);
216        let mut cursor = Cursor::new(&tokens);
217        skip_past_declaration(&mut cursor);
218        assert!(cursor.at_punct(Punct::Comma));
219    }
220
221    #[test]
222    fn a_declaration_resumes_after_its_semicolon() {
223        let tokens = stream(&[Punct::Star, Punct::Semi, Punct::Comma]);
224        let mut cursor = Cursor::new(&tokens);
225        skip_past_declaration(&mut cursor);
226        assert!(cursor.at_punct(Punct::Comma));
227    }
228
229    #[test]
230    fn a_skip_always_reaches_the_end() {
231        // A stray closer at depth zero is skipped rather than counted, so this terminates
232        // instead of underflowing.
233        let tokens = stream(&[Punct::RParen, Punct::RBracket, Punct::Comma]);
234        let mut cursor = Cursor::new(&tokens);
235        skip_to_statement_end(&mut cursor);
236        assert!(cursor.is_eof());
237        let mut cursor = Cursor::new(&tokens);
238        skip_past_declaration(&mut cursor);
239        assert!(cursor.is_eof());
240    }
241
242    #[test]
243    fn a_poisoned_node_holds_back_the_message_about_it() {
244        let mut ast = Ast::new();
245        let bad = ast.expr(Expr::Error, Span::empty_at(0));
246        let good = ast.expr(Expr::Bool(true), Span::new(0, 4));
247        let mut errors = Errors::default();
248        let at = Span::empty_at(0);
249        push_about(&mut errors, &ast, bad, Diagnostic::error("about the broken one", at));
250        assert!(errors.is_empty());
251        push_about(&mut errors, &ast, good, Diagnostic::error("about the good one", at));
252        assert_eq!(errors.len(), 1);
253    }
254}