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}