Skip to main content

microcad_syntax/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::Span;
7use crate::parser::error::rich::RichPattern;
8use crate::tokens::Token;
9use miette::{Diagnostic, LabeledSpan};
10pub use rich::{Rich, RichReason};
11use std::error::Error;
12use std::fmt::{Display, Formatter};
13use std::iter::once;
14use thiserror::Error;
15
16/// An error from building the abstract syntax tree
17#[derive(Debug)]
18pub struct ParseError {
19    /// The span of the source that caused the error
20    pub span: Span,
21    error: Rich<'static, Token<'static>, Span, ParseErrorKind>,
22}
23
24impl ParseError {
25    pub(crate) fn new<'tokens>(error: Rich<'tokens, Token<'tokens>, Span, ParseErrorKind>) -> Self {
26        Self {
27            span: error.span().clone(),
28            error: error.map_token(Token::into_owned).into_owned(),
29        }
30    }
31}
32
33#[derive(Debug, Error, Clone, Diagnostic)]
34pub enum ParseErrorKind {
35    #[error("'{0}' is a reserved keyword")]
36    ReservedKeyword(&'static str),
37    #[error("'{0}' is a reserved keyword and can't be used as an identifier")]
38    ReservedKeywordAsIdentifier(&'static str),
39    #[error("'{0}' is a keyword and can't be used as an identifier")]
40    KeywordAsIdentifier(&'static str),
41    #[error("unclosed string")]
42    UnterminatedString,
43    #[error("Unclosed {kind}")]
44    UnclosedBracket {
45        #[label("{kind} opened here")]
46        open: Span,
47        #[label("expected {kind} to be closed by here with a '{close_token}'")]
48        end: Span,
49        kind: &'static str,
50        close_token: Token<'static>,
51    },
52}
53
54impl Display for ParseError {
55    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
56        match self.error.reason() {
57            RichReason::Custom(error) => write!(f, "{error}"),
58            RichReason::ExpectedFound { expected, .. } => {
59                write!(f, "Expected ")?;
60                let mut expected = expected.iter().filter(|pat| match pat {
61                    // don't show 'whitespace' as possible tokens, if there are also others
62                    RichPattern::Label(label) if expected.len() > 1 => label != "whitespace",
63                    _ => true,
64                });
65                if let Some(pattern) = expected.next() {
66                    write!(f, "{pattern}")?;
67                }
68                let last = expected.next_back();
69                for pattern in expected {
70                    write!(f, ", {pattern}")?;
71                }
72                if let Some(pattern) = last {
73                    write!(f, " or {pattern}")?;
74                }
75                Ok(())
76            }
77        }
78    }
79}
80
81impl Error for ParseError {}
82
83impl Diagnostic for ParseError {
84    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
85        match self.error.reason() {
86            RichReason::Custom(error) => error.help(),
87            _ => None,
88        }
89    }
90
91    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
92        let msg = match self.error.reason() {
93            RichReason::Custom(error) => {
94                if let Some(labels) = error.labels() {
95                    return Some(labels);
96                }
97                error.to_string()
98            }
99            RichReason::ExpectedFound {
100                found: Some(found), ..
101            } if found.is_error() => found.kind().into(),
102            RichReason::ExpectedFound {
103                found: Some(found), ..
104            } => format!("unexpected {}", found.kind()),
105            RichReason::ExpectedFound { found: None, .. } => "unexpected token".into(),
106        };
107        Some(Box::new(once(LabeledSpan::new(
108            Some(msg),
109            self.span.start,
110            self.span.len(),
111        ))))
112    }
113}