Skip to main content

aiken_lang/parser/
error.rs

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