Skip to main content

acts_expr/
error.rs

1//! Errors the evaluator reports.
2
3use std::fmt;
4
5/// Everything `acts-expr` can fail with.
6///
7/// A parse error carries the byte offset in the source it stopped at; an
8/// evaluation error names the operation and the value type it could not apply
9/// to, because the same expression can be valid for one context and not for
10/// the next.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum Error {
13    /// The source is not a well-formed expression.
14    Parse { message: String, pos: usize },
15    /// An identifier is not in the context.
16    Unknown { name: String },
17    /// A member of an object (or an index of a list) does not exist.
18    Missing { name: String },
19    /// A list index is negative or past the end, with the length it was read
20    /// against.
21    OutOfRange { index: i64, len: usize },
22    /// An operand or receiver has no type for the operation.
23    Type {
24        /// The operation as it reads in the source, e.g. `'+'` or `'<'`.
25        operation: &'static str,
26        /// The value's type: `null`, `int`, `float`, `string`, `bool`, `list`,
27        /// `map` or `function`.
28        got: &'static str,
29    },
30    /// An arithmetic operation overflowed an `i64`.
31    Overflow { operation: &'static str },
32    /// A division or remainder had a zero divisor.
33    DivideByZero { operation: &'static str },
34    /// A native function (an injected method) failed.
35    Function { name: String, message: String },
36    /// The receiver of a method call is not an object and no function of that
37    /// name is in the context.
38    UnknownMethod { name: String },
39    /// A value could not cross the `json` bridge.
40    Convert { message: String },
41}
42
43/// The crate's result type: the error is `Error` unless a caller names
44/// another.
45pub type Result<T, E = Error> = std::result::Result<T, E>;
46
47impl Error {
48    /// The byte offset a parse error stopped at, for a host that wants to
49    /// point at it. `None` for every error raised at evaluation time.
50    pub fn position(&self) -> Option<usize> {
51        match self {
52            Error::Parse { pos, .. } => Some(*pos),
53            _ => None,
54        }
55    }
56
57    pub(crate) fn parse(message: impl Into<String>, pos: usize) -> Error {
58        Error::Parse {
59            message: message.into(),
60            pos,
61        }
62    }
63}
64
65impl fmt::Display for Error {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        match self {
68            Error::Parse { message, pos } => write!(f, "parse error at byte {pos}: {message}"),
69            Error::Unknown { name } => write!(f, "unknown variable '{name}'"),
70            Error::Missing { name } => write!(f, "no such member '{name}'"),
71            Error::OutOfRange { index, len } => {
72                write!(f, "index {index} is out of range for a list of {len}")
73            }
74            Error::Type { operation, got } => {
75                write!(f, "no {operation} for a {got} value")
76            }
77            Error::Overflow { operation } => write!(f, "'{operation}' overflowed an int"),
78            Error::DivideByZero { operation } => write!(f, "'{operation}' by zero"),
79            Error::Function { name, message } => {
80                write!(f, "'{name}' failed: {message}")
81            }
82            Error::UnknownMethod { name } => write!(f, "no such method '{name}'"),
83            Error::Convert { message } => write!(f, "{message}"),
84        }
85    }
86}
87
88impl std::error::Error for Error {}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn position_is_only_present_for_parse_errors() {
96        let parse = Error::parse("expected a value", 7);
97        assert_eq!(parse.position(), Some(7));
98        assert_eq!(
99            parse,
100            Error::Parse {
101                message: "expected a value".to_string(),
102                pos: 7
103            }
104        );
105
106        for error in [
107            Error::Unknown {
108                name: "x".to_string(),
109            },
110            Error::Missing {
111                name: "x".to_string(),
112            },
113            Error::OutOfRange { index: 1, len: 0 },
114            Error::Type {
115                operation: "+",
116                got: "int",
117            },
118            Error::Overflow { operation: "+" },
119            Error::DivideByZero { operation: "/" },
120            Error::Function {
121                name: "f".to_string(),
122                message: "failed".to_string(),
123            },
124            Error::UnknownMethod {
125                name: "m".to_string(),
126            },
127            Error::Convert {
128                message: "bad".to_string(),
129            },
130        ] {
131            assert_eq!(error.position(), None);
132        }
133    }
134
135    #[test]
136    fn display_names_the_problem() {
137        assert_eq!(
138            Error::parse("expected a value", 4).to_string(),
139            "parse error at byte 4: expected a value"
140        );
141        assert_eq!(
142            Error::Unknown {
143                name: "count".to_string()
144            }
145            .to_string(),
146            "unknown variable 'count'"
147        );
148        assert_eq!(
149            Error::Missing {
150                name: "retries".to_string()
151            }
152            .to_string(),
153            "no such member 'retries'"
154        );
155        assert_eq!(
156            Error::OutOfRange { index: 3, len: 2 }.to_string(),
157            "index 3 is out of range for a list of 2"
158        );
159        assert_eq!(
160            Error::Type {
161                operation: "+",
162                got: "string"
163            }
164            .to_string(),
165            "no + for a string value"
166        );
167        assert_eq!(
168            Error::Overflow { operation: "*" }.to_string(),
169            "'*' overflowed an int"
170        );
171        assert_eq!(
172            Error::DivideByZero { operation: "%" }.to_string(),
173            "'%' by zero"
174        );
175        assert_eq!(
176            Error::Function {
177                name: "upper".to_string(),
178                message: "takes 1 argument(s), got 0".to_string()
179            }
180            .to_string(),
181            "'upper' failed: takes 1 argument(s), got 0"
182        );
183        assert_eq!(
184            Error::UnknownMethod {
185                name: "upper".to_string()
186            }
187            .to_string(),
188            "no such method 'upper'"
189        );
190        assert_eq!(
191            Error::Convert {
192                message: "not json".to_string()
193            }
194            .to_string(),
195            "not json"
196        );
197    }
198
199    #[test]
200    fn it_is_a_std_error_with_no_source() {
201        let error = Error::Unknown {
202            name: "x".to_string(),
203        };
204        let std_error: &dyn std::error::Error = &error;
205        assert!(std_error.source().is_none());
206        assert_eq!(format!("{std_error}"), "unknown variable 'x'");
207    }
208}