aiken_lang/parser/
error.rs

1use crate::{
2    ast::{CurveType, Span},
3    parser::token::Token,
4};
5use indoc::formatdoc;
6use itertools::Itertools;
7use miette::Diagnostic;
8use owo_colors::{OwoColorize, Stream::Stdout};
9use std::collections::HashSet;
10
11#[derive(Debug, Clone, Diagnostic, thiserror::Error)]
12#[error("{kind}\n")]
13#[diagnostic(
14    help(
15        "{}",
16        match kind {
17            ErrorKind::Unexpected(..) if !expected.is_empty() => {
18                format!(
19                    "I am looking for one of the following patterns:\n{}",
20                    expected
21                        .iter()
22                        .sorted()
23                        .map(|x| format!(
24                            "→ {}",
25                            x.to_aiken()
26                             .if_supports_color(Stdout, |s| s.purple())
27                        ))
28                        .collect::<Vec<_>>()
29                        .join("\n")
30                )
31            },
32            _ => {
33                kind.help().map(|x| x.to_string()).unwrap_or_default()
34            }
35        }
36    )
37)]
38pub struct ParseError {
39    pub kind: ErrorKind,
40    #[label("{}", .label.unwrap_or_default())]
41    pub span: Span,
42    #[allow(dead_code)]
43    while_parsing: Option<(Span, &'static str)>,
44    expected: HashSet<Pattern>,
45    label: Option<&'static str>,
46}
47
48impl ParseError {
49    pub fn merge(mut self, other: Self) -> Self {
50        // TODO: Use HashSet
51        for expected in other.expected.into_iter() {
52            self.expected.insert(expected);
53        }
54        self
55    }
56
57    pub fn expected_but_got(expected: Pattern, got: Pattern, span: Span) -> Self {
58        Self {
59            kind: ErrorKind::Unexpected(got),
60            expected: HashSet::from_iter([expected]),
61            span,
62            while_parsing: None,
63            label: None,
64        }
65    }
66
67    pub fn invalid_assignment_right_hand_side(span: Span) -> Self {
68        Self {
69            kind: ErrorKind::UnfinishedAssignmentRightHandSide,
70            span,
71            while_parsing: None,
72            expected: HashSet::new(),
73            label: Some("invalid assignment right-hand side"),
74        }
75    }
76
77    pub fn invalid_tuple_index(span: Span, index: String, suffix: Option<String>) -> Self {
78        let hint = suffix.map(|suffix| format!("Did you mean '{index}{suffix}'?"));
79        Self {
80            kind: ErrorKind::InvalidTupleIndex { hint },
81            span,
82            while_parsing: None,
83            expected: HashSet::new(),
84            label: None,
85        }
86    }
87
88    pub fn deprecated_when_clause_guard(span: Span) -> Self {
89        Self {
90            kind: ErrorKind::DeprecatedWhenClause,
91            span,
92            while_parsing: None,
93            expected: HashSet::new(),
94            label: Some("deprecated"),
95        }
96    }
97
98    pub fn point_not_on_curve(curve: CurveType, span: Span) -> Self {
99        Self {
100            kind: ErrorKind::PointNotOnCurve { curve },
101            span,
102            while_parsing: None,
103            expected: HashSet::new(),
104            label: Some("out off curve"),
105        }
106    }
107
108    pub fn unknown_point_curve(curve: String, point: Option<String>, span: Span) -> Self {
109        let label = if point.is_some() {
110            Some("unknown curve")
111        } else {
112            Some("unknown point")
113        };
114
115        Self {
116            kind: ErrorKind::UnknownCurvePoint { curve, point },
117            span,
118            while_parsing: None,
119            expected: HashSet::new(),
120            label,
121        }
122    }
123
124    pub fn malformed_base16_string_literal(span: Span) -> Self {
125        Self {
126            kind: ErrorKind::MalformedBase16StringLiteral,
127            span,
128            while_parsing: None,
129            expected: HashSet::new(),
130            label: None,
131        }
132    }
133
134    pub fn malformed_base16_digits(span: Span) -> Self {
135        Self {
136            kind: ErrorKind::MalformedBase16Digits,
137            span,
138            while_parsing: None,
139            expected: HashSet::new(),
140            label: None,
141        }
142    }
143
144    pub fn hybrid_notation_in_bytearray(span: Span) -> Self {
145        Self {
146            kind: ErrorKind::HybridNotationInByteArray,
147            span,
148            while_parsing: None,
149            expected: HashSet::new(),
150            label: None,
151        }
152    }
153
154    pub fn match_on_curve(span: Span) -> Self {
155        Self {
156            kind: ErrorKind::PatternMatchOnCurvePoint,
157            span,
158            while_parsing: None,
159            expected: HashSet::new(),
160            label: Some("cannot pattern-match on curve point"),
161        }
162    }
163
164    pub fn match_string(span: Span) -> Self {
165        Self {
166            kind: ErrorKind::PatternMatchOnString,
167            span,
168            while_parsing: None,
169            expected: HashSet::new(),
170            label: Some("cannot pattern-match on string"),
171        }
172    }
173}
174
175impl PartialEq for ParseError {
176    fn eq(&self, other: &Self) -> bool {
177        self.kind == other.kind && self.span == other.span && self.label == other.label
178    }
179}
180
181impl<T: Into<Pattern>> chumsky::Error<T> for ParseError {
182    type Span = Span;
183
184    type Label = &'static str;
185
186    fn expected_input_found<Iter: IntoIterator<Item = Option<T>>>(
187        span: Self::Span,
188        expected: Iter,
189        found: Option<T>,
190    ) -> Self {
191        Self {
192            kind: found
193                .map(Into::into)
194                .map(ErrorKind::Unexpected)
195                .unwrap_or(ErrorKind::UnexpectedEnd),
196            span,
197            while_parsing: None,
198            expected: expected
199                .into_iter()
200                .map(|x| x.map(Into::into).unwrap_or(Pattern::End))
201                .collect(),
202            label: None,
203        }
204    }
205
206    fn with_label(mut self, label: Self::Label) -> Self {
207        self.label.get_or_insert(label);
208        self
209    }
210
211    fn merge(self, other: Self) -> Self {
212        ParseError::merge(self, other)
213    }
214}
215
216#[derive(Debug, Clone, PartialEq, Eq, Diagnostic, thiserror::Error)]
217pub enum ErrorKind {
218    #[error("I arrived at the end of the file unexpectedly.")]
219    UnexpectedEnd,
220
221    #[error("{0}")]
222    #[diagnostic(help("{}", .0.help().unwrap_or_else(|| Box::new("")))) ]
223    Unexpected(Pattern),
224
225    #[error("I discovered an invalid tuple index.")]
226    #[diagnostic()]
227    InvalidTupleIndex {
228        #[help]
229        hint: Option<String>,
230    },
231
232    #[error("I spotted an unfinished assignment.")]
233    #[diagnostic(
234        help(
235            "{} and {} bindings must be followed by a valid, complete, expression.",
236            "let".if_supports_color(Stdout, |s| s.yellow()),
237            "expect".if_supports_color(Stdout, |s| s.yellow()),
238        ),
239    )]
240    UnfinishedAssignmentRightHandSide,
241
242    #[error("I tripped over a {}", fmt_curve_type(.curve))]
243    PointNotOnCurve { curve: CurveType },
244
245    #[error("I tripped over a {}", fmt_unknown_curve(.curve, .point))]
246    UnknownCurvePoint {
247        curve: String,
248        point: Option<String>,
249    },
250
251    #[error("I tripped over a malformed hexadecimal digits.")]
252    #[diagnostic(help("{}", formatdoc! {
253        r#"When numbers starts with '0x', they are treated as hexadecimal numbers. Thus, only digits from 0-9 or letter from a-f (or A-F) can be used following a '0x' number declaration. Plus, hexadecimal digits always go by pairs, so the total number of digits must be even (not counting leading zeros)."#
254    }))]
255    MalformedBase16Digits,
256
257    #[error("I tripped over a malformed base16-encoded string literal.")]
258    #[diagnostic(help("{}", formatdoc! {
259        r#"You can declare literal bytearrays from base16-encoded (a.k.a. hexadecimal) string literals.
260
261           For example:
262
263             ┍━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
264             │ {} my_policy_id {}
265             │   #{}
266        "#,
267        "pub const".if_supports_color(Stdout, |s| s.bright_blue()),
268        "=".if_supports_color(Stdout, |s| s.yellow()),
269        "\"f4c9f9c4252d86702c2f4c2e49e6648c7cffe3c8f2b6b7d779788f50\""
270            .if_supports_color(Stdout, |s| s.bright_purple())
271    }))]
272    MalformedBase16StringLiteral,
273
274    #[error("I came across a bytearray declared using two different notations.")]
275    #[diagnostic(url("https://aiken-lang.org/language-tour/primitive-types#bytearray"))]
276    #[diagnostic(help("Either use decimal or hexadecimal notation, but don't mix them."))]
277    HybridNotationInByteArray,
278
279    #[error("I found a now-deprecated clause guard in a when/is expression.")]
280    #[diagnostic(help("{}", formatdoc! {
281        r#"Clause guards have been removed from Aiken. They were underused, considered potentially harmful and created needless complexity in the compiler. If you were using clause guards, our apologies, but you can now update your code and move the clause guards patterns inside a nested if/else expression.
282        "#
283    }))]
284    DeprecatedWhenClause,
285
286    #[error("I choked on a curve point in a bytearray pattern.")]
287    #[diagnostic(help(
288        "You can pattern-match on bytearrays just fine, but not on G1 nor G2 elements. Use if/else with an equality if you have to compare those."
289    ))]
290    PatternMatchOnCurvePoint,
291
292    #[error("I refuse to cooperate and match a utf-8 string.")]
293    #[diagnostic(help(
294        "You can pattern-match on bytearrays but not on strings. Note that I can parse utf-8 encoded bytearrays just fine, so you probably want to drop the extra '@' and only manipulate bytearrays wherever you need to. On-chain, strings shall be avoided as much as possible."
295    ))]
296    PatternMatchOnString,
297}
298
299fn fmt_curve_type(curve: &CurveType) -> String {
300    match curve {
301        CurveType::Bls12_381(point) => {
302            format!("{point} point that is not in the bls12_381 curve")
303        }
304    }
305}
306
307fn fmt_unknown_curve(curve: &String, point: &Option<String>) -> String {
308    match point {
309        Some(point) => {
310            format!(
311                "{} which is an unknown point for curve {}",
312                point.if_supports_color(Stdout, |s| s.purple()),
313                curve.if_supports_color(Stdout, |s| s.purple()),
314            )
315        }
316        None => {
317            format!(
318                "{} which is an unknown curve",
319                curve.if_supports_color(Stdout, |s| s.purple())
320            )
321        }
322    }
323}
324
325#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Diagnostic, thiserror::Error)]
326pub enum Pattern {
327    #[error("I found an unexpected char '{0:?}'.")]
328    #[diagnostic(help("Try removing it!"))]
329    Char(char),
330    #[error("I found an unexpected token '{0}'.")]
331    #[diagnostic(help("Try removing it!"))]
332    Token(Token),
333    #[error("I found an unexpected end of input.")]
334    End,
335    #[error("I found a malformed list spread pattern.")]
336    #[diagnostic(help("List spread in matches can use a discard '_' or var."))]
337    Match,
338    #[error("I found an out-of-bound byte literal.")]
339    #[diagnostic(help("Bytes must be between 0-255."))]
340    Byte,
341    #[error("I found an unexpected label.")]
342    #[diagnostic(help("You can only use labels surrounded by curly braces"))]
343    Label,
344    #[error("I found an unexpected discard '_'.")]
345    #[diagnostic(help("You can only use capture syntax with functions not constructors."))]
346    Discard,
347}
348
349impl Pattern {
350    fn to_aiken(&self) -> String {
351        use Pattern::*;
352        match self {
353            Token(tok) => tok.to_string(),
354            Char(c) => c.to_string(),
355            End => "<END OF FILE>".to_string(),
356            Match => "A pattern (a discard, a var, etc...)".to_string(),
357            Byte => "A byte between [0; 255]".to_string(),
358            Label => "A label".to_string(),
359            Discard => "_".to_string(),
360        }
361    }
362}
363
364impl From<char> for Pattern {
365    fn from(c: char) -> Self {
366        Self::Char(c)
367    }
368}
369
370impl From<Token> for Pattern {
371    fn from(tok: Token) -> Self {
372        Self::Token(tok)
373    }
374}