Skip to main content

oxilean_parse/error/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5pub use crate::error_impl::{ParseError, ParseErrorKind};
6
7use super::functions::*;
8
9/// Represents a user-facing error explanation page.
10#[allow(dead_code)]
11#[allow(missing_docs)]
12pub struct ErrorExplanation {
13    pub code: String,
14    pub title: String,
15    pub description: String,
16    pub example_bad: String,
17    #[allow(missing_docs)]
18    pub example_good: String,
19}
20impl ErrorExplanation {
21    #[allow(dead_code)]
22    #[allow(missing_docs)]
23    pub fn new(
24        code: impl Into<String>,
25        title: impl Into<String>,
26        desc: impl Into<String>,
27        bad: impl Into<String>,
28        good: impl Into<String>,
29    ) -> Self {
30        Self {
31            code: code.into(),
32            title: title.into(),
33            description: desc.into(),
34            example_bad: bad.into(),
35            example_good: good.into(),
36        }
37    }
38    #[allow(dead_code)]
39    #[allow(missing_docs)]
40    pub fn render(&self) -> String {
41        format!(
42            "[{}] {}\n\n{}\n\nBad:\n{}\n\nGood:\n{}",
43            self.code, self.title, self.description, self.example_bad, self.example_good
44        )
45    }
46}
47/// Error location resolver: maps span to line/column.
48#[allow(dead_code)]
49#[allow(missing_docs)]
50pub struct ErrorLocationResolver {
51    source: String,
52    line_starts: Vec<usize>,
53}
54impl ErrorLocationResolver {
55    #[allow(dead_code)]
56    #[allow(missing_docs)]
57    pub fn new(source: impl Into<String>) -> Self {
58        let s = source.into();
59        let mut starts = vec![0usize];
60        for (i, c) in s.char_indices() {
61            if c == '\n' {
62                starts.push(i + 1);
63            }
64        }
65        Self {
66            source: s,
67            line_starts: starts,
68        }
69    }
70    #[allow(dead_code)]
71    #[allow(missing_docs)]
72    pub fn resolve(&self, byte_offset: usize) -> (usize, usize) {
73        let off = byte_offset.min(self.source.len());
74        let line = self
75            .line_starts
76            .partition_point(|&s| s <= off)
77            .saturating_sub(1);
78        let col = off - self.line_starts[line];
79        (line, col)
80    }
81    #[allow(dead_code)]
82    #[allow(missing_docs)]
83    pub fn line_text(&self, line: usize) -> &str {
84        let start = *self.line_starts.get(line).unwrap_or(&self.source.len());
85        let end = *self.line_starts.get(line + 1).unwrap_or(&self.source.len());
86        let end = if end > start && self.source.as_bytes().get(end - 1) == Some(&b'\n') {
87            end - 1
88        } else {
89            end
90        };
91        &self.source[start..end]
92    }
93    #[allow(dead_code)]
94    #[allow(missing_docs)]
95    pub fn line_count(&self) -> usize {
96        self.line_starts.len()
97    }
98    #[allow(dead_code)]
99    #[allow(missing_docs)]
100    pub fn snippet(&self, byte_offset: usize, context_lines: usize) -> String {
101        let (line, col) = self.resolve(byte_offset);
102        let start_line = line.saturating_sub(context_lines);
103        let end_line = (line + context_lines).min(self.line_count().saturating_sub(1));
104        let mut out = String::new();
105        for l in start_line..=end_line {
106            let text = self.line_text(l);
107            out.push_str(&format!("{:4} | {}\n", l + 1, text));
108            if l == line {
109                out.push_str(&format!("     | {}^\n", " ".repeat(col)));
110            }
111        }
112        out
113    }
114}
115/// An error sink that writes errors to a string buffer.
116#[allow(dead_code)]
117#[allow(missing_docs)]
118pub struct StringErrorSink {
119    buffer: String,
120    count: usize,
121}
122impl StringErrorSink {
123    #[allow(dead_code)]
124    #[allow(missing_docs)]
125    pub fn new() -> Self {
126        Self {
127            buffer: String::new(),
128            count: 0,
129        }
130    }
131    #[allow(dead_code)]
132    #[allow(missing_docs)]
133    pub fn emit(&mut self, e: &RichError) {
134        if !self.buffer.is_empty() {
135            self.buffer.push('\n');
136        }
137        self.buffer.push_str(&e.format());
138        self.count += 1;
139    }
140    #[allow(dead_code)]
141    #[allow(missing_docs)]
142    pub fn contents(&self) -> &str {
143        &self.buffer
144    }
145    #[allow(dead_code)]
146    #[allow(missing_docs)]
147    pub fn count(&self) -> usize {
148        self.count
149    }
150    #[allow(dead_code)]
151    #[allow(missing_docs)]
152    pub fn clear(&mut self) {
153        self.buffer.clear();
154        self.count = 0;
155    }
156}
157/// Attaches contextual information to a `ParseError`.
158#[allow(dead_code)]
159#[allow(missing_docs)]
160#[derive(Clone, Debug)]
161pub struct ParseErrorContext {
162    /// The underlying error.
163    pub error: ParseError,
164    /// The name of the declaration being parsed when the error occurred.
165    pub decl_name: Option<String>,
166    /// The parser phase in which the error occurred.
167    #[allow(missing_docs)]
168    pub phase: Option<String>,
169}
170impl ParseErrorContext {
171    /// Create a context wrapping an error.
172    #[allow(dead_code)]
173    #[allow(missing_docs)]
174    pub fn new(error: ParseError) -> Self {
175        Self {
176            error,
177            decl_name: None,
178            phase: None,
179        }
180    }
181    /// Attach a declaration name.
182    #[allow(dead_code)]
183    #[allow(missing_docs)]
184    pub fn with_decl(mut self, name: &str) -> Self {
185        self.decl_name = Some(name.to_string());
186        self
187    }
188    /// Attach a parser phase.
189    #[allow(dead_code)]
190    #[allow(missing_docs)]
191    pub fn with_phase(mut self, phase: &str) -> Self {
192        self.phase = Some(phase.to_string());
193        self
194    }
195}
196/// Error severity enum for extended error handling.
197#[allow(dead_code)]
198#[allow(missing_docs)]
199#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
200pub enum ErrorSeverityLevel {
201    Hint,
202    Note,
203    Warning,
204    Error,
205    Fatal,
206}
207/// The severity of a parse diagnostic.
208#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
209#[allow(missing_docs)]
210pub enum ErrorSeverity {
211    /// Informational note.
212    Note,
213    /// Non-fatal warning.
214    Warning,
215    /// Fatal error — parsing cannot produce a valid result.
216    Error,
217}
218impl ErrorSeverity {
219    /// Whether this severity represents a hard error.
220    #[allow(missing_docs)]
221    pub fn is_error(&self) -> bool {
222        matches!(self, ErrorSeverity::Error)
223    }
224    /// Whether parsing can continue after this diagnostic.
225    #[allow(missing_docs)]
226    pub fn is_recoverable(&self) -> bool {
227        !self.is_error()
228    }
229}
230/// Accumulates multiple parse errors before reporting them all at once.
231#[derive(Clone, Debug, Default)]
232#[allow(missing_docs)]
233pub struct ParseErrorCollector {
234    /// Collected errors in order.
235    errors: Vec<ParseError>,
236    /// Maximum number of errors to store (0 = unlimited).
237    limit: usize,
238}
239impl ParseErrorCollector {
240    /// Create an unlimited collector.
241    #[allow(missing_docs)]
242    pub fn new() -> Self {
243        Self::default()
244    }
245    /// Create a collector with a maximum error count.
246    #[allow(missing_docs)]
247    pub fn with_limit(limit: usize) -> Self {
248        Self {
249            errors: Vec::new(),
250            limit,
251        }
252    }
253    /// Add an error.
254    #[allow(missing_docs)]
255    pub fn add(&mut self, error: ParseError) {
256        if self.limit == 0 || self.errors.len() < self.limit {
257            self.errors.push(error);
258        }
259    }
260    /// Whether any errors have been collected.
261    #[allow(missing_docs)]
262    pub fn has_errors(&self) -> bool {
263        !self.errors.is_empty()
264    }
265    /// Number of errors collected.
266    #[allow(missing_docs)]
267    pub fn len(&self) -> usize {
268        self.errors.len()
269    }
270    /// Whether the collector is empty.
271    #[allow(missing_docs)]
272    pub fn is_empty(&self) -> bool {
273        self.errors.is_empty()
274    }
275    /// Consume the collector and return the error list.
276    #[allow(missing_docs)]
277    pub fn into_errors(self) -> Vec<ParseError> {
278        self.errors
279    }
280    /// Return a reference to all errors.
281    #[allow(missing_docs)]
282    pub fn errors(&self) -> &[ParseError] {
283        &self.errors
284    }
285    /// Take the first error, if any.
286    #[allow(missing_docs)]
287    pub fn first_error(&self) -> Option<&ParseError> {
288        self.errors.first()
289    }
290    /// Clear all collected errors.
291    #[allow(missing_docs)]
292    pub fn clear(&mut self) {
293        self.errors.clear();
294    }
295    /// Whether the error limit has been reached.
296    #[allow(missing_docs)]
297    pub fn is_full(&self) -> bool {
298        self.limit > 0 && self.errors.len() >= self.limit
299    }
300    /// Merge another collector's errors into this one.
301    #[allow(missing_docs)]
302    pub fn merge(&mut self, other: ParseErrorCollector) {
303        for e in other.errors {
304            self.add(e);
305        }
306    }
307}
308/// Batch error report with summary statistics.
309#[allow(dead_code)]
310#[allow(missing_docs)]
311pub struct BatchErrorReport {
312    pub errors: Vec<RichError>,
313    pub source_file: String,
314    pub parse_time_us: u64,
315}
316impl BatchErrorReport {
317    #[allow(dead_code)]
318    #[allow(missing_docs)]
319    pub fn new(source_file: impl Into<String>, errors: Vec<RichError>, parse_time_us: u64) -> Self {
320        Self {
321            errors,
322            source_file: source_file.into(),
323            parse_time_us,
324        }
325    }
326    #[allow(dead_code)]
327    #[allow(missing_docs)]
328    pub fn error_count(&self) -> usize {
329        self.errors
330            .iter()
331            .filter(|e| e.severity >= ErrorSeverityLevel::Error)
332            .count()
333    }
334    #[allow(dead_code)]
335    #[allow(missing_docs)]
336    pub fn warning_count(&self) -> usize {
337        self.errors
338            .iter()
339            .filter(|e| e.severity == ErrorSeverityLevel::Warning)
340            .count()
341    }
342    #[allow(dead_code)]
343    #[allow(missing_docs)]
344    pub fn is_success(&self) -> bool {
345        self.error_count() == 0
346    }
347    #[allow(dead_code)]
348    #[allow(missing_docs)]
349    pub fn summary_line(&self) -> String {
350        format!(
351            "{}: {} error(s), {} warning(s) in {}us",
352            self.source_file,
353            self.error_count(),
354            self.warning_count(),
355            self.parse_time_us
356        )
357    }
358}
359/// An error grouper: clusters errors by their error code.
360#[allow(dead_code)]
361#[allow(missing_docs)]
362pub struct ErrorGrouper {
363    groups: std::collections::HashMap<String, Vec<RichError>>,
364}
365impl ErrorGrouper {
366    #[allow(dead_code)]
367    #[allow(missing_docs)]
368    pub fn new() -> Self {
369        Self {
370            groups: std::collections::HashMap::new(),
371        }
372    }
373    #[allow(dead_code)]
374    #[allow(missing_docs)]
375    pub fn add(&mut self, e: RichError) {
376        let key = e.code.clone().unwrap_or_else(|| "no-code".to_string());
377        self.groups.entry(key).or_default().push(e);
378    }
379    #[allow(dead_code)]
380    #[allow(missing_docs)]
381    pub fn group_count(&self) -> usize {
382        self.groups.len()
383    }
384    #[allow(dead_code)]
385    #[allow(missing_docs)]
386    pub fn errors_in_group(&self, code: &str) -> &[RichError] {
387        self.groups.get(code).map(|v| v.as_slice()).unwrap_or(&[])
388    }
389    #[allow(dead_code)]
390    #[allow(missing_docs)]
391    pub fn most_common_code(&self) -> Option<&str> {
392        self.groups
393            .iter()
394            .max_by_key(|(_, v)| v.len())
395            .map(|(k, _)| k.as_str())
396    }
397    #[allow(dead_code)]
398    #[allow(missing_docs)]
399    pub fn total_error_count(&self) -> usize {
400        self.groups.values().map(|v| v.len()).sum()
401    }
402}
403#[allow(dead_code)]
404#[allow(missing_docs)]
405#[derive(Clone, Copy, Debug, PartialEq, Eq)]
406pub enum RecoveryHintKind {
407    InsertBefore,
408    InsertAfter,
409    Delete,
410    Replace,
411    Reorder,
412}
413/// Tracks error rates over time for adaptive error handling.
414#[allow(dead_code)]
415#[allow(missing_docs)]
416#[derive(Default, Debug)]
417pub struct ErrorRateTracker {
418    window_size: usize,
419    counts: std::collections::VecDeque<usize>,
420    current: usize,
421}
422impl ErrorRateTracker {
423    #[allow(dead_code)]
424    #[allow(missing_docs)]
425    pub fn new(window_size: usize) -> Self {
426        Self {
427            window_size,
428            counts: std::collections::VecDeque::new(),
429            current: 0,
430        }
431    }
432    #[allow(dead_code)]
433    #[allow(missing_docs)]
434    pub fn record(&mut self, errors: usize) {
435        self.current += errors;
436    }
437    #[allow(dead_code)]
438    #[allow(missing_docs)]
439    pub fn commit_window(&mut self) {
440        self.counts.push_back(self.current);
441        if self.counts.len() > self.window_size {
442            self.counts.pop_front();
443        }
444        self.current = 0;
445    }
446    #[allow(dead_code)]
447    #[allow(missing_docs)]
448    pub fn average(&self) -> f64 {
449        if self.counts.is_empty() {
450            return 0.0;
451        }
452        self.counts.iter().sum::<usize>() as f64 / self.counts.len() as f64
453    }
454    #[allow(dead_code)]
455    #[allow(missing_docs)]
456    pub fn trend(&self) -> f64 {
457        if self.counts.len() < 2 {
458            return 0.0;
459        }
460        let first = *self
461            .counts
462            .front()
463            .expect("counts.len() >= 2 per check above") as f64;
464        let last = *self
465            .counts
466            .back()
467            .expect("counts.len() >= 2 per check above") as f64;
468        last - first
469    }
470}
471/// A non-fatal parse warning.
472#[derive(Clone, Debug)]
473#[allow(missing_docs)]
474pub struct ParseWarning {
475    /// Warning message.
476    pub message: String,
477    /// Source line.
478    pub line: u32,
479    /// Source column.
480    #[allow(missing_docs)]
481    pub col: u32,
482}
483impl ParseWarning {
484    /// Create a new warning.
485    #[allow(missing_docs)]
486    pub fn new(msg: &str, line: u32, col: u32) -> Self {
487        Self {
488            message: msg.to_string(),
489            line,
490            col,
491        }
492    }
493}
494/// A rich parse diagnostic combining severity, location, and message.
495#[derive(Clone, Debug)]
496#[allow(missing_docs)]
497pub struct ParseDiagnostic {
498    /// Severity of this diagnostic.
499    pub severity: ErrorSeverity,
500    /// Source file name.
501    pub filename: String,
502    /// 1-based line number.
503    #[allow(missing_docs)]
504    pub line: u32,
505    /// 1-based column number.
506    pub col: u32,
507    /// The diagnostic message.
508    pub message: String,
509    /// Optional hint for fixing the issue.
510    #[allow(missing_docs)]
511    pub hint: Option<String>,
512    /// Optional code fragment illustrating the issue.
513    pub code: Option<String>,
514}
515impl ParseDiagnostic {
516    /// Create a new diagnostic.
517    #[allow(missing_docs)]
518    pub fn new(
519        severity: ErrorSeverity,
520        filename: &str,
521        line: u32,
522        col: u32,
523        message: &str,
524    ) -> Self {
525        Self {
526            severity,
527            filename: filename.to_string(),
528            line,
529            col,
530            message: message.to_string(),
531            hint: None,
532            code: None,
533        }
534    }
535    /// Create an error diagnostic.
536    #[allow(missing_docs)]
537    pub fn error(filename: &str, line: u32, col: u32, msg: &str) -> Self {
538        Self::new(ErrorSeverity::Error, filename, line, col, msg)
539    }
540    /// Create a warning diagnostic.
541    #[allow(missing_docs)]
542    pub fn warning(filename: &str, line: u32, col: u32, msg: &str) -> Self {
543        Self::new(ErrorSeverity::Warning, filename, line, col, msg)
544    }
545    /// Attach a hint.
546    #[allow(missing_docs)]
547    pub fn with_hint(mut self, hint: &str) -> Self {
548        self.hint = Some(hint.to_string());
549        self
550    }
551    /// Attach a code snippet.
552    #[allow(missing_docs)]
553    pub fn with_code(mut self, code: &str) -> Self {
554        self.code = Some(code.to_string());
555        self
556    }
557    /// Whether this is a hard error.
558    #[allow(missing_docs)]
559    pub fn is_error(&self) -> bool {
560        self.severity.is_error()
561    }
562}
563/// A collection of error explanations.
564#[allow(dead_code)]
565#[allow(missing_docs)]
566pub struct ErrorExplanationBook {
567    pages: std::collections::HashMap<String, ErrorExplanation>,
568}
569impl ErrorExplanationBook {
570    #[allow(dead_code)]
571    #[allow(missing_docs)]
572    pub fn new() -> Self {
573        Self {
574            pages: std::collections::HashMap::new(),
575        }
576    }
577    #[allow(dead_code)]
578    #[allow(missing_docs)]
579    pub fn add(&mut self, page: ErrorExplanation) {
580        self.pages.insert(page.code.clone(), page);
581    }
582    #[allow(dead_code)]
583    #[allow(missing_docs)]
584    pub fn lookup(&self, code: &str) -> Option<&ErrorExplanation> {
585        self.pages.get(code)
586    }
587    #[allow(dead_code)]
588    #[allow(missing_docs)]
589    pub fn count(&self) -> usize {
590        self.pages.len()
591    }
592}
593/// A named group of related parse errors.
594#[derive(Clone, Debug, Default)]
595#[allow(missing_docs)]
596pub struct ParseErrorGroup {
597    /// Group label.
598    pub label: String,
599    /// Errors in this group.
600    pub errors: Vec<ParseError>,
601}
602impl ParseErrorGroup {
603    /// Create a new group.
604    #[allow(missing_docs)]
605    pub fn new(label: &str) -> Self {
606        Self {
607            label: label.to_string(),
608            errors: Vec::new(),
609        }
610    }
611    /// Add an error.
612    #[allow(missing_docs)]
613    pub fn add(&mut self, e: ParseError) {
614        self.errors.push(e);
615    }
616    /// Number of errors.
617    #[allow(missing_docs)]
618    pub fn len(&self) -> usize {
619        self.errors.len()
620    }
621    /// Whether the group is empty.
622    #[allow(missing_docs)]
623    pub fn is_empty(&self) -> bool {
624        self.errors.is_empty()
625    }
626}
627/// A recovery hint associated with an error.
628#[allow(dead_code)]
629#[allow(missing_docs)]
630#[derive(Clone, Debug)]
631pub struct RecoveryHint {
632    pub kind: RecoveryHintKind,
633    pub text: String,
634}
635impl RecoveryHint {
636    #[allow(dead_code)]
637    #[allow(missing_docs)]
638    pub fn insert_before(text: impl Into<String>) -> Self {
639        Self {
640            kind: RecoveryHintKind::InsertBefore,
641            text: text.into(),
642        }
643    }
644    #[allow(dead_code)]
645    #[allow(missing_docs)]
646    pub fn delete(text: impl Into<String>) -> Self {
647        Self {
648            kind: RecoveryHintKind::Delete,
649            text: text.into(),
650        }
651    }
652    #[allow(dead_code)]
653    #[allow(missing_docs)]
654    pub fn replace(text: impl Into<String>) -> Self {
655        Self {
656            kind: RecoveryHintKind::Replace,
657            text: text.into(),
658        }
659    }
660    #[allow(dead_code)]
661    #[allow(missing_docs)]
662    pub fn description(&self) -> String {
663        match self.kind {
664            RecoveryHintKind::InsertBefore => {
665                format!("insert '{}' before this token", self.text)
666            }
667            RecoveryHintKind::InsertAfter => {
668                format!("insert '{}' after this token", self.text)
669            }
670            RecoveryHintKind::Delete => format!("delete '{}'", self.text),
671            RecoveryHintKind::Replace => format!("replace with '{}'", self.text),
672            RecoveryHintKind::Reorder => format!("reorder: {}", self.text),
673        }
674    }
675}
676/// Extended rich error with tags and recovery hints.
677#[allow(dead_code)]
678#[allow(missing_docs)]
679#[derive(Clone, Debug)]
680pub struct TaggedError {
681    pub inner: RichError,
682    pub tags: Vec<ErrorTag>,
683    pub hints: Vec<RecoveryHint>,
684}
685impl TaggedError {
686    #[allow(dead_code)]
687    #[allow(missing_docs)]
688    pub fn new(e: RichError) -> Self {
689        Self {
690            inner: e,
691            tags: Vec::new(),
692            hints: Vec::new(),
693        }
694    }
695    #[allow(dead_code)]
696    #[allow(missing_docs)]
697    pub fn with_tag(mut self, tag: ErrorTag) -> Self {
698        self.tags.push(tag);
699        self
700    }
701    #[allow(dead_code)]
702    #[allow(missing_docs)]
703    pub fn with_hint(mut self, hint: RecoveryHint) -> Self {
704        self.hints.push(hint);
705        self
706    }
707    #[allow(dead_code)]
708    #[allow(missing_docs)]
709    pub fn has_tag(&self, tag: ErrorTag) -> bool {
710        self.tags.contains(&tag)
711    }
712    #[allow(dead_code)]
713    #[allow(missing_docs)]
714    pub fn format_full(&self) -> String {
715        let mut out = self.inner.format();
716        for hint in &self.hints {
717            out.push_str(&format!("\n  help: {}", hint.description()));
718        }
719        out
720    }
721}
722/// Error tagging for categorisation.
723#[allow(dead_code)]
724#[allow(missing_docs)]
725#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
726pub enum ErrorTag {
727    Syntax,
728    Type,
729    Name,
730    Import,
731    Layout,
732    Unicode,
733    Overflow,
734    Internal,
735}
736/// An error filter: suppresses errors matching certain patterns.
737#[allow(dead_code)]
738#[allow(missing_docs)]
739pub struct ErrorFilter {
740    suppressed_codes: std::collections::HashSet<String>,
741    min_severity: ErrorSeverityLevel,
742}
743impl ErrorFilter {
744    #[allow(dead_code)]
745    #[allow(missing_docs)]
746    pub fn new(min_severity: ErrorSeverityLevel) -> Self {
747        Self {
748            suppressed_codes: std::collections::HashSet::new(),
749            min_severity,
750        }
751    }
752    #[allow(dead_code)]
753    #[allow(missing_docs)]
754    pub fn suppress_code(&mut self, code: impl Into<String>) {
755        self.suppressed_codes.insert(code.into());
756    }
757    #[allow(dead_code)]
758    #[allow(missing_docs)]
759    pub fn should_show(&self, e: &RichError) -> bool {
760        if e.severity < self.min_severity {
761            return false;
762        }
763        if let Some(code) = &e.code {
764            if self.suppressed_codes.contains(code.as_str()) {
765                return false;
766            }
767        }
768        true
769    }
770    #[allow(dead_code)]
771    #[allow(missing_docs)]
772    pub fn filter<'a>(&self, errors: &'a [RichError]) -> Vec<&'a RichError> {
773        errors.iter().filter(|e| self.should_show(e)).collect()
774    }
775}
776/// Controls how the parser recovers from errors.
777#[derive(Clone, Copy, Debug, PartialEq, Eq)]
778#[allow(missing_docs)]
779pub enum RecoveryStrategy {
780    /// Abort parsing immediately on the first error.
781    Abort,
782    /// Skip tokens until a synchronization point (e.g., `def`, `theorem`).
783    SkipToSync,
784    /// Insert a synthetic token and continue.
785    InsertToken,
786    /// Replace the offending token with a placeholder and continue.
787    Replace,
788}
789impl RecoveryStrategy {
790    /// Whether this strategy allows parsing to continue.
791    #[allow(missing_docs)]
792    pub fn continues(&self) -> bool {
793        !matches!(self, RecoveryStrategy::Abort)
794    }
795}
796/// Aggregate statistics about parse errors.
797#[derive(Clone, Debug, Default)]
798#[allow(missing_docs)]
799pub struct ParseErrorStats {
800    /// Total number of errors.
801    pub total: u64,
802    /// Errors at line 0 (synthetic/EOF errors).
803    pub eof_errors: u64,
804    /// Errors with known location.
805    #[allow(missing_docs)]
806    pub located_errors: u64,
807}
808impl ParseErrorStats {
809    /// Create zero-initialized stats.
810    #[allow(missing_docs)]
811    pub fn new() -> Self {
812        Self::default()
813    }
814    /// Record an error.
815    #[allow(missing_docs)]
816    pub fn record(&mut self, err: &ParseError) {
817        self.total += 1;
818        if err.span.line == 0 {
819            self.eof_errors += 1;
820        } else {
821            self.located_errors += 1;
822        }
823    }
824    /// Whether any errors were recorded.
825    #[allow(missing_docs)]
826    pub fn has_errors(&self) -> bool {
827        self.total > 0
828    }
829}
830/// An error budget: stop reporting after too many errors.
831#[allow(dead_code)]
832#[allow(missing_docs)]
833pub struct ErrorBudget {
834    budget: usize,
835    spent: usize,
836}
837impl ErrorBudget {
838    #[allow(dead_code)]
839    #[allow(missing_docs)]
840    pub fn new(budget: usize) -> Self {
841        Self { budget, spent: 0 }
842    }
843    #[allow(dead_code)]
844    #[allow(missing_docs)]
845    pub fn spend(&mut self) -> bool {
846        if self.spent < self.budget {
847            self.spent += 1;
848            true
849        } else {
850            false
851        }
852    }
853    #[allow(dead_code)]
854    #[allow(missing_docs)]
855    pub fn remaining(&self) -> usize {
856        self.budget.saturating_sub(self.spent)
857    }
858    #[allow(dead_code)]
859    #[allow(missing_docs)]
860    pub fn is_exhausted(&self) -> bool {
861        self.spent >= self.budget
862    }
863    #[allow(dead_code)]
864    #[allow(missing_docs)]
865    pub fn fraction_used(&self) -> f64 {
866        if self.budget == 0 {
867            1.0
868        } else {
869            self.spent as f64 / self.budget as f64
870        }
871    }
872}
873/// Accumulates errors and warnings over a parse session.
874#[allow(dead_code)]
875#[allow(missing_docs)]
876pub struct ErrorAccumulator2 {
877    errors: Vec<RichError>,
878    max_errors: usize,
879    fatal_encountered: bool,
880}
881impl ErrorAccumulator2 {
882    #[allow(dead_code)]
883    #[allow(missing_docs)]
884    pub fn new(max_errors: usize) -> Self {
885        Self {
886            errors: Vec::new(),
887            max_errors,
888            fatal_encountered: false,
889        }
890    }
891    #[allow(dead_code)]
892    #[allow(missing_docs)]
893    pub fn add(&mut self, e: RichError) -> bool {
894        if self.errors.len() >= self.max_errors {
895            return false;
896        }
897        if e.is_fatal() {
898            self.fatal_encountered = true;
899        }
900        self.errors.push(e);
901        true
902    }
903    #[allow(dead_code)]
904    #[allow(missing_docs)]
905    pub fn error_count(&self) -> usize {
906        self.errors
907            .iter()
908            .filter(|e| e.severity >= ErrorSeverityLevel::Error)
909            .count()
910    }
911    #[allow(dead_code)]
912    #[allow(missing_docs)]
913    pub fn warning_count(&self) -> usize {
914        self.errors
915            .iter()
916            .filter(|e| e.severity == ErrorSeverityLevel::Warning)
917            .count()
918    }
919    #[allow(dead_code)]
920    #[allow(missing_docs)]
921    pub fn has_fatal(&self) -> bool {
922        self.fatal_encountered
923    }
924    #[allow(dead_code)]
925    #[allow(missing_docs)]
926    pub fn is_clean(&self) -> bool {
927        self.errors.is_empty()
928    }
929    #[allow(dead_code)]
930    #[allow(missing_docs)]
931    pub fn all_errors(&self) -> &[RichError] {
932        &self.errors
933    }
934    #[allow(dead_code)]
935    #[allow(missing_docs)]
936    pub fn format_all(&self) -> String {
937        self.errors
938            .iter()
939            .map(|e| e.format())
940            .collect::<Vec<_>>()
941            .join("\n")
942    }
943    #[allow(dead_code)]
944    #[allow(missing_docs)]
945    pub fn sorted_by_severity(&self) -> Vec<&RichError> {
946        let mut sorted: Vec<_> = self.errors.iter().collect();
947        sorted.sort_by(|a, b| b.severity.cmp(&a.severity));
948        sorted
949    }
950}
951/// A rich error message with optional suggestions.
952#[allow(dead_code)]
953#[allow(missing_docs)]
954#[derive(Clone, Debug)]
955pub struct RichError {
956    pub message: String,
957    pub severity: ErrorSeverityLevel,
958    pub code: Option<String>,
959    pub suggestions: Vec<String>,
960    #[allow(missing_docs)]
961    pub notes: Vec<String>,
962    pub span_start: usize,
963    pub span_end: usize,
964}
965impl RichError {
966    #[allow(dead_code)]
967    #[allow(missing_docs)]
968    pub fn error(msg: impl Into<String>, start: usize, end: usize) -> Self {
969        Self {
970            message: msg.into(),
971            severity: ErrorSeverityLevel::Error,
972            code: None,
973            suggestions: Vec::new(),
974            notes: Vec::new(),
975            span_start: start,
976            span_end: end,
977        }
978    }
979    #[allow(dead_code)]
980    #[allow(missing_docs)]
981    pub fn warning(msg: impl Into<String>, start: usize, end: usize) -> Self {
982        Self {
983            severity: ErrorSeverityLevel::Warning,
984            ..Self::error(msg, start, end)
985        }
986    }
987    #[allow(dead_code)]
988    #[allow(missing_docs)]
989    pub fn with_code(mut self, code: impl Into<String>) -> Self {
990        self.code = Some(code.into());
991        self
992    }
993    #[allow(dead_code)]
994    #[allow(missing_docs)]
995    pub fn with_suggestion(mut self, s: impl Into<String>) -> Self {
996        self.suggestions.push(s.into());
997        self
998    }
999    #[allow(dead_code)]
1000    #[allow(missing_docs)]
1001    pub fn with_note(mut self, n: impl Into<String>) -> Self {
1002        self.notes.push(n.into());
1003        self
1004    }
1005    #[allow(dead_code)]
1006    #[allow(missing_docs)]
1007    pub fn is_fatal(&self) -> bool {
1008        self.severity >= ErrorSeverityLevel::Fatal
1009    }
1010    #[allow(dead_code)]
1011    #[allow(missing_docs)]
1012    pub fn span_len(&self) -> usize {
1013        self.span_end.saturating_sub(self.span_start)
1014    }
1015    #[allow(dead_code)]
1016    #[allow(missing_docs)]
1017    pub fn format(&self) -> String {
1018        let mut out = format!("[{}] {}", self.severity, self.message);
1019        if let Some(code) = &self.code {
1020            out = format!("[{}][{}] {}", code, self.severity, self.message);
1021        }
1022        for sug in &self.suggestions {
1023            out.push_str(&format!("\n  suggestion: {}", sug));
1024        }
1025        for note in &self.notes {
1026            out.push_str(&format!("\n  note: {}", note));
1027        }
1028        out
1029    }
1030}
1031/// Renders parse errors with surrounding source context.
1032#[allow(missing_docs)]
1033pub struct ParseErrorFormatter<'a> {
1034    /// Source text.
1035    pub src: &'a str,
1036    /// Source file name.
1037    pub filename: &'a str,
1038    /// Number of context lines above/below the error.
1039    #[allow(missing_docs)]
1040    pub context_lines: usize,
1041}
1042impl<'a> ParseErrorFormatter<'a> {
1043    /// Create a formatter with default context (2 lines).
1044    #[allow(missing_docs)]
1045    pub fn new(src: &'a str, filename: &'a str) -> Self {
1046        Self {
1047            src,
1048            filename,
1049            context_lines: 2,
1050        }
1051    }
1052    /// Set the number of context lines.
1053    #[allow(missing_docs)]
1054    pub fn with_context(mut self, n: usize) -> Self {
1055        self.context_lines = n;
1056        self
1057    }
1058    /// Format a `ParseError` into a human-readable string.
1059    #[allow(missing_docs)]
1060    pub fn format(&self, err: &ParseError) -> String {
1061        let line = err.span.line;
1062        let col = err.span.column;
1063        let lines: Vec<&str> = self.src.lines().collect();
1064        let mut out = format!(
1065            "{}:{}:{}: error: {:?}\n",
1066            self.filename, line, col, err.kind
1067        );
1068        if line > 0 && line <= lines.len() {
1069            let start = line.saturating_sub(1 + self.context_lines);
1070            let end = (line + self.context_lines).min(lines.len());
1071            for (i, ln) in lines[start..end].iter().enumerate() {
1072                let lineno = start + i + 1;
1073                out.push_str(&format!("{:>4} | {}\n", lineno, ln));
1074                if lineno == line {
1075                    let spaces = " ".repeat(col.saturating_sub(1));
1076                    out.push_str(&format!("     | {}^\n", spaces));
1077                }
1078            }
1079        }
1080        out
1081    }
1082    /// Format all errors in a collector.
1083    #[allow(missing_docs)]
1084    pub fn format_all(&self, collector: &ParseErrorCollector) -> String {
1085        collector
1086            .errors()
1087            .iter()
1088            .map(|e| self.format(e))
1089            .collect::<Vec<_>>()
1090            .join("\n")
1091    }
1092}
1093/// An aggregated error report combining errors, warnings, and notes.
1094#[allow(dead_code)]
1095#[allow(missing_docs)]
1096#[derive(Clone, Debug, Default)]
1097pub struct ParseErrorReport {
1098    /// File name.
1099    pub filename: String,
1100    /// All diagnostics.
1101    pub diagnostics: Vec<ParseDiagnostic>,
1102}
1103impl ParseErrorReport {
1104    /// Create an empty report for a file.
1105    #[allow(dead_code)]
1106    #[allow(missing_docs)]
1107    pub fn new(filename: &str) -> Self {
1108        Self {
1109            filename: filename.to_string(),
1110            diagnostics: Vec::new(),
1111        }
1112    }
1113    /// Add a diagnostic.
1114    #[allow(dead_code)]
1115    #[allow(missing_docs)]
1116    pub fn add(&mut self, d: ParseDiagnostic) {
1117        self.diagnostics.push(d);
1118    }
1119    /// Number of hard errors.
1120    #[allow(dead_code)]
1121    #[allow(missing_docs)]
1122    pub fn error_count(&self) -> usize {
1123        self.diagnostics.iter().filter(|d| d.is_error()).count()
1124    }
1125    /// Number of warnings.
1126    #[allow(dead_code)]
1127    #[allow(missing_docs)]
1128    pub fn warning_count(&self) -> usize {
1129        self.diagnostics
1130            .iter()
1131            .filter(|d| d.severity == ErrorSeverity::Warning)
1132            .count()
1133    }
1134    /// Whether the report contains no errors.
1135    #[allow(dead_code)]
1136    #[allow(missing_docs)]
1137    pub fn is_clean(&self) -> bool {
1138        self.error_count() == 0
1139    }
1140    /// Return all errors.
1141    #[allow(dead_code)]
1142    #[allow(missing_docs)]
1143    pub fn errors(&self) -> Vec<&ParseDiagnostic> {
1144        errors_only(&self.diagnostics)
1145    }
1146    /// Return all warnings.
1147    #[allow(dead_code)]
1148    #[allow(missing_docs)]
1149    pub fn warnings(&self) -> Vec<&ParseDiagnostic> {
1150        warnings_only(&self.diagnostics)
1151    }
1152}
1153/// Error chain: a sequence of related errors.
1154#[allow(dead_code)]
1155#[allow(missing_docs)]
1156pub struct ErrorChain {
1157    chain: Vec<RichError>,
1158}
1159impl ErrorChain {
1160    #[allow(dead_code)]
1161    #[allow(missing_docs)]
1162    pub fn new(root: RichError) -> Self {
1163        Self { chain: vec![root] }
1164    }
1165    #[allow(dead_code)]
1166    #[allow(missing_docs)]
1167    pub fn caused_by(mut self, e: RichError) -> Self {
1168        self.chain.push(e);
1169        self
1170    }
1171    #[allow(dead_code)]
1172    #[allow(missing_docs)]
1173    pub fn root(&self) -> &RichError {
1174        &self.chain[0]
1175    }
1176    #[allow(dead_code)]
1177    #[allow(missing_docs)]
1178    pub fn len(&self) -> usize {
1179        self.chain.len()
1180    }
1181    #[allow(dead_code)]
1182    #[allow(missing_docs)]
1183    pub fn is_empty(&self) -> bool {
1184        self.chain.is_empty()
1185    }
1186    #[allow(dead_code)]
1187    #[allow(missing_docs)]
1188    pub fn format_chain(&self) -> String {
1189        self.chain
1190            .iter()
1191            .enumerate()
1192            .map(|(i, e)| {
1193                if i == 0 {
1194                    e.format()
1195                } else {
1196                    format!("  caused by: {}", e.message)
1197                }
1198            })
1199            .collect::<Vec<_>>()
1200            .join("\n")
1201    }
1202}
1203/// Tracks parse errors against a budget; aborts when exceeded.
1204#[derive(Clone, Debug)]
1205#[allow(missing_docs)]
1206pub struct ParseErrorBudget {
1207    /// Total budget.
1208    pub budget: usize,
1209    /// Remaining budget.
1210    pub remaining: usize,
1211}
1212impl ParseErrorBudget {
1213    /// Create a budget.
1214    #[allow(missing_docs)]
1215    pub fn new(budget: usize) -> Self {
1216        Self {
1217            budget,
1218            remaining: budget,
1219        }
1220    }
1221    /// Consume one error token. Returns `true` if budget remains.
1222    #[allow(missing_docs)]
1223    pub fn consume(&mut self) -> bool {
1224        if self.remaining > 0 {
1225            self.remaining -= 1;
1226            true
1227        } else {
1228            false
1229        }
1230    }
1231    /// Whether the budget has been exhausted.
1232    #[allow(missing_docs)]
1233    pub fn is_exhausted(&self) -> bool {
1234        self.remaining == 0
1235    }
1236    /// Number of errors consumed.
1237    #[allow(missing_docs)]
1238    pub fn consumed(&self) -> usize {
1239        self.budget - self.remaining
1240    }
1241    /// Reset the budget to its initial value.
1242    #[allow(missing_docs)]
1243    pub fn reset(&mut self) {
1244        self.remaining = self.budget;
1245    }
1246}
1247/// Error code catalogue with descriptions.
1248#[allow(dead_code)]
1249#[allow(missing_docs)]
1250pub struct ErrorCodeCatalogue {
1251    entries: std::collections::HashMap<String, String>,
1252}
1253impl ErrorCodeCatalogue {
1254    #[allow(dead_code)]
1255    #[allow(missing_docs)]
1256    pub fn new() -> Self {
1257        let mut cat = Self {
1258            entries: std::collections::HashMap::new(),
1259        };
1260        cat.add("E0001", "unexpected token");
1261        cat.add("E0002", "missing closing bracket");
1262        cat.add("E0003", "expected identifier");
1263        cat.add("E0004", "invalid escape sequence");
1264        cat.add("E0005", "unterminated string literal");
1265        cat.add("E0006", "unexpected end of file");
1266        cat.add("E0007", "integer literal too large");
1267        cat.add("E0008", "invalid character");
1268        cat.add("E0009", "indentation error");
1269        cat.add("E0010", "ambiguous parse");
1270        cat
1271    }
1272    #[allow(dead_code)]
1273    #[allow(missing_docs)]
1274    pub fn add(&mut self, code: impl Into<String>, desc: impl Into<String>) {
1275        self.entries.insert(code.into(), desc.into());
1276    }
1277    #[allow(dead_code)]
1278    #[allow(missing_docs)]
1279    pub fn description(&self, code: &str) -> Option<&str> {
1280        self.entries.get(code).map(|s| s.as_str())
1281    }
1282    #[allow(dead_code)]
1283    #[allow(missing_docs)]
1284    pub fn count(&self) -> usize {
1285        self.entries.len()
1286    }
1287}
1288/// Maps error codes to their suggested quick-fix actions.
1289#[allow(dead_code)]
1290#[allow(missing_docs)]
1291pub struct QuickFixRegistry {
1292    fixes: std::collections::HashMap<String, Vec<String>>,
1293}
1294impl QuickFixRegistry {
1295    #[allow(dead_code)]
1296    #[allow(missing_docs)]
1297    pub fn new() -> Self {
1298        Self {
1299            fixes: std::collections::HashMap::new(),
1300        }
1301    }
1302    #[allow(dead_code)]
1303    #[allow(missing_docs)]
1304    pub fn register(&mut self, code: impl Into<String>, fix: impl Into<String>) {
1305        self.fixes.entry(code.into()).or_default().push(fix.into());
1306    }
1307    #[allow(dead_code)]
1308    #[allow(missing_docs)]
1309    pub fn fixes_for(&self, code: &str) -> &[String] {
1310        self.fixes.get(code).map(|v| v.as_slice()).unwrap_or(&[])
1311    }
1312    #[allow(dead_code)]
1313    #[allow(missing_docs)]
1314    pub fn has_fixes(&self, code: &str) -> bool {
1315        !self.fixes_for(code).is_empty()
1316    }
1317    #[allow(dead_code)]
1318    #[allow(missing_docs)]
1319    pub fn total_codes(&self) -> usize {
1320        self.fixes.len()
1321    }
1322}
1323/// Error deduplication: suppress repeated identical messages.
1324#[allow(dead_code)]
1325#[allow(missing_docs)]
1326pub struct ErrorDeduplicator {
1327    seen: std::collections::HashSet<String>,
1328    suppressed: usize,
1329}
1330impl ErrorDeduplicator {
1331    #[allow(dead_code)]
1332    #[allow(missing_docs)]
1333    pub fn new() -> Self {
1334        Self {
1335            seen: std::collections::HashSet::new(),
1336            suppressed: 0,
1337        }
1338    }
1339    #[allow(dead_code)]
1340    #[allow(missing_docs)]
1341    pub fn should_emit(&mut self, key: &str) -> bool {
1342        if self.seen.contains(key) {
1343            self.suppressed += 1;
1344            false
1345        } else {
1346            self.seen.insert(key.to_string());
1347            true
1348        }
1349    }
1350    #[allow(dead_code)]
1351    #[allow(missing_docs)]
1352    pub fn suppressed_count(&self) -> usize {
1353        self.suppressed
1354    }
1355    #[allow(dead_code)]
1356    #[allow(missing_docs)]
1357    pub fn unique_count(&self) -> usize {
1358        self.seen.len()
1359    }
1360}
1361/// An error with its source context pre-rendered.
1362#[allow(dead_code)]
1363#[allow(missing_docs)]
1364pub struct ContextualRichError {
1365    pub error: RichError,
1366    pub context_snippet: String,
1367    pub file: String,
1368    pub line: usize,
1369    #[allow(missing_docs)]
1370    pub column: usize,
1371}
1372impl ContextualRichError {
1373    #[allow(dead_code)]
1374    #[allow(missing_docs)]
1375    pub fn new(error: RichError, source: &str, file: impl Into<String>) -> Self {
1376        let resolver = ErrorLocationResolver::new(source);
1377        let (line, col) = resolver.resolve(error.span_start);
1378        let snippet = resolver.snippet(error.span_start, 1);
1379        Self {
1380            error,
1381            context_snippet: snippet,
1382            file: file.into(),
1383            line,
1384            column: col,
1385        }
1386    }
1387    #[allow(dead_code)]
1388    #[allow(missing_docs)]
1389    pub fn format_full(&self) -> String {
1390        format!(
1391            "{}:{}:{}: {}\n{}",
1392            self.file,
1393            self.line + 1,
1394            self.column + 1,
1395            self.error.format(),
1396            self.context_snippet
1397        )
1398    }
1399}