## Coverage Report

#### Created: 2025-09-08 21: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
    0
        fn default() -> Self {
    105
    0
            Self {
    106
    0
                sync_tokens: vec![
    107
    0
                    ";".to_string(),
    108
    0
                    "}".to_string(),
    109
    0
                    "fun".to_string(),
    110
    0
                    "let".to_string(),
    111
    0
                    "if".to_string(),
    112
    0
                    "for".to_string(),
    113
    0
                    "while".to_string(),
    114
    0
                    "return".to_string(),
    115
    0
                    "struct".to_string(),
    116
    0
                    "enum".to_string(),
    117
    0
                ],
    118
    0
                max_errors: 100,
    119
    0
                error_count: 0,
    120
    0
            }
    121
    0
        }
    122
    }
    123
    124
    impl ErrorRecovery {
    125
        #[must_use]
    126
    0
        pub fn new() -> Self {
    127
    0
            Self::default()
    128
    0
        }
    129
    130
        /// Create a synthetic error node for missing function name
    131
    0
        pub fn missing_function_name(&mut self, location: SourceLocation) -> ErrorNode {
    132
    0
            self.error_count += 1;
    133
    0
            ErrorNode {
    134
    0
                message: "expected function name".to_string(),
    135
    0
                location,
    136
    0
                context: ErrorContext::FunctionDecl {
    137
    0
                    name: None,
    138
    0
                    params: None,
    139
    0
                    body: None,
    140
    0
                },
    141
    0
                recovery: RecoveryStrategy::InsertToken("error_fn".to_string()),
    142
    0
            }
    143
    0
        }
    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
    0
        pub fn should_continue(&self) -> bool {
    222
    0
            self.error_count < self.max_errors
    223
    0
        }
    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
    0
        pub fn is_sync_token(&self, token: &str) -> bool {
    233
    0
            self.sync_tokens.contains(&token.to_string())
    234
    0
        }
    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
    0
        pub fn select_strategy(context: &ErrorContext) -> RecoveryStrategy {
    257
    0
            match context {
    258
    0
                ErrorContext::FunctionDecl { name, params, body } => {
    259
    0
                    if name.is_none() {
    260
    0
                        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
    0
        }
    277
    278
        /// Generate synthetic AST for error recovery
    279
        #[must_use]
    280
    0
        pub fn synthesize_ast(error: &ErrorNode) -> Expr {
    281
    0
            let default_span = Span::new(0, 0);
    282
    0
            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
    0
                ErrorContext::LetBinding { name, value } => {
    294
                    // Create a let with whatever we could parse
    295
    0
                    Expr::new(
    296
                        ExprKind::Let {
    297
    0
                            name: name.clone().unwrap_or_else(|| "_error".to_string()),
    298
    0
                            type_annotation: None,
    299
    0
                            value: value.clone().unwrap_or_else(|| {
    300
    0
                                Box::new(Expr::new(ExprKind::Literal(Literal::Unit), default_span))
    301
    0
                            }),
    302
    0
                            body: Box::new(Expr::new(ExprKind::Literal(Literal::Unit), default_span)),
    303
                            is_mutable: false,
    304
                        },
    305
    0
                        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
    0
        }
    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
        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
    }