boxworks_lang/
error.rs

1use super::Str;
2
3/// Error encountered when parsing Box language.
4#[derive(Debug, PartialEq, Eq)]
5pub enum Error<'a> {
6    /// A positional argument appears after a keyword argument.
7    ///
8    /// This is an error, just like in Python.
9    /// The problem is that it's ambiguous which parameter the
10    /// positional argument should apply to.
11    PositionalArgAfterKeywordArg {
12        positional_arg: Str<'a>,
13        keyword_arg: Str<'a>,
14    },
15
16    /// A function argument has the wrong type.
17    IncorrectType {
18        wanted_type: &'static str,
19        got_type: &'static str,
20        got_raw_value: Str<'a>,
21        function_name: Str<'a>,
22        parameter_name: &'a str,
23    },
24
25    /// Too many positional arguments provided.
26    TooManyPositionalArgs {
27        extra_positional_arg: Str<'a>,
28        function_name: Str<'a>,
29        max_positional_args: usize,
30    },
31
32    /// No such argument to this function.
33    NoSuchArgument {
34        function_name: Str<'a>,
35        argument: Str<'a>,
36    },
37
38    /// The same argument was provided multiple times.
39    DuplicateArgument {
40        parameter_name: &'a str,
41        first_assignment: Str<'a>,
42        second_assignment: Str<'a>,
43    },
44
45    /// The specified function does not exist.
46    NoSuchFunction { function_name: Str<'a> },
47
48    /// An opening bracket token '[' or '(' was unmatched.
49    UnmatchedOpeningBracket { open: Str<'a> },
50
51    /// A closing bracket token ']' or ')' was unmatched.
52    UnmatchedClosingBracket { close: Str<'a> },
53
54    /// An invalid unit was provided for a dimension.
55    InvalidDimensionUnit { dimension: Str<'a>, unit: Str<'a> },
56
57    /// An unexpected token was encountered.
58    UnexpectedToken {
59        want: &'static str,
60        got: Str<'a>,
61        skipped: Str<'a>,
62    },
63
64    /// A function name appeared without any arguments
65    MissingArgsForFunction { function_name: Str<'a> },
66
67    /// Mismatched braces - e.g. [) or (].
68    MismatchedBraces { open: Str<'a>, close: Str<'a> },
69
70    /// Input ended while parsing a function keyword argument
71    IncompleteKeywordArg { keyword: Str<'a> },
72
73    /// A number has multiple decimal points.
74    MultipleDecimalPoints { point: Str<'a> },
75
76    /// No units were specified for a number.
77    NumberWithoutUnits { number: Str<'a> },
78
79    /// Input contains an invalid character (like a non-ASCII character)
80    InvalidCharacter { char: Str<'a> },
81}
82
83impl<'a> Error<'a> {
84    pub fn message(&self) -> String {
85        use Error::*;
86        match self {
87            PositionalArgAfterKeywordArg { .. } => {
88                "A positional argument appears after a keyword argument".into()
89            }
90            IncorrectType { .. } => "An argument has the wrong type".into(),
91            TooManyPositionalArgs { .. } => "Too many positional arguments provided".into(),
92            NoSuchArgument { .. } => "No such argument to this function".into(),
93            DuplicateArgument { parameter_name, .. } => {
94                format!["The `{parameter_name}` argument was provided multiple times"]
95            }
96            NoSuchFunction { function_name } => {
97                format!["Unknown function `{function_name}`"]
98            }
99            UnmatchedOpeningBracket { .. } => "Unmatched opening bracket".into(),
100            UnmatchedClosingBracket { .. } => "Unmatched closing bracket".into(),
101            InvalidDimensionUnit { dimension: _, unit } => {
102                format!["Dimension has an invalid unit '{unit}'"]
103            }
104            UnexpectedToken { want, .. } => {
105                format!["Unexpected token while looking for {want}"]
106            }
107            MissingArgsForFunction { function_name } => {
108                format!["No arguments provided for '{function_name}'"]
109            }
110            MismatchedBraces { open, close } => format!["Mismatched braces '{open}' and '{close}'"],
111            IncompleteKeywordArg { .. } => "Incomplete keyword argument".into(),
112            MultipleDecimalPoints { .. } => "A number has multiple decimal points".into(),
113            NumberWithoutUnits { .. } => "No units were provided for this number".into(),
114            InvalidCharacter { .. } => "Invalid character in the input".into(),
115        }
116    }
117    pub fn labels(&self) -> Vec<ErrorLabel> {
118        use Error::*;
119        match self {
120            PositionalArgAfterKeywordArg {
121                        positional_arg,
122                        keyword_arg,
123                    } => {
124                        vec![
125                            ErrorLabel {
126                                span: positional_arg.span(),
127                                text: "The positional argument appears here".into(),
128                            },
129                            ErrorLabel {
130                                span: keyword_arg.span(),
131                                text: "The keyword argument appears here".into(),
132                            },
133                        ]
134                    }
135            IncorrectType {
136                        wanted_type,
137                        got_type: got,
138                        got_raw_value,
139                        function_name,
140                        parameter_name,
141                    } => vec![
142                        ErrorLabel {
143                            span: got_raw_value.span(),
144                            text: format!["The provided argument is {got}"],
145                        },
146                        ErrorLabel {
147                            span: function_name.span(),
148                            text: format![
149                                "The `{parameter_name}` parameter of the `{}` function requires {wanted_type}",
150                                function_name.str()
151                            ],
152                        },
153                    ],
154            TooManyPositionalArgs { extra_positional_arg, function_name, max_positional_args } => vec![
155                        ErrorLabel {
156                            span: extra_positional_arg.span(),
157                            text: "an extra positional arguments was provided".to_string(),
158                        },
159                        ErrorLabel {
160                            span: function_name.span(),
161                            text: format![
162                                "The `{}` function accepts up to {max_positional_args} positional arguments",
163                                function_name.str()
164                            ],
165                        },
166                    ],
167            NoSuchArgument { function_name, argument } => vec![
168                        ErrorLabel {
169                            span: argument.span(),
170                            text: format![
171                                "an argument with name `{}` appears here",
172                                argument.str(),
173                            ],
174                        },
175                        ErrorLabel {
176                            span: function_name.span(),
177                            text: format![
178                                "The `{}` function does not have a parameter with name `{}`",
179                                function_name.str(),
180                                argument.str(),
181                            ],
182                        },
183                    ],
184            DuplicateArgument { parameter_name: _, first_assignment, second_assignment } => vec![
185                        ErrorLabel {
186                            span: first_assignment.span(),
187                            text: "the first value appears here".to_string(),
188                        },
189                        ErrorLabel {
190                            span: second_assignment.span(),
191                            text: "the second value appear here".to_string(),
192                        },
193                    ],
194            NoSuchFunction { function_name } => vec![
195                        ErrorLabel {
196                            span: function_name.span(),
197                            text: "there is no function with this name".to_string(),
198                        },
199                    ],
200            UnmatchedOpeningBracket{ open: close } => vec![
201                        ErrorLabel {
202                            span: close.span(),
203                            text: "this bracket does not correspond to any subsequent closing bracket".to_string(),
204                        },
205                    ],
206            UnmatchedClosingBracket{ close } => vec![
207                        ErrorLabel {
208                            span: close.span(),
209                            text: "this bracket does not correspond to any preceding opening bracket".to_string(),
210                        },
211                    ],
212            InvalidDimensionUnit { dimension: _, unit } => vec![
213                        ErrorLabel {
214                            span: unit.span(),
215                            text: "valid units are the same as TeX".to_string(),
216                        },
217                    ],
218            UnexpectedToken { want: _, got, skipped } => vec![
219                        ErrorLabel {
220                            span: got.span(),
221                            text: "this token was not expected".to_string(),
222                        },
223                        ErrorLabel {
224                            span: skipped.span(),
225                            text: "this source code will be skipped".to_string(),
226                        },
227                    ],
228            MissingArgsForFunction { function_name } => vec![
229                        ErrorLabel {
230                            span: function_name.span(),
231                            text: "a function name must be followed by arguments".to_string(),
232                        },
233                    ],
234            MismatchedBraces { open, close } => vec![
235                        ErrorLabel {
236                            span: open.span(),
237                            text: format!["the opening brace is '{open}'"],
238                        },
239                        ErrorLabel {
240                            span: close.span(),
241                            text: format!["the closing brace is '{close}'"],
242                        },
243                    ],
244            IncompleteKeywordArg { keyword  } => vec![
245                        ErrorLabel {
246                            span: keyword.span(),
247                            text: "a value was not provided for this keyword".into(),
248                        },
249                    ],
250MultipleDecimalPoints { point } => vec![
251    ErrorLabel {
252        span: point.span(),
253        text: "this is the second or subsequent decimal point".into(),
254    },
255],
256            NumberWithoutUnits { number } =>vec![
257    ErrorLabel {
258        span: number.span(),
259                            text: "valid units are the same as TeX".to_string(),
260    },
261
262            ],
263            InvalidCharacter { char } => vec![
264    ErrorLabel {
265        span: char.span(),
266        text: "this character is not allowed here".into(),
267    },
268
269            ],
270        }
271    }
272    pub fn notes(&self) -> Vec<String> {
273        use Error::*;
274        match self {
275            PositionalArgAfterKeywordArg { .. } => {
276                vec![
277                    "Positional arguments must be appear before keyword arguments (as in Python)"
278                        .to_string(),
279                ]
280            }
281            UnmatchedClosingBracket { .. } | UnmatchedOpeningBracket { .. } => {
282                vec!["The token will be ignored".to_string()]
283            }
284            MismatchedBraces { .. } => {
285                vec![
286                    "A '(' opening brace must be closed by ')' and similar for '[' and ']'"
287                        .to_string(),
288                ]
289            }
290            UnexpectedToken { .. }
291            | MissingArgsForFunction { .. }
292            | IncorrectType { .. }
293            | TooManyPositionalArgs { .. }
294            | NoSuchArgument { .. }
295            | DuplicateArgument { .. }
296            | NoSuchFunction { .. }
297            | IncompleteKeywordArg { .. }
298            | InvalidDimensionUnit { .. }
299            | MultipleDecimalPoints { .. }
300            | NumberWithoutUnits { .. }
301            | InvalidCharacter { .. } => vec![],
302        }
303    }
304}
305
306/// Label on an error message.
307///
308/// A label identifies a particular piece of source code and some
309/// information about it.
310pub struct ErrorLabel {
311    pub span: std::ops::Range<usize>,
312    pub text: String,
313}
314
315impl<'a> Error<'a> {
316    #[cfg(feature = "ariadne")]
317    pub fn ariadne_report(
318        &self,
319        file_name: &'a str,
320    ) -> ariadne::Report<'static, (&str, std::ops::Range<usize>)> {
321        let mut report =
322            ariadne::Report::build(ariadne::ReportKind::Error, (file_name, 0..file_name.len()))
323                .with_message(self.message());
324        let mut color = ariadne::Color::BrightRed;
325        for label in self.labels() {
326            report = report.with_label(
327                ariadne::Label::new((file_name, label.span))
328                    .with_message(label.text)
329                    .with_color(color),
330            );
331            color = ariadne::Color::BrightYellow;
332        }
333        for note in self.notes() {
334            report = report.with_note(note);
335        }
336        report.finish()
337    }
338}
339
340/// A data structure for accumulating errors.
341#[derive(Clone, Debug, Default)]
342pub struct ErrorAccumulator<'a> {
343    errs: std::rc::Rc<std::cell::RefCell<Vec<Error<'a>>>>,
344}
345
346impl<'a> ErrorAccumulator<'a> {
347    pub fn add(&self, err: Error<'a>) {
348        self.errs.borrow_mut().push(err);
349    }
350    pub fn len(&self) -> usize {
351        self.errs.borrow().len()
352    }
353    pub fn is_empty(&self) -> bool {
354        self.len() == 0
355    }
356    pub fn check(self) -> Result<(), Vec<Error<'a>>> {
357        let errs = self.errs.take();
358        if errs.is_empty() {
359            Ok(())
360        } else {
361            Err(errs)
362        }
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369
370    pub fn get_unique_err(source: &'_ str) -> Error<'_> {
371        let mut errs = super::super::parse_horizontal_list(source).unwrap_err();
372        assert_eq!(errs.len(), 1, "{:?}", errs);
373        errs.pop().unwrap()
374    }
375
376    macro_rules! error_tests {
377        ( $(
378            ($name: ident, $source: expr, $want_variant: ident,),
379        )+ ) => {
380            $(
381            #[test]
382            fn $name() {
383                let source = $source;
384                let err = get_unique_err(source);
385                println!["got: {err:?}"];
386                assert!(matches!(err, Error::$want_variant {..}), "{:?}", err);
387            }
388            )+
389        };
390    }
391    error_tests!(
392        (invalid_dimension_unit, "glue(0plx)", InvalidDimensionUnit,),
393        (invalid_type_positional, r#"chars(1pc)"#, IncorrectType,),
394        (invalid_type_keyword, r#"chars(content=1pc)"#, IncorrectType,),
395        (
396            too_many_positional_args_1,
397            r#"chars("Hello", 3, "Mundo")"#,
398            TooManyPositionalArgs,
399        ),
400        (
401            too_many_positional_args_2,
402            r#"chars("Hello", font=3, "Mundo")"#,
403            PositionalArgAfterKeywordArg,
404        ),
405        (
406            positional_arg_after_keyword_arg,
407            r#"chars(font=3, "Hello")"#,
408            PositionalArgAfterKeywordArg,
409        ),
410        (
411            duplicate_keyword_args,
412            r#"chars(content="Hello", content="World")"#,
413            DuplicateArgument,
414        ),
415        (
416            duplicate_positional_and_keyword_args,
417            r#"chars("Hello", content="Mundo")"#,
418            DuplicateArgument,
419        ),
420        (
421            invalid_keyword_arg,
422            r#"chars(random="Hello")"#,
423            NoSuchArgument,
424        ),
425        (invalid_func_name, r#"random()"#, NoSuchFunction,),
426        (trailing_closed_square, "chars()]", UnmatchedClosingBracket,),
427        (trailing_open_square, "chars())", UnmatchedClosingBracket,),
428        (
429            unexpected_token_before_func_name,
430            ",chars()",
431            UnexpectedToken,
432        ),
433        (
434            unexpected_token_after_func_name,
435            "chars,()",
436            UnexpectedToken,
437        ),
438        (missing_func_name, "()", UnexpectedToken,),
439        (
440            missing_paren_after_func_name,
441            "text",
442            MissingArgsForFunction,
443        ),
444        (incorrect_func_braces, "chars[]()", UnexpectedToken,),
445        (incorrect_closing_brace_round, "chars(]", MismatchedBraces,),
446        (
447            incorrect_closing_brace_square,
448            "hlist(content=[))",
449            MismatchedBraces,
450        ),
451        (
452            unmatched_opening_brace_round,
453            r#"chars("Hello""#,
454            UnmatchedOpeningBracket,
455        ),
456        (
457            unexpected_token_in_args_1,
458            "glue(,width=1pt)",
459            UnexpectedToken,
460        ),
461        (
462            unexpected_token_in_args_2,
463            "glue(width,=1pt)",
464            UnexpectedToken,
465        ),
466        (
467            unexpected_token_in_args_3,
468            "glue(width=,1pt)",
469            UnexpectedToken,
470        ),
471        (unexpected_token_in_args_4, "glue(,1pt)", UnexpectedToken,),
472        (end_of_input_in_args_1, "glue(width)", IncompleteKeywordArg,),
473        (end_of_input_in_args_2, "glue(width=)", IncompleteKeywordArg,),
474        (
475            two_decimal_points,
476            "glue(width=1.1.1pt)",
477            MultipleDecimalPoints,
478        ),
479        (number_no_unit, "glue(width=1.1)", NumberWithoutUnits,),
480        (random_character, "/", InvalidCharacter,),
481        (non_ascii_character, "รค", InvalidCharacter,),
482    );
483}