Coverage Report

Created: 2025-09-05 15:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/ruchy/src/parser/error_recovery.rs
Line
Count
Source
1
//! Deterministic Error Recovery for Ruchy Parser
2
//!
3
//! Based on docs/ruchy-transpiler-docs.md Section 4: Deterministic Error Recovery
4
//! Ensures predictable parser behavior on malformed input
5
6
use crate::frontend::ast::{Expr, ExprKind, Literal, Param, Span};
7
8
/// Synthetic error node that can be embedded in the AST
9
#[derive(Debug, Clone, PartialEq)]
10
pub struct ErrorNode {
11
    /// The error message
12
    pub message: String,
13
    /// Source location of the error
14
    pub location: SourceLocation,
15
    /// The partial context that was successfully parsed
16
    pub context: ErrorContext,
17
    /// Recovery strategy used
18
    pub recovery: RecoveryStrategy,
19
}
20
21
#[derive(Debug, Clone, PartialEq)]
22
pub struct SourceLocation {
23
    pub line: usize,
24
    pub column: usize,
25
    pub file: Option<String>,
26
}
27
28
#[derive(Debug, Clone, PartialEq)]
29
pub enum ErrorContext {
30
    FunctionDecl {
31
        name: Option<String>,
32
        params: Option<Vec<Param>>,
33
        body: Option<Box<Expr>>,
34
    },
35
    LetBinding {
36
        name: Option<String>,
37
        value: Option<Box<Expr>>,
38
    },
39
    IfExpression {
40
        condition: Option<Box<Expr>>,
41
        then_branch: Option<Box<Expr>>,
42
        else_branch: Option<Box<Expr>>,
43
    },
44
    ArrayLiteral {
45
        elements: Vec<Expr>,
46
        error_at_index: usize,
47
    },
48
    BinaryOp {
49
        left: Option<Box<Expr>>,
50
        op: Option<String>,
51
        right: Option<Box<Expr>>,
52
    },
53
    StructLiteral {
54
        name: Option<String>,
55
        fields: Vec<(String, Expr)>,
56
        error_field: Option<String>,
57
    },
58
}
59
60
#[derive(Debug, Clone, PartialEq)]
61
pub enum RecoveryStrategy {
62
    /// Skip tokens until a synchronization point
63
    SkipUntilSync,
64
    /// Insert a synthetic token
65
    InsertToken(String),
66
    /// Replace with a default value
67
    DefaultValue,
68
    /// Wrap partial parse in error node
69
    PartialParse,
70
    /// Panic mode - skip until statement boundary
71
    PanicMode,
72
}
73
74
/// Extension to Expr to support error nodes
75
#[derive(Debug, Clone, PartialEq)]
76
pub enum ExprWithError {
77
    Valid(Expr),
78
    Error(ErrorNode),
79
}
80
81
impl From<Expr> for ExprWithError {
82
0
    fn from(expr: Expr) -> Self {
83
0
        ExprWithError::Valid(expr)
84
0
    }
85
}
86
87
impl From<ErrorNode> for ExprWithError {
88
0
    fn from(error: ErrorNode) -> Self {
89
0
        ExprWithError::Error(error)
90
0
    }
91
}
92
93
/// Parser error recovery implementation
94
pub struct ErrorRecovery {
95
    /// Synchronization tokens for panic mode recovery
96
    sync_tokens: Vec<String>,
97
    /// Maximum errors before giving up
98
    max_errors: usize,
99
    /// Current error count
100
    error_count: usize,
101
}
102
103
impl Default for ErrorRecovery {
104
447
    fn default() -> Self {
105
447
        Self {
106
447
            sync_tokens: vec![
107
447
                ";".to_string(),
108
447
                "}".to_string(),
109
447
                "fun".to_string(),
110
447
                "let".to_string(),
111
447
                "if".to_string(),
112
447
                "for".to_string(),
113
447
                "while".to_string(),
114
447
                "return".to_string(),
115
447
                "struct".to_string(),
116
447
                "enum".to_string(),
117
447
            ],
118
447
            max_errors: 100,
119
447
            error_count: 0,
120
447
        }
121
447
    }
122
}
123
124
impl ErrorRecovery {
125
    #[must_use]
126
447
    pub fn new() -> Self {
127
447
        Self::default()
128
447
    }
129
130
    /// Create a synthetic error node for missing function name
131
4
    pub fn missing_function_name(&mut self, location: SourceLocation) -> ErrorNode {
132
4
        self.error_count += 1;
133
4
        ErrorNode {
134
4
            message: "expected function name".to_string(),
135
4
            location,
136
4
            context: ErrorContext::FunctionDecl {
137
4
                name: None,
138
4
                params: None,
139
4
                body: None,
140
4
            },
141
4
            recovery: RecoveryStrategy::InsertToken("error_fn".to_string()),
142
4
        }
143
4
    }
144
145
    /// Create a synthetic error node for missing function parameters
146
0
    pub fn missing_function_params(&mut self, name: String, location: SourceLocation) -> ErrorNode {
147
0
        self.error_count += 1;
148
0
        ErrorNode {
149
0
            message: "expected function parameters".to_string(),
150
0
            location,
151
0
            context: ErrorContext::FunctionDecl {
152
0
                name: Some(name),
153
0
                params: None,
154
0
                body: None,
155
0
            },
156
0
            recovery: RecoveryStrategy::DefaultValue,
157
0
        }
158
0
    }
159
160
    /// Create a synthetic error node for missing function body
161
0
    pub fn missing_function_body(
162
0
        &mut self,
163
0
        name: String,
164
0
        params: Vec<Param>,
165
0
        location: SourceLocation,
166
0
    ) -> ErrorNode {
167
0
        self.error_count += 1;
168
0
        ErrorNode {
169
0
            message: "expected function body".to_string(),
170
0
            location,
171
0
            context: ErrorContext::FunctionDecl {
172
0
                name: Some(name),
173
0
                params: Some(params),
174
0
                body: None,
175
0
            },
176
0
            recovery: RecoveryStrategy::InsertToken("{ /* missing body */ }".to_string()),
177
0
        }
178
0
    }
179
180
    /// Create error node for malformed let binding
181
0
    pub fn malformed_let_binding(
182
0
        &mut self,
183
0
        partial_name: Option<String>,
184
0
        partial_value: Option<Box<Expr>>,
185
0
        location: SourceLocation,
186
0
    ) -> ErrorNode {
187
0
        self.error_count += 1;
188
0
        ErrorNode {
189
0
            message: "malformed let binding".to_string(),
190
0
            location,
191
0
            context: ErrorContext::LetBinding {
192
0
                name: partial_name,
193
0
                value: partial_value,
194
0
            },
195
0
            recovery: RecoveryStrategy::PartialParse,
196
0
        }
197
0
    }
198
199
    /// Create error node for incomplete if expression
200
0
    pub fn incomplete_if_expr(
201
0
        &mut self,
202
0
        condition: Option<Box<Expr>>,
203
0
        then_branch: Option<Box<Expr>>,
204
0
        location: SourceLocation,
205
0
    ) -> ErrorNode {
206
0
        self.error_count += 1;
207
0
        ErrorNode {
208
0
            message: "incomplete if expression".to_string(),
209
0
            location,
210
0
            context: ErrorContext::IfExpression {
211
0
                condition,
212
0
                then_branch,
213
0
                else_branch: None,
214
0
            },
215
0
            recovery: RecoveryStrategy::DefaultValue,
216
0
        }
217
0
    }
218
219
    /// Check if we should continue parsing or give up
220
    #[must_use]
221
7
    pub fn should_continue(&self) -> bool {
222
7
        self.error_count < self.max_errors
223
7
    }
224
225
    /// Reset error count for new parsing session
226
0
    pub fn reset(&mut self) {
227
0
        self.error_count = 0;
228
0
    }
229
230
    /// Check if token is a synchronization point
231
    #[must_use]
232
5
    pub fn is_sync_token(&self, token: &str) -> bool {
233
5
        self.sync_tokens.contains(&token.to_string())
234
5
    }
235
236
    /// Skip tokens until we find a synchronization point
237
0
    pub fn skip_until_sync<'a, I>(&self, tokens: &mut I) -> Option<String>
238
0
    where
239
0
        I: Iterator<Item = &'a str>,
240
    {
241
0
        for token in tokens {
242
0
            if self.is_sync_token(token) {
243
0
                return Some(token.to_string());
244
0
            }
245
        }
246
0
        None
247
0
    }
248
}
249
250
/// Error recovery rules for different contexts
251
pub struct RecoveryRules;
252
253
impl RecoveryRules {
254
    /// Determine recovery strategy based on context
255
    #[must_use]
256
1
    pub fn select_strategy(context: &ErrorContext) -> RecoveryStrategy {
257
1
        match context {
258
1
            ErrorContext::FunctionDecl { name, params, body } => {
259
1
                if name.is_none() {
260
1
                    RecoveryStrategy::InsertToken("error_fn".to_string())
261
0
                } else if params.is_none() {
262
0
                    RecoveryStrategy::DefaultValue
263
0
                } else if body.is_none() {
264
0
                    RecoveryStrategy::InsertToken("{ }".to_string())
265
                } else {
266
0
                    RecoveryStrategy::PartialParse
267
                }
268
            }
269
0
            ErrorContext::LetBinding { .. } => RecoveryStrategy::SkipUntilSync,
270
0
            ErrorContext::IfExpression { .. } => RecoveryStrategy::DefaultValue,
271
            ErrorContext::ArrayLiteral { .. } | ErrorContext::StructLiteral { .. } => {
272
0
                RecoveryStrategy::PartialParse
273
            }
274
0
            ErrorContext::BinaryOp { .. } => RecoveryStrategy::PanicMode,
275
        }
276
1
    }
277
278
    /// Generate synthetic AST for error recovery
279
    #[must_use]
280
1
    pub fn synthesize_ast(error: &ErrorNode) -> Expr {
281
1
        let default_span = Span::new(0, 0);
282
1
        match &error.context {
283
            ErrorContext::FunctionDecl { .. } => {
284
                // Return a synthetic function that does nothing
285
0
                Expr::new(
286
0
                    ExprKind::Lambda {
287
0
                        params: vec![],
288
0
                        body: Box::new(Expr::new(ExprKind::Literal(Literal::Unit), default_span)),
289
0
                    },
290
0
                    default_span,
291
                )
292
            }
293
1
            ErrorContext::LetBinding { name, value } => {
294
                // Create a let with whatever we could parse
295
1
                Expr::new(
296
                    ExprKind::Let {
297
1
                        name: name.clone().unwrap_or_else(|| 
"_error"0
.
to_string0
()),
298
1
                        type_annotation: None,
299
1
                        value: value.clone().unwrap_or_else(|| {
300
1
                            Box::new(Expr::new(ExprKind::Literal(Literal::Unit), default_span))
301
1
                        }),
302
1
                        body: Box::new(Expr::new(ExprKind::Literal(Literal::Unit), default_span)),
303
                        is_mutable: false,
304
                    },
305
1
                    default_span,
306
                )
307
            }
308
            ErrorContext::IfExpression {
309
0
                condition,
310
0
                then_branch,
311
                ..
312
            } => {
313
                // Create an if with defaults for missing parts
314
0
                Expr::new(
315
                    ExprKind::If {
316
0
                        condition: condition.clone().unwrap_or_else(|| {
317
0
                            Box::new(Expr::new(
318
0
                                ExprKind::Literal(Literal::Bool(false)),
319
0
                                default_span,
320
                            ))
321
0
                        }),
322
0
                        then_branch: then_branch.clone().unwrap_or_else(|| {
323
0
                            Box::new(Expr::new(ExprKind::Literal(Literal::Unit), default_span))
324
0
                        }),
325
0
                        else_branch: Some(Box::new(Expr::new(
326
0
                            ExprKind::Literal(Literal::Unit),
327
0
                            default_span,
328
0
                        ))),
329
                    },
330
0
                    default_span,
331
                )
332
            }
333
0
            ErrorContext::ArrayLiteral { elements, .. } => {
334
                // Return partial array with valid elements
335
0
                Expr::new(ExprKind::List(elements.clone()), default_span)
336
            }
337
0
            ErrorContext::BinaryOp { left, .. } => {
338
                // Return left side if available, otherwise unit
339
0
                if let Some(left) = left {
340
0
                    *left.clone()
341
                } else {
342
0
                    Expr::new(ExprKind::Literal(Literal::Unit), default_span)
343
                }
344
            }
345
0
            ErrorContext::StructLiteral { name, fields, .. } => {
346
                // Return struct with partial fields
347
0
                if let Some(name) = name {
348
0
                    Expr::new(
349
0
                        ExprKind::StructLiteral {
350
0
                            name: name.clone(),
351
0
                            fields: fields.clone(),
352
0
                        },
353
0
                        default_span,
354
                    )
355
                } else {
356
0
                    Expr::new(ExprKind::Literal(Literal::Unit), default_span)
357
                }
358
            }
359
        }
360
1
    }
361
}
362
363
/// Integration with parser
364
pub trait ErrorRecoverable {
365
    /// Try to recover from parse error
366
    fn recover_from_error(&mut self, error: ErrorNode) -> Option<Expr>;
367
368
    /// Check if we're in a recoverable state
369
    fn can_recover(&self) -> bool;
370
371
    /// Get current error nodes
372
    fn get_errors(&self) -> Vec<ErrorNode>;
373
}
374
375
#[cfg(test)]
376
#[allow(clippy::unwrap_used, clippy::panic)]
377
mod tests {
378
    use super::*;
379
380
    #[test]
381
1
    fn test_error_recovery_creation() {
382
1
        let mut recovery = ErrorRecovery::new();
383
384
1
        let error = recovery.missing_function_name(SourceLocation {
385
1
            line: 1,
386
1
            column: 5,
387
1
            file: None,
388
1
        });
389
390
1
        assert_eq!(error.message, "expected function name");
391
1
        assert_eq!(recovery.error_count, 1);
392
1
        assert!(recovery.should_continue());
393
1
    }
394
395
    #[test]
396
1
    fn test_recovery_strategy_selection() {
397
1
        let context = ErrorContext::FunctionDecl {
398
1
            name: None,
399
1
            params: None,
400
1
            body: None,
401
1
        };
402
403
1
        let strategy = RecoveryRules::select_strategy(&context);
404
405
1
        match strategy {
406
1
            RecoveryStrategy::InsertToken(token) => {
407
1
                assert_eq!(token, "error_fn");
408
            }
409
0
            _ => panic!("Expected InsertToken strategy"),
410
        }
411
1
    }
412
413
    #[test]
414
1
    fn test_synthetic_ast_generation() {
415
1
        let error = ErrorNode {
416
1
            message: "test error".to_string(),
417
1
            location: SourceLocation {
418
1
                line: 1,
419
1
                column: 1,
420
1
                file: None,
421
1
            },
422
1
            context: ErrorContext::LetBinding {
423
1
                name: Some("x".to_string()),
424
1
                value: None,
425
1
            },
426
1
            recovery: RecoveryStrategy::DefaultValue,
427
1
        };
428
429
1
        let ast = RecoveryRules::synthesize_ast(&error);
430
431
1
        match ast.kind {
432
1
            ExprKind::Let { name, type_annotation: _, value, .. } => {
433
1
                assert_eq!(name, "x");
434
1
                match value.kind {
435
1
                    ExprKind::Literal(Literal::Unit) => {}
436
0
                    _ => panic!("Expected Unit value"),
437
                }
438
            }
439
0
            _ => panic!("Expected Let expression"),
440
        }
441
1
    }
442
443
    #[test]
444
1
    fn test_sync_token_detection() {
445
1
        let recovery = ErrorRecovery::new();
446
447
1
        assert!(recovery.is_sync_token(";"));
448
1
        assert!(recovery.is_sync_token("fun"));
449
1
        assert!(recovery.is_sync_token("let"));
450
1
        assert!(!recovery.is_sync_token("="));
451
1
        assert!(!recovery.is_sync_token("+"));
452
1
    }
453
454
    #[test]
455
1
    fn test_max_errors_limit() {
456
1
        let mut recovery = ErrorRecovery::new();
457
1
        recovery.max_errors = 3;
458
459
6
        for 
i5
in 0..5 {
460
5
            if recovery.should_continue() {
461
3
                recovery.missing_function_name(SourceLocation {
462
3
                    line: i,
463
3
                    column: 0,
464
3
                    file: None,
465
3
                });
466
3
            
}2
467
        }
468
469
1
        assert_eq!(recovery.error_count, 3);
470
1
        assert!(!recovery.should_continue());
471
1
    }
472
}