Skip to main content

microcad_lang_parse/parser/error/
mod.rs

1// Copyright © 2026 The µcad authors <info@microcad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4mod rich;
5
6use crate::ParseContext;
7use crate::parser::error::rich::RichPattern;
8use crate::tokens::Token;
9use microcad_lang_base::{Diagnostics, Refer, Span};
10use miette::{Diagnostic, LabeledSpan};
11pub use rich::{Rich, RichReason};
12use std::error::Error;
13use std::fmt::{Display, Formatter};
14use std::iter::once;
15use thiserror::Error;
16
17/// Type alias for RichError
18pub type RichError<'tokens> = Rich<'tokens, Token<'tokens>, Span, ParseErrorKind>;
19
20/// An error from building the abstract syntax tree
21#[derive(Debug)]
22pub struct ParseError {
23    /// The span of the source that caused the error
24    pub span: Span,
25    error: RichError<'static>,
26}
27
28impl ParseError {
29    pub(crate) fn new<'tokens>(error: RichError<'tokens>) -> Self {
30        Self {
31            span: error.span().clone(),
32            error: error.map_token(Token::into_owned).into_owned(),
33        }
34    }
35}
36
37/// Parse error collection.
38#[derive(Debug, Error, derive_more::Deref, miette::Diagnostic)]
39pub struct ParseErrors(#[related] pub Vec<ParseError>);
40
41impl ParseErrors {
42    /// Convert parse errors to diagnostics
43    pub fn to_diagnostics(self, context: &ParseContext) -> Diagnostics {
44        let mut diag_list = Diagnostics::default();
45        use microcad_lang_base::{Diagnostic as D, PushDiag};
46
47        for err in self.0 {
48            let span = err.span.clone();
49            diag_list
50                .push_diag(D::Error(
51                    Refer::<miette::Report>::new(err.into(), context.src_ref(&span)).into(),
52                ))
53                .expect("Diag list must return no error");
54        }
55
56        diag_list
57    }
58}
59
60impl std::fmt::Display for ParseErrors {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        write!(f, "Found {} parse errors", self.0.len())
63    }
64}
65
66impl<'tokens> From<Vec<RichError<'tokens>>> for ParseErrors {
67    fn from(errors: Vec<RichError<'tokens>>) -> Self {
68        Self(errors.into_iter().map(ParseError::new).collect())
69    }
70}
71
72#[derive(Debug, Error, Clone, Diagnostic)]
73pub enum ParseErrorKind {
74    #[error("'{0}' is a reserved keyword")]
75    ReservedKeyword(&'static str),
76    #[error("'{0}' is a reserved keyword and can't be used as an identifier")]
77    ReservedKeywordAsIdentifier(&'static str),
78    #[error("'{0}' is a keyword and can't be used as an identifier")]
79    KeywordAsIdentifier(&'static str),
80    #[error("unclosed string")]
81    UnterminatedString,
82    #[error("Unclosed {kind}")]
83    UnclosedBracket {
84        #[label("{kind} opened here")]
85        open: Span,
86        #[label("expected {kind} to be closed by here with a '{close_token}'")]
87        end: Span,
88        kind: &'static str,
89        close_token: Token<'static>,
90    },
91    #[error("Expression statements need to have a trailing semicolon")]
92    ExpressionMissingSemicolon {
93        #[label("exprected a semicolon after this expression")]
94        span: Span,
95    },
96}
97
98impl Display for ParseError {
99    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
100        match self.error.reason() {
101            RichReason::Custom(error) => write!(f, "{error}"),
102            RichReason::ExpectedFound { expected, .. } => {
103                write!(f, "Expected ")?;
104                let mut expected = expected.iter().filter(|pat| match pat {
105                    // don't show 'whitespace' as possible tokens, if there are also others
106                    RichPattern::Label(label) if expected.len() > 1 => label != "whitespace",
107                    _ => true,
108                });
109                if let Some(pattern) = expected.next() {
110                    write!(f, "{pattern}")?;
111                }
112                let last = expected.next_back();
113                for pattern in expected {
114                    write!(f, ", {pattern}")?;
115                }
116                if let Some(pattern) = last {
117                    write!(f, " or {pattern}")?;
118                }
119                Ok(())
120            }
121        }
122    }
123}
124
125impl Error for ParseError {}
126
127impl Diagnostic for ParseError {
128    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
129        match self.error.reason() {
130            RichReason::Custom(error) => error.help(),
131            _ => None,
132        }
133    }
134
135    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
136        let msg = match self.error.reason() {
137            RichReason::Custom(error) => {
138                if let Some(labels) = error.labels() {
139                    return Some(labels);
140                }
141                error.to_string()
142            }
143            RichReason::ExpectedFound {
144                found: Some(found), ..
145            } if found.is_error() => found.kind().into(),
146            RichReason::ExpectedFound {
147                found: Some(found), ..
148            } => format!("unexpected {}", found.kind()),
149            RichReason::ExpectedFound { found: None, .. } => "unexpected token".into(),
150        };
151        Some(Box::new(once(LabeledSpan::new(
152            Some(msg),
153            self.span.start,
154            self.span.len(),
155        ))))
156    }
157}