#![allow(dead_code)]
use crate::ast::{Token, Value};
use crate::error::{Error, Span};
use crate::error::ml_patterns::MLPatternRecognizer;
use regex::Regex;
use rustc_hash::FxHashMap;
use std::collections::VecDeque;
#[derive(Debug, Clone)]
pub struct RecoverySuggestion {
pub description: String,
pub confidence: f64,
pub fixed_input: String,
pub category: SuggestionCategory,
pub fix_location: Span,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SuggestionCategory {
MissingBracket,
UnmatchedQuote,
MissingComma,
TrailingComma,
InvalidEscape,
TypeMismatch,
StructuralError,
Other,
}
pub struct ErrorRecoveryEngineV2 {
pattern_db: PatternDatabase,
ml_recognizer: MLPatternRecognizer,
context_analyzer: ContextAnalyzer,
strategies: Vec<Box<dyn RecoveryStrategy>>,
config: RecoveryConfig,
}
#[derive(Debug, Clone)]
pub struct RecoveryConfig {
pub max_suggestions: usize,
pub min_confidence: f64,
pub enable_ml: bool,
pub enable_context: bool,
pub max_attempts: usize,
}
impl Default for RecoveryConfig {
fn default() -> Self {
RecoveryConfig {
max_suggestions: 5,
min_confidence: 0.5,
enable_ml: true,
enable_context: true,
max_attempts: 3,
}
}
}
struct PatternDatabase {
patterns: FxHashMap<String, ErrorPattern>,
learned_patterns: Vec<LearnedPattern>,
compiled_regexes: FxHashMap<String, Regex>,
}
#[derive(Clone)]
struct ErrorPattern {
id: String,
error_type: String,
pattern: String,
base_confidence: f64,
fix_template: String,
success_count: usize,
}
#[derive(Debug, Clone)]
struct LearnedPattern {
context: String,
fix: String,
occurrences: usize,
success_rate: f64,
}
struct ContextAnalyzer {
schema: Option<Value>,
history: VecDeque<String>,
lookahead_size: usize,
}
#[derive(Debug, Clone)]
pub struct ErrorContext {
pub error: Error,
pub input: String,
pub position: usize,
pub tokens_before: Vec<(Token, Span)>,
pub partial_ast: Option<Value>,
pub parsing_context: String,
}
trait RecoveryStrategy {
fn name(&self) -> &str;
fn recover(&self, context: &ErrorContext) -> Vec<RecoverySuggestion>;
}
impl ErrorRecoveryEngineV2 {
pub fn new() -> Self {
Self::with_config(RecoveryConfig::default())
}
pub fn with_config(config: RecoveryConfig) -> Self {
let mut engine = ErrorRecoveryEngineV2 {
pattern_db: PatternDatabase::new(),
ml_recognizer: MLPatternRecognizer::new(),
context_analyzer: ContextAnalyzer::new(),
strategies: Vec::new(),
config,
};
engine.add_default_strategies();
engine
}
fn add_default_strategies(&mut self) {
self.strategies
.push(Box::new(BracketMatchingStrategy::new()));
self.strategies
.push(Box::new(QuoteInferenceStrategy::new()));
self.strategies
.push(Box::new(CommaSuggestionStrategy::new()));
self.strategies.push(Box::new(TypeCoercionStrategy::new()));
self.strategies
.push(Box::new(StructuralRepairStrategy::new()));
}
pub fn suggest_recovery(&mut self, context: &ErrorContext) -> Vec<RecoverySuggestion> {
let mut suggestions = Vec::new();
if self.config.enable_ml {
let ml_suggestions = self.ml_recognizer.recognize_and_suggest(context);
suggestions.extend(ml_suggestions);
suggestions.extend(self.pattern_db.find_matches(context));
}
for strategy in &self.strategies {
let strategy_suggestions = strategy.recover(context);
suggestions.extend(strategy_suggestions);
}
if self.config.enable_context {
suggestions = self
.context_analyzer
.refine_suggestions(suggestions, context);
}
suggestions.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap());
let mut seen_fixes = std::collections::HashSet::new();
suggestions.retain(|s| seen_fixes.insert(s.fixed_input.clone()));
suggestions.truncate(self.config.max_suggestions);
suggestions.retain(|s| s.confidence >= self.config.min_confidence);
suggestions
}
pub fn create_visual_error(&mut self, context: &ErrorContext) -> String {
let mut output = String::new();
output.push_str(&format!("Error: {}\n\n", context.error));
output.push_str(&self.create_source_visualization(context));
let suggestions = self.suggest_recovery(context);
if !suggestions.is_empty() {
output.push_str("\nSuggestions:\n");
for (i, suggestion) in suggestions.iter().enumerate() {
output.push_str(&format!(
" {}. {} (confidence: {:.0}%)\n",
i + 1,
suggestion.description,
suggestion.confidence * 100.0
));
}
}
output
}
fn create_source_visualization(&self, context: &ErrorContext) -> String {
let mut output = String::new();
let lines: Vec<&str> = context.input.lines().collect();
let mut current_pos = 0;
let mut error_line = 0;
let mut error_col = 0;
for (line_idx, line) in lines.iter().enumerate() {
let line_end = current_pos + line.len() + 1; if context.position >= current_pos && context.position < line_end {
error_line = line_idx;
error_col = context.position - current_pos;
break;
}
current_pos = line_end;
}
let start_line = error_line.saturating_sub(2);
let end_line = (error_line + 3).min(lines.len());
for (idx, line) in lines[start_line..end_line].iter().enumerate() {
let line_num = start_line + idx + 1;
output.push_str(&format!("{line_num:4} | {line}\n"));
if start_line + idx == error_line {
output.push_str(&format!(" | {}^\n", " ".repeat(error_col)));
output.push_str(&format!(" | {}--- error here\n", " ".repeat(error_col)));
}
}
output
}
}
impl Default for ErrorRecoveryEngineV2 {
fn default() -> Self {
Self::new()
}
}
impl PatternDatabase {
fn new() -> Self {
let mut db = PatternDatabase {
patterns: FxHashMap::default(),
learned_patterns: Vec::new(),
compiled_regexes: FxHashMap::default(),
};
db.add_common_patterns();
db
}
fn compile_regex(&mut self, pattern_id: &str, pattern: &str) -> Option<&Regex> {
if !self.compiled_regexes.contains_key(pattern_id) {
if let Ok(regex) = Regex::new(pattern) {
self.compiled_regexes.insert(pattern_id.to_string(), regex);
}
}
self.compiled_regexes.get(pattern_id)
}
fn add_common_patterns(&mut self) {
self.patterns.insert(
"missing_closing_bracket".to_string(),
ErrorPattern {
id: "missing_closing_bracket".to_string(),
error_type: "UnexpectedEof".to_string(),
pattern: r"\[.*[^\]]*$".to_string(),
base_confidence: 0.75, fix_template: "{{input}}]".to_string(),
success_count: 0,
},
);
self.patterns.insert(
"missing_closing_brace".to_string(),
ErrorPattern {
id: "missing_closing_brace".to_string(),
error_type: "UnexpectedEof".to_string(),
pattern: r"\{.*[^\}]*$".to_string(),
base_confidence: 0.85,
fix_template: "{{input}}}".to_string(),
success_count: 0,
},
);
self.patterns.insert(
"missing_comma_array".to_string(),
ErrorPattern {
id: "missing_comma_array".to_string(),
error_type: "Expected".to_string(),
pattern: r#"\[(.*?)(\d+|"[^"]*"|true|false|null)\s+(\d+|"[^"]*"|true|false|null)"#.to_string(),
base_confidence: 0.80,
fix_template: "missing_comma".to_string(),
success_count: 0,
},
);
self.patterns.insert(
"unmatched_quote".to_string(),
ErrorPattern {
id: "unmatched_quote".to_string(),
error_type: "UnterminatedString".to_string(),
pattern: r#""[^"]*$"#.to_string(),
base_confidence: 0.90,
fix_template: "{{input}}\"".to_string(),
success_count: 0,
},
);
}
fn find_matches(&mut self, context: &ErrorContext) -> Vec<RecoverySuggestion> {
let mut suggestions = Vec::new();
let pattern_ids: Vec<String> = self.patterns.keys().cloned().collect();
for pattern_id in pattern_ids {
if let Some(pattern) = self.patterns.get(&pattern_id) {
let pattern = pattern.clone();
if let Some(suggestion) = self.match_pattern(&pattern, context) {
let mut suggestion = suggestion;
if (pattern.id == "missing_closing_brace" && context.input.contains('{')) ||
(pattern.id == "missing_closing_bracket" && context.input.contains('[')) {
suggestion.confidence *= 1.1;
}
suggestions.push(suggestion);
}
}
}
suggestions.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal));
suggestions
}
fn match_pattern(&mut self, pattern: &ErrorPattern, context: &ErrorContext) -> Option<RecoverySuggestion> {
let error_type = match &context.error {
Error::UnexpectedEof(_) => "UnexpectedEof",
Error::UnterminatedString(_) => "UnterminatedString",
Error::Expected { .. } => "Expected",
_ => "Other",
};
if pattern.error_type != error_type {
return None;
}
let pattern_matches = if let Some(regex) = self.compile_regex(&pattern.id, &pattern.pattern) {
regex.is_match(&context.input)
} else {
true
};
if !pattern_matches {
return None;
}
let fixed_input = match pattern.fix_template.as_str() {
"{{input}}}" => format!("{}}}", context.input),
"{{input}}]" => format!("{}]", context.input),
"{{input}}\"" => format!("{}\"", context.input),
"missing_comma" => self.fix_missing_comma(context),
_ => context.input.clone(),
};
Some(RecoverySuggestion {
description: self.get_pattern_description(pattern),
confidence: pattern.base_confidence,
fixed_input,
category: self.get_pattern_category(pattern),
fix_location: Span {
start: context.position,
end: context.position,
},
})
}
fn fix_missing_comma(&self, context: &ErrorContext) -> String {
let pos = context.position;
if pos > 0 && pos < context.input.len() {
let mut fixed = context.input.clone();
fixed.insert(pos, ',');
fixed
} else {
context.input.clone()
}
}
fn get_pattern_description(&self, pattern: &ErrorPattern) -> String {
match pattern.id.as_str() {
"missing_closing_bracket" => "Add missing closing bracket ']'".to_string(),
"missing_closing_brace" => "Add missing closing brace '}'".to_string(),
"missing_comma_array" => "Add missing comma between array elements".to_string(),
"unmatched_quote" => "Add missing closing quote".to_string(),
_ => "Fix structural error".to_string(),
}
}
fn get_pattern_category(&self, pattern: &ErrorPattern) -> SuggestionCategory {
match pattern.id.as_str() {
"missing_closing_bracket" | "missing_closing_brace" => SuggestionCategory::MissingBracket,
"unmatched_quote" => SuggestionCategory::UnmatchedQuote,
"missing_comma_array" => SuggestionCategory::MissingComma,
_ => SuggestionCategory::Other,
}
}
}
impl ContextAnalyzer {
fn new() -> Self {
ContextAnalyzer {
schema: None,
history: VecDeque::with_capacity(10),
lookahead_size: 5,
}
}
fn refine_suggestions(
&self,
mut suggestions: Vec<RecoverySuggestion>,
_context: &ErrorContext,
) -> Vec<RecoverySuggestion> {
for suggestion in &mut suggestions {
if let Some(ref schema) = self.schema {
if self.matches_schema(&suggestion.fixed_input, schema) {
suggestion.confidence *= 1.2;
}
}
for previous in &self.history {
let similarity = self.calculate_similarity(&suggestion.fixed_input, previous);
suggestion.confidence *= 1.0 + (similarity * 0.2);
}
}
suggestions
}
fn matches_schema(&self, _input: &str, _schema: &Value) -> bool {
true
}
fn calculate_similarity(&self, _a: &str, _b: &str) -> f64 {
0.5
}
}
struct BracketMatchingStrategy {
bracket_pairs: Vec<(char, char)>,
}
impl BracketMatchingStrategy {
fn new() -> Self {
BracketMatchingStrategy {
bracket_pairs: vec![('{', '}'), ('[', ']'), ('(', ')')],
}
}
}
impl RecoveryStrategy for BracketMatchingStrategy {
fn name(&self) -> &str {
"bracket_matching"
}
fn recover(&self, context: &ErrorContext) -> Vec<RecoverySuggestion> {
let mut suggestions = Vec::new();
let mut stack = Vec::new();
for ch in context.input.chars() {
for (open, close) in &self.bracket_pairs {
if ch == *open {
stack.push(ch);
} else if ch == *close {
if let Some(last) = stack.last() {
if *last == *open {
stack.pop();
} else {
}
}
}
}
}
while let Some(unclosed) = stack.pop() {
for (open, close) in &self.bracket_pairs {
if unclosed == *open {
let mut fixed = context.input.clone();
fixed.push(*close);
suggestions.push(RecoverySuggestion {
description: format!("Add missing '{close}'"),
confidence: 0.8,
fixed_input: fixed,
category: SuggestionCategory::MissingBracket,
fix_location: Span {
start: context.input.len(),
end: context.input.len(),
},
});
}
}
}
suggestions
}
}
struct QuoteInferenceStrategy;
impl QuoteInferenceStrategy {
fn new() -> Self {
QuoteInferenceStrategy
}
}
impl RecoveryStrategy for QuoteInferenceStrategy {
fn name(&self) -> &str {
"quote_inference"
}
fn recover(&self, context: &ErrorContext) -> Vec<RecoverySuggestion> {
let mut suggestions = Vec::new();
let mut in_string = false;
let mut quote_char = '\0';
let mut escape = false;
for ch in context.input.chars() {
if escape {
escape = false;
continue;
}
if ch == '\\' {
escape = true;
continue;
}
if !in_string && (ch == '"' || ch == '\'') {
in_string = true;
quote_char = ch;
} else if in_string && ch == quote_char {
in_string = false;
}
}
if in_string {
let mut fixed = context.input.clone();
fixed.push(quote_char);
suggestions.push(RecoverySuggestion {
description: format!("Add missing closing quote '{quote_char}'"),
confidence: 0.9,
fixed_input: fixed,
category: SuggestionCategory::UnmatchedQuote,
fix_location: Span {
start: context.input.len(),
end: context.input.len(),
},
});
}
suggestions
}
}
struct CommaSuggestionStrategy;
impl CommaSuggestionStrategy {
fn new() -> Self {
CommaSuggestionStrategy
}
}
impl RecoveryStrategy for CommaSuggestionStrategy {
fn name(&self) -> &str {
"comma_suggestion"
}
fn recover(&self, context: &ErrorContext) -> Vec<RecoverySuggestion> {
let mut suggestions = Vec::new();
if let Error::Expected { expected, .. } = &context.error {
if expected.contains("comma") {
let before_pos = context.position.saturating_sub(10);
let after_pos = (context.position + 10).min(context.input.len());
if after_pos > before_pos && after_pos <= context.input.len() {
let context_str = &context.input[before_pos..after_pos];
if context_str.contains("\" \"") || context_str.contains("} {") ||
context_str.contains("] [") || context_str.contains("e ") {
let mut fixed = context.input.clone();
fixed.insert(context.position, ',');
suggestions.push(RecoverySuggestion {
description: "Add missing comma between elements".to_string(),
confidence: 0.75,
fixed_input: fixed,
category: SuggestionCategory::MissingComma,
fix_location: Span {
start: context.position,
end: context.position,
},
});
}
}
}
}
suggestions
}
}
struct TypeCoercionStrategy;
impl TypeCoercionStrategy {
fn new() -> Self {
TypeCoercionStrategy
}
}
impl RecoveryStrategy for TypeCoercionStrategy {
fn name(&self) -> &str {
"type_coercion"
}
fn recover(&self, context: &ErrorContext) -> Vec<RecoverySuggestion> {
let mut suggestions = Vec::new();
let search_end = context.position.min(context.input.len());
if let Some(pos) = context.input[..search_end].rfind('"') {
let value_start = pos + 1;
if let Some(end_pos) = context.input[value_start..].find('"') {
let value = &context.input[value_start..value_start + end_pos];
if value == "true" || value == "false" {
let mut fixed = context.input.clone();
fixed.replace_range(pos..value_start + end_pos + 1, value);
suggestions.push(RecoverySuggestion {
description: format!("Convert string \"{value}\" to boolean {value}"),
confidence: 0.70,
fixed_input: fixed,
category: SuggestionCategory::TypeMismatch,
fix_location: Span {
start: pos,
end: value_start + end_pos + 1,
},
});
}
if value.parse::<f64>().is_ok() {
let mut fixed = context.input.clone();
fixed.replace_range(pos..value_start + end_pos + 1, value);
suggestions.push(RecoverySuggestion {
description: format!("Convert string \"{value}\" to number {value}"),
confidence: 0.65,
fixed_input: fixed,
category: SuggestionCategory::TypeMismatch,
fix_location: Span {
start: pos,
end: value_start + end_pos + 1,
},
});
}
}
}
suggestions
}
}
struct StructuralRepairStrategy;
impl StructuralRepairStrategy {
fn new() -> Self {
StructuralRepairStrategy
}
}
impl RecoveryStrategy for StructuralRepairStrategy {
fn name(&self) -> &str {
"structural_repair"
}
fn recover(&self, context: &ErrorContext) -> Vec<RecoverySuggestion> {
let mut suggestions = Vec::new();
if context.parsing_context == "top_level" {
let trimmed = context.input.trim();
if trimmed.contains(':') && !trimmed.starts_with('{') && !trimmed.starts_with('[') {
let fixed = format!("{{{trimmed}}}");
suggestions.push(RecoverySuggestion {
description: "Wrap in object braces for implicit object".to_string(),
confidence: 0.80,
fixed_input: fixed,
category: SuggestionCategory::StructuralError,
fix_location: Span {
start: 0,
end: context.input.len(),
},
});
}
if trimmed.contains(',') && !trimmed.starts_with('[') && !trimmed.starts_with('{') {
let fixed = format!("[{trimmed}]");
suggestions.push(RecoverySuggestion {
description: "Wrap in array brackets for implicit array".to_string(),
confidence: 0.75,
fixed_input: fixed,
category: SuggestionCategory::StructuralError,
fix_location: Span {
start: 0,
end: context.input.len(),
},
});
}
}
suggestions
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bracket_matching() {
let mut engine = ErrorRecoveryEngineV2::new();
let context = ErrorContext {
error: Error::UnexpectedEof(10),
input: r#"{"name": "test""#.to_string(),
position: 15,
tokens_before: vec![],
partial_ast: None,
parsing_context: "in_object".to_string(),
};
let suggestions = engine.suggest_recovery(&context);
assert!(!suggestions.is_empty());
let first = &suggestions[0];
assert_eq!(first.category, SuggestionCategory::MissingBracket);
assert!(first.fixed_input.ends_with('}'));
}
#[test]
fn test_quote_inference() {
let mut engine = ErrorRecoveryEngineV2::new();
let context = ErrorContext {
error: Error::UnterminatedString(5),
input: r#"{"key": "value"#.to_string(),
position: 14,
tokens_before: vec![],
partial_ast: None,
parsing_context: "in_string".to_string(),
};
let suggestions = engine.suggest_recovery(&context);
assert!(!suggestions.is_empty());
let first = &suggestions[0];
assert_eq!(first.category, SuggestionCategory::UnmatchedQuote);
}
#[test]
fn test_visual_error() {
let mut engine = ErrorRecoveryEngineV2::new();
let context = ErrorContext {
error: Error::UnexpectedEof(20),
input: "{\n \"name\": \"test\"\n \"age\": 25\n}".to_string(),
position: 20, tokens_before: vec![],
partial_ast: None,
parsing_context: "in_object".to_string(),
};
let visual = engine.create_visual_error(&context);
assert!(visual.contains("---"));
assert!(visual.contains("^"));
}
}