Skip to main content

perl_error/
recovery.rs

1//! Error recovery for the Perl parser
2//!
3//! This module implements error recovery strategies to continue parsing
4//! even when syntax errors are encountered. This is essential for IDE
5//! scenarios where code is often incomplete or temporarily invalid.
6//!
7//! # Progress Invariant
8//!
9//! All recovery operations guarantee forward progress: every recovery attempt
10//! must consume at least one token or exit. This prevents infinite loops when
11//! the parser cannot make sense of the input.
12//!
13//! # Budget Awareness
14//!
15//! Recovery operations respect the `ParseBudget` limits to prevent runaway
16//! parsing on adversarial input. When budget is exhausted, recovery returns
17//! immediately with an appropriate error node.
18
19use crate::{BudgetTracker, ParseBudget};
20use perl_ast_v2::Node;
21use perl_lexer::TokenType;
22use perl_position_tracking::Range;
23
24/// Error information with recovery context for comprehensive Perl parsing error handling.
25///
26/// This structure encapsulates all information needed for intelligent error recovery
27/// in the Perl parser, enabling continued parsing after syntax errors and providing
28/// detailed diagnostic information for IDE integration.
29#[derive(Debug, Clone)]
30pub struct ParseError {
31    /// Human-readable error message describing the parsing issue
32    pub message: String,
33    /// Source code range where the error occurred
34    pub range: Range,
35    /// List of token types that were expected at this position
36    pub expected: Vec<String>,
37    /// The token that was actually found instead of expected
38    pub found: String,
39    /// Optional hint for error recovery or fixing the issue
40    pub recovery_hint: Option<String>,
41}
42
43impl ParseError {
44    /// Create a new parse error
45    pub fn new(message: String, range: Range) -> Self {
46        ParseError {
47            message,
48            range,
49            expected: Vec::new(),
50            found: String::new(),
51            recovery_hint: None,
52        }
53    }
54
55    /// Add expected tokens
56    pub fn with_expected(mut self, expected: Vec<String>) -> Self {
57        self.expected = expected;
58        self
59    }
60
61    /// Add found token
62    pub fn with_found(mut self, found: String) -> Self {
63        self.found = found;
64        self
65    }
66
67    /// Add recovery hint
68    pub fn with_hint(mut self, hint: String) -> Self {
69        self.recovery_hint = Some(hint);
70        self
71    }
72}
73
74/// Synchronization tokens for error recovery
75#[derive(Debug, Clone, Copy, PartialEq)]
76pub enum SyncPoint {
77    /// Semicolon - statement boundary
78    Semicolon,
79    /// Closing brace - block boundary
80    CloseBrace,
81    /// Keywords that start statements
82    Keyword,
83    /// End of file
84    Eof,
85}
86
87/// Result of a recovery operation.
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum RecoveryResult {
90    /// Recovery succeeded, consumed the given number of tokens.
91    Recovered(usize),
92    /// Already at a sync point when recovery was called.
93    /// The caller must decide whether to consume the sync token.
94    /// This prevents infinite loops at call boundaries.
95    AtSyncPoint,
96    /// Recovery failed due to budget exhaustion.
97    BudgetExhausted,
98    /// Recovery reached EOF without finding sync point.
99    ReachedEof,
100}
101
102/// Error recovery strategies
103pub trait ErrorRecovery {
104    /// Create an error node and recover
105    fn create_error_node(
106        &mut self,
107        message: String,
108        expected: Vec<String>,
109        partial: Option<Node>,
110    ) -> Node;
111
112    /// Synchronize to a recovery point
113    fn synchronize(&mut self, sync_points: &[SyncPoint]) -> bool;
114
115    /// Try to recover from an error
116    fn recover_with_node(&mut self, error: ParseError) -> Node;
117
118    /// Skip tokens until a sync point.
119    ///
120    /// # Progress Invariant
121    ///
122    /// This method guarantees forward progress: it will consume at least one
123    /// token on each call (unless already at EOF or a sync point), preventing
124    /// infinite recovery loops.
125    fn skip_until(&mut self, sync_points: &[SyncPoint]) -> usize;
126
127    /// Budget-aware skip that respects limits.
128    ///
129    /// # Progress Invariant
130    ///
131    /// Consumes at least one token per call (unless at sync point, EOF, or budget exhausted).
132    fn skip_until_with_budget(
133        &mut self,
134        sync_points: &[SyncPoint],
135        budget: &ParseBudget,
136        tracker: &mut BudgetTracker,
137    ) -> RecoveryResult;
138
139    /// Check if current token is a sync point
140    fn is_sync_point(&self, sync_point: SyncPoint) -> bool;
141}
142
143/// Parser extensions for error recovery
144pub trait ParserErrorRecovery {
145    /// Parse with error recovery enabled
146    fn parse_with_recovery(&mut self) -> (Node, Vec<ParseError>);
147
148    /// Try to parse, returning an error node on failure
149    fn try_parse<F>(&mut self, parse_fn: F) -> Node
150    where
151        F: FnOnce(&mut Self) -> Option<Node>;
152
153    /// Parse a list with recovery on each element
154    fn parse_list_with_recovery<F>(
155        &mut self,
156        parse_element: F,
157        separator: TokenType,
158        terminator: TokenType,
159    ) -> Vec<Node>
160    where
161        F: Fn(&mut Self) -> Node;
162}
163
164/// Recovery-aware statement parsing
165pub trait StatementRecovery {
166    /// Parse statement with recovery
167    fn parse_statement_with_recovery(&mut self) -> Node;
168
169    /// Parse expression with recovery
170    fn parse_expression_with_recovery(&mut self) -> Node;
171
172    /// Parse block with recovery
173    fn parse_block_with_recovery(&mut self) -> Node;
174}