ruchy/parser/
error_recovery.rs

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
6use 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)]
10pub 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)]
22pub struct SourceLocation {
23    pub line: usize,
24    pub column: usize,
25    pub file: Option<String>,
26}
27
28#[derive(Debug, Clone, PartialEq)]
29pub 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)]
61pub 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)]
76pub enum ExprWithError {
77    Valid(Expr),
78    Error(ErrorNode),
79}
80
81impl From<Expr> for ExprWithError {
82    fn from(expr: Expr) -> Self {
83        ExprWithError::Valid(expr)
84    }
85}
86
87impl From<ErrorNode> for ExprWithError {
88    fn from(error: ErrorNode) -> Self {
89        ExprWithError::Error(error)
90    }
91}
92
93/// Parser error recovery implementation
94pub 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
103impl Default for ErrorRecovery {
104    fn default() -> Self {
105        Self {
106            sync_tokens: vec![
107                ";".to_string(),
108                "}".to_string(),
109                "fun".to_string(),
110                "let".to_string(),
111                "if".to_string(),
112                "for".to_string(),
113                "while".to_string(),
114                "return".to_string(),
115                "struct".to_string(),
116                "enum".to_string(),
117            ],
118            max_errors: 100,
119            error_count: 0,
120        }
121    }
122}
123
124impl ErrorRecovery {
125    #[must_use]
126    pub fn new() -> Self {
127        Self::default()
128    }
129
130    /// Create a synthetic error node for missing function name
131    pub fn missing_function_name(&mut self, location: SourceLocation) -> ErrorNode {
132        self.error_count += 1;
133        ErrorNode {
134            message: "expected function name".to_string(),
135            location,
136            context: ErrorContext::FunctionDecl {
137                name: None,
138                params: None,
139                body: None,
140            },
141            recovery: RecoveryStrategy::InsertToken("error_fn".to_string()),
142        }
143    }
144
145    /// Create a synthetic error node for missing function parameters
146    pub fn missing_function_params(&mut self, name: String, location: SourceLocation) -> ErrorNode {
147        self.error_count += 1;
148        ErrorNode {
149            message: "expected function parameters".to_string(),
150            location,
151            context: ErrorContext::FunctionDecl {
152                name: Some(name),
153                params: None,
154                body: None,
155            },
156            recovery: RecoveryStrategy::DefaultValue,
157        }
158    }
159
160    /// Create a synthetic error node for missing function body
161    pub fn missing_function_body(
162        &mut self,
163        name: String,
164        params: Vec<Param>,
165        location: SourceLocation,
166    ) -> ErrorNode {
167        self.error_count += 1;
168        ErrorNode {
169            message: "expected function body".to_string(),
170            location,
171            context: ErrorContext::FunctionDecl {
172                name: Some(name),
173                params: Some(params),
174                body: None,
175            },
176            recovery: RecoveryStrategy::InsertToken("{ /* missing body */ }".to_string()),
177        }
178    }
179
180    /// Create error node for malformed let binding
181    pub fn malformed_let_binding(
182        &mut self,
183        partial_name: Option<String>,
184        partial_value: Option<Box<Expr>>,
185        location: SourceLocation,
186    ) -> ErrorNode {
187        self.error_count += 1;
188        ErrorNode {
189            message: "malformed let binding".to_string(),
190            location,
191            context: ErrorContext::LetBinding {
192                name: partial_name,
193                value: partial_value,
194            },
195            recovery: RecoveryStrategy::PartialParse,
196        }
197    }
198
199    /// Create error node for incomplete if expression
200    pub fn incomplete_if_expr(
201        &mut self,
202        condition: Option<Box<Expr>>,
203        then_branch: Option<Box<Expr>>,
204        location: SourceLocation,
205    ) -> ErrorNode {
206        self.error_count += 1;
207        ErrorNode {
208            message: "incomplete if expression".to_string(),
209            location,
210            context: ErrorContext::IfExpression {
211                condition,
212                then_branch,
213                else_branch: None,
214            },
215            recovery: RecoveryStrategy::DefaultValue,
216        }
217    }
218
219    /// Check if we should continue parsing or give up
220    #[must_use]
221    pub fn should_continue(&self) -> bool {
222        self.error_count < self.max_errors
223    }
224
225    /// Reset error count for new parsing session
226    pub fn reset(&mut self) {
227        self.error_count = 0;
228    }
229
230    /// Check if token is a synchronization point
231    #[must_use]
232    pub fn is_sync_token(&self, token: &str) -> bool {
233        self.sync_tokens.contains(&token.to_string())
234    }
235
236    /// Skip tokens until we find a synchronization point
237    pub fn skip_until_sync<'a, I>(&self, tokens: &mut I) -> Option<String>
238    where
239        I: Iterator<Item = &'a str>,
240    {
241        for token in tokens {
242            if self.is_sync_token(token) {
243                return Some(token.to_string());
244            }
245        }
246        None
247    }
248}
249
250/// Error recovery rules for different contexts
251pub struct RecoveryRules;
252
253impl RecoveryRules {
254    /// Determine recovery strategy based on context
255    #[must_use]
256    pub fn select_strategy(context: &ErrorContext) -> RecoveryStrategy {
257        match context {
258            ErrorContext::FunctionDecl { name, params, body } => {
259                if name.is_none() {
260                    RecoveryStrategy::InsertToken("error_fn".to_string())
261                } else if params.is_none() {
262                    RecoveryStrategy::DefaultValue
263                } else if body.is_none() {
264                    RecoveryStrategy::InsertToken("{ }".to_string())
265                } else {
266                    RecoveryStrategy::PartialParse
267                }
268            }
269            ErrorContext::LetBinding { .. } => RecoveryStrategy::SkipUntilSync,
270            ErrorContext::IfExpression { .. } => RecoveryStrategy::DefaultValue,
271            ErrorContext::ArrayLiteral { .. } | ErrorContext::StructLiteral { .. } => {
272                RecoveryStrategy::PartialParse
273            }
274            ErrorContext::BinaryOp { .. } => RecoveryStrategy::PanicMode,
275        }
276    }
277
278    /// Generate synthetic AST for error recovery
279    #[must_use]
280    pub fn synthesize_ast(error: &ErrorNode) -> Expr {
281        let default_span = Span::new(0, 0);
282        match &error.context {
283            ErrorContext::FunctionDecl { .. } => {
284                // Return a synthetic function that does nothing
285                Expr::new(
286                    ExprKind::Lambda {
287                        params: vec![],
288                        body: Box::new(Expr::new(ExprKind::Literal(Literal::Unit), default_span)),
289                    },
290                    default_span,
291                )
292            }
293            ErrorContext::LetBinding { name, value } => {
294                // Create a let with whatever we could parse
295                Expr::new(
296                    ExprKind::Let {
297                        name: name.clone().unwrap_or_else(|| "_error".to_string()),
298                        type_annotation: None,
299                        value: value.clone().unwrap_or_else(|| {
300                            Box::new(Expr::new(ExprKind::Literal(Literal::Unit), default_span))
301                        }),
302                        body: Box::new(Expr::new(ExprKind::Literal(Literal::Unit), default_span)),
303                        is_mutable: false,
304                    },
305                    default_span,
306                )
307            }
308            ErrorContext::IfExpression {
309                condition,
310                then_branch,
311                ..
312            } => {
313                // Create an if with defaults for missing parts
314                Expr::new(
315                    ExprKind::If {
316                        condition: condition.clone().unwrap_or_else(|| {
317                            Box::new(Expr::new(
318                                ExprKind::Literal(Literal::Bool(false)),
319                                default_span,
320                            ))
321                        }),
322                        then_branch: then_branch.clone().unwrap_or_else(|| {
323                            Box::new(Expr::new(ExprKind::Literal(Literal::Unit), default_span))
324                        }),
325                        else_branch: Some(Box::new(Expr::new(
326                            ExprKind::Literal(Literal::Unit),
327                            default_span,
328                        ))),
329                    },
330                    default_span,
331                )
332            }
333            ErrorContext::ArrayLiteral { elements, .. } => {
334                // Return partial array with valid elements
335                Expr::new(ExprKind::List(elements.clone()), default_span)
336            }
337            ErrorContext::BinaryOp { left, .. } => {
338                // Return left side if available, otherwise unit
339                if let Some(left) = left {
340                    *left.clone()
341                } else {
342                    Expr::new(ExprKind::Literal(Literal::Unit), default_span)
343                }
344            }
345            ErrorContext::StructLiteral { name, fields, .. } => {
346                // Return struct with partial fields
347                if let Some(name) = name {
348                    Expr::new(
349                        ExprKind::StructLiteral {
350                            name: name.clone(),
351                            fields: fields.clone(),
352                        },
353                        default_span,
354                    )
355                } else {
356                    Expr::new(ExprKind::Literal(Literal::Unit), default_span)
357                }
358            }
359        }
360    }
361}
362
363/// Integration with parser
364pub 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)]
377mod tests {
378    use super::*;
379
380    #[test]
381    fn test_error_recovery_creation() {
382        let mut recovery = ErrorRecovery::new();
383
384        let error = recovery.missing_function_name(SourceLocation {
385            line: 1,
386            column: 5,
387            file: None,
388        });
389
390        assert_eq!(error.message, "expected function name");
391        assert_eq!(recovery.error_count, 1);
392        assert!(recovery.should_continue());
393    }
394
395    #[test]
396    fn test_recovery_strategy_selection() {
397        let context = ErrorContext::FunctionDecl {
398            name: None,
399            params: None,
400            body: None,
401        };
402
403        let strategy = RecoveryRules::select_strategy(&context);
404
405        match strategy {
406            RecoveryStrategy::InsertToken(token) => {
407                assert_eq!(token, "error_fn");
408            }
409            _ => panic!("Expected InsertToken strategy"),
410        }
411    }
412
413    #[test]
414    fn test_synthetic_ast_generation() {
415        let error = ErrorNode {
416            message: "test error".to_string(),
417            location: SourceLocation {
418                line: 1,
419                column: 1,
420                file: None,
421            },
422            context: ErrorContext::LetBinding {
423                name: Some("x".to_string()),
424                value: None,
425            },
426            recovery: RecoveryStrategy::DefaultValue,
427        };
428
429        let ast = RecoveryRules::synthesize_ast(&error);
430
431        match ast.kind {
432            ExprKind::Let { name, type_annotation: _, value, .. } => {
433                assert_eq!(name, "x");
434                match value.kind {
435                    ExprKind::Literal(Literal::Unit) => {}
436                    _ => panic!("Expected Unit value"),
437                }
438            }
439            _ => panic!("Expected Let expression"),
440        }
441    }
442
443    #[test]
444    fn test_sync_token_detection() {
445        let recovery = ErrorRecovery::new();
446
447        assert!(recovery.is_sync_token(";"));
448        assert!(recovery.is_sync_token("fun"));
449        assert!(recovery.is_sync_token("let"));
450        assert!(!recovery.is_sync_token("="));
451        assert!(!recovery.is_sync_token("+"));
452    }
453
454    #[test]
455    fn test_max_errors_limit() {
456        let mut recovery = ErrorRecovery::new();
457        recovery.max_errors = 3;
458
459        for i in 0..5 {
460            if recovery.should_continue() {
461                recovery.missing_function_name(SourceLocation {
462                    line: i,
463                    column: 0,
464                    file: None,
465                });
466            }
467        }
468
469        assert_eq!(recovery.error_count, 3);
470        assert!(!recovery.should_continue());
471    }
472}