Skip to main content

maplibre_expr/
error.rs

1//! Error types for parsing and evaluation.
2//!
3//! Errors are modeled as semantic *kinds* (an enum), with a [`Display`] "printer"
4//! that renders the message text, so callers can match on the cause rather than
5//! parse a string. [`ParseError`] also carries a `key` — the location path of
6//! the offending sub-expression (e.g. `[2][1]`), collected as the error bubbles
7//! up through parsing — mirroring the reference implementation's error keys.
8//!
9//! [`Display`]: std::fmt::Display
10
11use std::fmt;
12
13/// The semantic cause of a parse/compile error.
14#[derive(Debug, Clone, PartialEq)]
15pub enum ParseErrorKind {
16    /// A wrapped runtime error surfaced by compile-time constant folding.
17    /// (Every intrinsic parse error has a dedicated variant below.)
18    Other(String),
19    /// An unrecognized operator name.
20    UnknownExpression(String),
21    /// Wrong number of arguments to an operator (`expected` is a human range).
22    WrongArgCount {
23        op: String,
24        expected: String,
25        found: usize,
26    },
27    /// An expression's type did not satisfy the expected type.
28    TypeMismatch { expected: String, found: String },
29    /// A comparison operator applied to an unsupported operand type.
30    NotComparable { op: String, ty: String },
31    /// A comparison between two incompatible concrete types.
32    CannotCompare { lhs: String, rhs: String },
33    /// An `interpolate` output whose type cannot be interpolated.
34    NotInterpolatable(String),
35    /// An unbound `var` reference.
36    UnboundVariable(String),
37    /// Misuse of the `zoom` expression.
38    Zoom(&'static str),
39    /// An operator that takes exactly one argument (`literal`/`within`/…).
40    RequiresExactlyOneArg { op: String, found: usize },
41    /// A single-argument coercion (`to-boolean`/`to-string`) with wrong arity.
42    ExpectedOneArgument,
43    /// A `CompoundExpression` whose arity matched no typed overload.
44    ExpectedArgsOfType { sig: String, found: String },
45    /// `match` with fewer than four arguments.
46    MatchAtLeast4 { found: usize },
47    /// The first (item-type) argument of `array` was not a valid type name.
48    ArrayItemType,
49    /// The length argument of `array` was not a positive integer literal.
50    ArrayLength,
51    /// A bare object used where an expression was expected.
52    BareObject,
53    /// An empty array (no operator).
54    EmptyArray,
55    /// The operator slot was not a string. `found` is the JS `typeof`.
56    ExpressionNameNotString { found: &'static str },
57    /// The `global-state` property argument was not a string. `found` is a type.
58    GlobalStateProperty { found: String },
59    /// `step`/`interpolate` stop inputs were not strictly ascending.
60    AscendingStops { kind: String },
61    /// `exponential` interpolation without a numeric base.
62    ExponentialBase,
63    /// A malformed `cubic-bezier` interpolation type.
64    CubicBezier,
65    /// The `collator` options argument was not an object.
66    CollatorOptions,
67    /// `number-format` given both `currency` and `unit`.
68    NumberFormatExclusive,
69    /// `slice`'s first argument was not an array or string.
70    SliceFirstArg { found: String },
71    /// An `in`/`index-of` needle (checked statically) was not a primitive.
72    SearchNeedle { found: String },
73    /// A `match` branch label was not a number or string.
74    BranchLabelsType,
75    /// Duplicate `match` branch labels.
76    BranchLabelsUnique,
77    /// A `match` branch with no labels.
78    BranchLabelsEmpty,
79    /// A non-integer numeric `match` branch label.
80    BranchLabelNotInteger,
81    /// A numeric `match` branch label beyond the safe-integer range.
82    BranchLabelTooLarge,
83    /// A `within`/`distance` argument was not valid polygon geojson.
84    GeojsonPolygon { op: String },
85    /// A `let` binding name was not a string.
86    LetBindingNameString,
87    /// A `var` binding name was not a string.
88    VarBindingName,
89    /// The `number-format` options argument was not an object.
90    NumberFormatOptionsObject,
91    /// A `format` `vertical-align` option had an invalid value.
92    VerticalAlign { found: String },
93    /// `match`/`step`/`interpolate` given an odd number of arguments.
94    ExpectedEvenArgs { op: &'static str },
95    /// `case` given an even number of arguments.
96    ExpectedOddArgsCase,
97    /// `let` given an even number of arguments.
98    ExpectedOddArgsLet,
99    /// `format` given no sections.
100    FormatAtLeastOne,
101    /// `collator` given other than one argument.
102    CollatorOneArg,
103    /// `number-format` given other than two arguments.
104    NumberFormatTwoArgs,
105    /// An operator taking a fixed count other than one, with wrong arity.
106    ExpectedNArgs { n: usize, found: usize },
107    /// A `format` first argument that was a bare options object.
108    FormatFirstSection,
109    /// A user macro/function/native call with the wrong argument count.
110    ExtArgCount {
111        kind: &'static str,
112        op: String,
113        expected: usize,
114        found: usize,
115    },
116    /// A macro expanded past the recursion-depth limit.
117    MacroDepth { op: String },
118    /// An `interpolate` stop input was not a number literal.
119    InterpolationStopNumber,
120    /// A `step` stop input was not a number literal.
121    StepStopNumber,
122    /// An interpolation type was not an array (e.g. `["linear"]`).
123    InterpolationTypeArray,
124    /// An interpolation type name was not a string.
125    InterpolationTypeName,
126    /// An unrecognized interpolation type.
127    UnknownInterpolationType { name: String },
128    /// A `collator` compared non-string operands.
129    CollatorNonString,
130    /// `slice`/`concat` argument was not a string or array.
131    ExpectedStringOrArray { found: String },
132    /// A `format` section's text was not a valid formatted type.
133    FormattedTextType,
134    /// A `let` variable name contained invalid characters.
135    VariableName,
136}
137
138impl fmt::Display for ParseErrorKind {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        match self {
141            ParseErrorKind::Other(s) => write!(f, "{s}"),
142            ParseErrorKind::UnknownExpression(op) => write!(
143                f,
144                "Unknown expression \"{op}\". If you wanted a literal array, use [\"literal\", [...]]."
145            ),
146            ParseErrorKind::WrongArgCount {
147                op,
148                expected,
149                found,
150            } => {
151                let _ = op;
152                write!(f, "Expected {expected}, but found {found} instead.")
153            }
154            ParseErrorKind::TypeMismatch { expected, found } => {
155                write!(f, "Expected {expected} but found {found} instead.")
156            }
157            ParseErrorKind::NotComparable { op, ty } => {
158                write!(f, "\"{op}\" comparisons are not supported for type '{ty}'.")
159            }
160            ParseErrorKind::CannotCompare { lhs, rhs } => {
161                write!(f, "Cannot compare types '{lhs}' and '{rhs}'.")
162            }
163            ParseErrorKind::NotInterpolatable(ty) => write!(f, "Type {ty} is not interpolatable."),
164            ParseErrorKind::UnboundVariable(name) => write!(
165                f,
166                "Unknown variable \"{name}\". Make sure \"{name}\" has been bound in an enclosing \"let\" expression before using it."
167            ),
168            ParseErrorKind::Zoom(msg) => write!(f, "{msg}"),
169            ParseErrorKind::RequiresExactlyOneArg { op, found } => write!(
170                f,
171                "'{op}' expression requires exactly one argument, but found {found} instead."
172            ),
173            ParseErrorKind::ExpectedOneArgument => write!(f, "Expected one argument."),
174            ParseErrorKind::ExpectedArgsOfType { sig, found } => write!(
175                f,
176                "Expected arguments of type {sig}, but found ({found}) instead."
177            ),
178            ParseErrorKind::MatchAtLeast4 { found } => {
179                write!(f, "Expected at least 4 arguments, but found only {found}.")
180            }
181            ParseErrorKind::ArrayItemType => write!(
182                f,
183                "The item type argument of \"array\" must be one of string, number, boolean"
184            ),
185            ParseErrorKind::ArrayLength => write!(
186                f,
187                "The length argument to \"array\" must be a positive integer literal"
188            ),
189            ParseErrorKind::BareObject => {
190                write!(f, "Bare objects invalid. Use [\"literal\", {{...}}] instead.")
191            }
192            ParseErrorKind::EmptyArray => write!(
193                f,
194                "Expected an array with at least one element. If you wanted a literal array, use [\"literal\", []]."
195            ),
196            ParseErrorKind::ExpressionNameNotString { found } => write!(
197                f,
198                "Expression name must be a string, but found {found} instead. If you wanted a literal array, use [\"literal\", [...]]."
199            ),
200            ParseErrorKind::GlobalStateProperty { found } => {
201                write!(f, "Global state property must be string, but found {found} instead.")
202            }
203            ParseErrorKind::AscendingStops { kind } => write!(
204                f,
205                "Input/output pairs for \"{kind}\" expressions must be arranged with input values in strictly ascending order."
206            ),
207            ParseErrorKind::ExponentialBase => {
208                write!(f, "Exponential interpolation requires a numeric base.")
209            }
210            ParseErrorKind::CubicBezier => write!(
211                f,
212                "Cubic bezier interpolation requires four numeric arguments with values between 0 and 1."
213            ),
214            ParseErrorKind::CollatorOptions => {
215                write!(f, "Collator options argument must be an object.")
216            }
217            ParseErrorKind::NumberFormatExclusive => write!(
218                f,
219                "NumberFormat options `currency` and `unit` are mutually exclusive"
220            ),
221            ParseErrorKind::SliceFirstArg { found } => write!(
222                f,
223                "Expected first argument to be of type array or string, but found {found} instead"
224            ),
225            ParseErrorKind::SearchNeedle { found } => write!(
226                f,
227                "Expected first argument to be of type boolean, string, number or null, but found {found} instead"
228            ),
229            ParseErrorKind::BranchLabelsType => {
230                write!(f, "Branch labels must be numbers or strings.")
231            }
232            ParseErrorKind::BranchLabelsUnique => write!(f, "Branch labels must be unique."),
233            ParseErrorKind::BranchLabelsEmpty => write!(f, "Expected at least one branch label."),
234            ParseErrorKind::BranchLabelNotInteger => {
235                write!(f, "Numeric branch labels must be integer values.")
236            }
237            ParseErrorKind::BranchLabelTooLarge => write!(
238                f,
239                "Branch labels must be integers no larger than 9007199254740991."
240            ),
241            ParseErrorKind::GeojsonPolygon { op } => write!(
242                f,
243                "'{op}' expression requires valid geojson object that contains polygon geometry type."
244            ),
245            ParseErrorKind::LetBindingNameString => {
246                write!(f, "'let' binding names must be strings.")
247            }
248            ParseErrorKind::VarBindingName => write!(f, "'var' requires a string binding name."),
249            ParseErrorKind::NumberFormatOptionsObject => {
250                write!(f, "'number-format' options must be an object.")
251            }
252            ParseErrorKind::VerticalAlign { found } => write!(
253                f,
254                "'vertical-align' must be one of: 'bottom', 'center', 'top' but found '{found}' instead."
255            ),
256            ParseErrorKind::ExpectedEvenArgs { op } => {
257                write!(f, "Expected an even number of arguments (>= 4) to '{op}'.")
258            }
259            ParseErrorKind::ExpectedOddArgsCase => {
260                write!(f, "Expected an odd number of arguments (>= 3) to 'case'.")
261            }
262            ParseErrorKind::ExpectedOddArgsLet => {
263                write!(f, "Expected an odd number of arguments to 'let'.")
264            }
265            ParseErrorKind::FormatAtLeastOne => {
266                write!(f, "Expected at least one argument to 'format'.")
267            }
268            ParseErrorKind::CollatorOneArg => write!(f, "Expected one argument to 'collator'."),
269            ParseErrorKind::NumberFormatTwoArgs => {
270                write!(f, "Expected two arguments to 'number-format'.")
271            }
272            ParseErrorKind::ExpectedNArgs { n, found } => {
273                write!(f, "Expected {n} arguments, but found {found} instead.")
274            }
275            ParseErrorKind::FormatFirstSection => {
276                write!(f, "First argument to 'format' must be an image or text section.")
277            }
278            ParseErrorKind::ExtArgCount {
279                kind,
280                op,
281                expected,
282                found,
283            } => write!(f, "{kind} '{op}' expects {expected} argument(s), found {found}."),
284            ParseErrorKind::MacroDepth { op } => {
285                write!(f, "Macro expansion too deep expanding '{op}' (recursive macro?).")
286            }
287            ParseErrorKind::InterpolationStopNumber => {
288                write!(f, "Interpolation stop inputs must be numbers.")
289            }
290            ParseErrorKind::StepStopNumber => write!(f, "Step stop inputs must be numbers."),
291            ParseErrorKind::InterpolationTypeArray => {
292                write!(f, "Interpolation type must be an array, e.g. [\"linear\"].")
293            }
294            ParseErrorKind::InterpolationTypeName => {
295                write!(f, "Interpolation type name must be a string.")
296            }
297            ParseErrorKind::UnknownInterpolationType { name } => {
298                write!(f, "Unknown interpolation type \"{name}\".")
299            }
300            ParseErrorKind::CollatorNonString => {
301                write!(f, "Cannot use collator to compare non-string types.")
302            }
303            ParseErrorKind::ExpectedStringOrArray { found } => write!(
304                f,
305                "Expected argument of type string or array, but found {found} instead."
306            ),
307            ParseErrorKind::FormattedTextType => write!(
308                f,
309                "Formatted text type must be 'string', 'value', 'image' or 'null'."
310            ),
311            ParseErrorKind::VariableName => write!(
312                f,
313                "Variable names must contain only alphanumeric characters or '_'."
314            ),
315        }
316    }
317}
318
319/// An error raised while turning JSON into an [`Expr`](crate::Expr).
320///
321/// Corresponds to a `"result": "error"` compile outcome in the spec fixtures.
322#[derive(Debug, Clone, PartialEq)]
323pub struct ParseError {
324    pub kind: ParseErrorKind,
325    /// Location path of the offending sub-expression, e.g. `"[2][1]"`.
326    pub key: String,
327}
328
329impl ParseError {
330    /// Build an error from a semantic kind.
331    pub fn of(kind: ParseErrorKind) -> ParseError {
332        ParseError {
333            kind,
334            key: String::new(),
335        }
336    }
337
338    /// Build an ad-hoc error from a message (kind [`ParseErrorKind::Other`]).
339    pub fn new(message: impl Into<String>) -> ParseError {
340        ParseError::of(ParseErrorKind::Other(message.into()))
341    }
342
343    /// Prepend an argument index to the location key as the error bubbles up.
344    pub(crate) fn at(mut self, index: usize) -> ParseError {
345        self.key = format!("[{index}]{}", self.key);
346        self
347    }
348}
349
350impl fmt::Display for ParseError {
351    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
352        write!(f, "{}", self.kind)
353    }
354}
355
356impl std::error::Error for ParseError {}
357
358/// The semantic cause of an evaluation error.
359#[derive(Debug, Clone, PartialEq)]
360pub enum EvalErrorKind {
361    /// A message-only error: the user-thrown `["error", msg]` operator, or a
362    /// parse error wrapped while compiling a user function body.
363    Other(String),
364    /// A value was not of the expected type.
365    TypeMismatch { expected: String, found: String },
366    /// Like [`TypeMismatch`](Self::TypeMismatch), but naming the offending
367    /// argument (e.g. `"second argument"`) instead of the generic "value".
368    TypeMismatchArg {
369        arg: &'static str,
370        expected: String,
371        found: String,
372    },
373    /// A value could not be parsed into a type (`to-color`, coercions, …).
374    /// `value` is already rendered (raw string, else `JSON.stringify`).
375    CouldNotParse { ty: &'static str, value: String },
376    /// A value could not be converted to a number (`to-number`).
377    CouldNotConvertToNumber { value: String },
378    /// An `at` index was negative.
379    ArrayIndexNegative { index: f64 },
380    /// An `at` index was past the end of the array.
381    ArrayIndexOutOfBounds { index: f64, max: usize },
382    /// An `at` index was not an integer.
383    ArrayIndexNotInteger { index: f64 },
384    /// An `rgb`/`rgba`/`to-color` array value was out of range or malformed.
385    /// `reason` is the clause after the value.
386    InvalidRgba { value: String, reason: &'static str },
387    /// Ordered comparison of two runtime values of incompatible types.
388    NotOrderedComparable {
389        op: String,
390        lhs: String,
391        rhs: String,
392    },
393    /// An `in`/`index-of` needle was not a primitive.
394    SearchNeedle { found: String },
395    /// An interpolation produced an uninterpolatable output at runtime.
396    InterpolationOutputs,
397    /// A user function recursed past the call-depth limit.
398    MaxCallDepth { op: String },
399    /// `zoom` used where no zoom is available.
400    ZoomUnavailable,
401    /// An operator MapLibre defines but this crate does not evaluate yet.
402    Unimplemented { op: String },
403    /// An unbound `var` reference at evaluation time.
404    UnknownVariable { name: String },
405}
406
407impl fmt::Display for EvalErrorKind {
408    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
409        match self {
410            EvalErrorKind::Other(s) => write!(f, "{s}"),
411            EvalErrorKind::TypeMismatch { expected, found } => write!(
412                f,
413                "Expected value to be of type {expected}, but found {found} instead."
414            ),
415            EvalErrorKind::TypeMismatchArg {
416                arg,
417                expected,
418                found,
419            } => write!(
420                f,
421                "Expected {arg} to be of type {expected}, but found {found} instead."
422            ),
423            EvalErrorKind::CouldNotParse { ty, value } => {
424                write!(f, "Could not parse {ty} from value '{value}'")
425            }
426            EvalErrorKind::CouldNotConvertToNumber { value } => {
427                write!(f, "Could not convert {value} to number.")
428            }
429            EvalErrorKind::ArrayIndexNegative { index } => {
430                write!(f, "Array index out of bounds: {index} < 0.")
431            }
432            EvalErrorKind::ArrayIndexOutOfBounds { index, max } => {
433                write!(f, "Array index out of bounds: {index} > {max}.")
434            }
435            EvalErrorKind::ArrayIndexNotInteger { index } => {
436                write!(f, "Array index must be an integer, but found {index} instead.")
437            }
438            EvalErrorKind::InvalidRgba { value, reason } => {
439                write!(f, "Invalid rgba value {value}: {reason}")
440            }
441            EvalErrorKind::NotOrderedComparable { op, lhs, rhs } => write!(
442                f,
443                "Expected arguments for \"{op}\" to be (string, string) or (number, number), but found ({lhs}, {rhs}) instead."
444            ),
445            EvalErrorKind::SearchNeedle { found } => write!(
446                f,
447                "Expected first argument to be of type boolean, string, number or null, but found {found} instead."
448            ),
449            EvalErrorKind::InterpolationOutputs => write!(
450                f,
451                "Interpolation outputs must be numbers, colors, or arrays of numbers."
452            ),
453            EvalErrorKind::MaxCallDepth { op } => {
454                write!(f, "Maximum call depth exceeded calling function '{op}'.")
455            }
456            EvalErrorKind::ZoomUnavailable => {
457                write!(f, "The 'zoom' expression is unavailable here.")
458            }
459            EvalErrorKind::Unimplemented { op } => write!(f, "Unimplemented operator \"{op}\"."),
460            EvalErrorKind::UnknownVariable { name } => write!(f, "Unknown variable \"{name}\"."),
461        }
462    }
463}
464
465/// An error raised while evaluating a well-formed expression.
466///
467/// Corresponds to a per-input `{ "error": ... }` output in the spec fixtures.
468#[derive(Debug, Clone, PartialEq)]
469pub struct EvalError {
470    pub kind: EvalErrorKind,
471}
472
473impl EvalError {
474    pub fn of(kind: EvalErrorKind) -> EvalError {
475        EvalError { kind }
476    }
477
478    pub fn new(message: impl Into<String>) -> EvalError {
479        EvalError::of(EvalErrorKind::Other(message.into()))
480    }
481}
482
483impl fmt::Display for EvalError {
484    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
485        write!(f, "{}", self.kind)
486    }
487}
488
489impl std::error::Error for EvalError {}