Skip to main content

atoxide_parser/chumsky/
error.rs

1//! Error types and ariadne-based error formatting for the chumsky parser.
2
3use ariadne::{Color, Label, Report, ReportKind, Source};
4use atoxide_lexer::{Span, Token};
5use chumsky::error::{Rich, RichPattern, RichReason};
6use chumsky::span::SimpleSpan;
7use std::fmt;
8use std::ops::Range;
9
10/// A parse error with context for display.
11#[derive(Debug, Clone)]
12pub struct ParseError {
13    /// The span where the error occurred.
14    pub span: Span,
15    /// The error message.
16    pub message: String,
17    /// What was expected at this position.
18    pub expected: Vec<String>,
19    /// What was found instead.
20    pub found: Option<String>,
21    /// Optional help text.
22    pub help: Option<String>,
23}
24
25impl ParseError {
26    /// Create a ParseError from a chumsky Rich error.
27    pub fn from_rich(error: &Rich<'_, Token, SimpleSpan>, _source: &str) -> Self {
28        let error_span = error.span();
29        let span = Span::new(error_span.start, error_span.end, 1, 1);
30
31        let expected: Vec<String> = error
32            .expected()
33            .map(|e| match e {
34                RichPattern::Token(t) => format!("{:?}", t.kind),
35                RichPattern::Label(l) => l.to_string(),
36                RichPattern::EndOfInput => "end of input".to_string(),
37                _ => "unknown".to_string(),
38            })
39            .collect();
40
41        let found = error.found().map(|t| format!("{:?}", t.kind));
42
43        let message = match error.reason() {
44            RichReason::ExpectedFound { .. } => {
45                if expected.is_empty() {
46                    "unexpected token".to_string()
47                } else if expected.len() == 1 {
48                    format!("expected {}", expected[0])
49                } else {
50                    format!("expected one of: {}", expected.join(", "))
51                }
52            }
53            RichReason::Custom(msg) => msg.to_string(),
54        };
55
56        ParseError {
57            span,
58            message,
59            expected,
60            found,
61            help: None,
62        }
63    }
64
65    /// Add help text to this error.
66    #[allow(dead_code)]
67    pub fn with_help(mut self, help: impl Into<String>) -> Self {
68        self.help = Some(help.into());
69        self
70    }
71}
72
73impl fmt::Display for ParseError {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        write!(f, "{}", self.message)?;
76        if let Some(found) = &self.found {
77            write!(f, ", found {}", found)?;
78        }
79        Ok(())
80    }
81}
82
83impl std::error::Error for ParseError {}
84
85/// Format a list of parse errors using ariadne for beautiful output.
86pub fn format_errors(errors: &[ParseError], source: &str, filename: &str) -> String {
87    let mut output = Vec::new();
88
89    for error in errors {
90        let report = Report::<(&str, Range<usize>)>::build(
91            ReportKind::Error,
92            (filename, error.span.start..error.span.end),
93        )
94        .with_message(&error.message)
95        .with_label(
96            Label::new((filename, error.span.start..error.span.end))
97                .with_message(if let Some(found) = &error.found {
98                    format!("found {} here", found)
99                } else {
100                    error.message.clone()
101                })
102                .with_color(Color::Red),
103        );
104
105        let report = if let Some(help) = &error.help {
106            report.with_help(help)
107        } else if !error.expected.is_empty() {
108            report.with_help(format!("expected: {}", error.expected.join(", ")))
109        } else {
110            report
111        };
112
113        let report = report.finish();
114
115        let mut buf = Vec::new();
116        report
117            .write((filename, Source::from(source)), &mut buf)
118            .unwrap();
119
120        output.extend(buf);
121    }
122
123    String::from_utf8(output).unwrap_or_else(|_| "Error formatting failed".to_string())
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn test_error_display() {
132        let error = ParseError {
133            span: Span::new(0, 5, 1, 1),
134            message: "expected identifier".to_string(),
135            expected: vec!["NAME".to_string()],
136            found: Some("Number".to_string()),
137            help: None,
138        };
139
140        let display = format!("{}", error);
141        assert!(display.contains("expected identifier"));
142        assert!(display.contains("Number"));
143    }
144
145    #[test]
146    fn test_format_errors() {
147        let errors = vec![ParseError {
148            span: Span::new(0, 5, 1, 1),
149            message: "unexpected token".to_string(),
150            expected: vec!["module".to_string()],
151            found: Some("Number".to_string()),
152            help: None,
153        }];
154
155        let output = format_errors(&errors, "12345\n", "test.ato");
156        assert!(output.contains("unexpected token"));
157    }
158}