Skip to main content

doge_runtime/
error.rs

1use std::fmt;
2use std::rc::Rc;
3
4use crate::value::Value;
5
6/// The deepest a `bonk`-able call chain may nest before the runtime stops it
7/// with a catchable [`ErrorKind::RecursionLimit`] error.
8pub const RECURSION_LIMIT: usize = 1000;
9
10/// The category of a runtime error. Each variant maps to a `pls`/`oh no`
11/// catchable failure a Doge program can hit.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ErrorKind {
14    /// An operator or builtin was handed a value of the wrong type.
15    TypeError,
16    /// `/` or `//` or `%` with a zero divisor.
17    DivisionByZero,
18    /// A number too large to materialize where a bounded one is unavoidable — a
19    /// `**` exponent too big to compute, or a non-finite Float narrowed to an Int.
20    /// Ordinary Int arithmetic is arbitrary precision and never overflows.
21    Overflow,
22    /// List/Str index outside the valid range.
23    IndexOutOfBounds,
24    /// Dict lookup for a key that is not present.
25    KeyError,
26    /// A value was the right type but not a usable value (e.g. `int("dog")`).
27    ValueError,
28    /// An I/O or environment operation failed: a file could not be read/written,
29    /// or held bytes that were not valid text.
30    IOError,
31    /// A missing field or method on an object, or a method call on a value whose
32    /// type has no methods at all.
33    AttrError,
34    /// A `bonk` raised by the program itself.
35    Bonk,
36    /// An `amaze` assertion whose condition was falsy.
37    AssertError,
38    /// A call chain nested past [`RECURSION_LIMIT`].
39    RecursionLimit,
40}
41
42impl ErrorKind {
43    /// Short stable identifier, handy for tests and future diagnostics.
44    pub fn as_str(self) -> &'static str {
45        match self {
46            ErrorKind::TypeError => "TypeError",
47            ErrorKind::DivisionByZero => "DivisionByZero",
48            ErrorKind::Overflow => "Overflow",
49            ErrorKind::IndexOutOfBounds => "IndexOutOfBounds",
50            ErrorKind::KeyError => "KeyError",
51            ErrorKind::ValueError => "ValueError",
52            ErrorKind::IOError => "IOError",
53            ErrorKind::AttrError => "AttrError",
54            ErrorKind::Bonk => "Bonk",
55            ErrorKind::AssertError => "AssertError",
56            ErrorKind::RecursionLimit => "RecursionLimit",
57        }
58    }
59}
60
61/// Where an error was raised: the script path and 1-based line it came from.
62/// Present only on a re-raised error (`bonk err`), so its original location
63/// survives instead of being overwritten by the `bonk` site.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct ErrorLocation {
66    pub file: Rc<str>,
67    pub line: u32,
68}
69
70/// A catchable runtime error: a category plus a precise, plain-English message.
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct DogeError {
73    pub kind: ErrorKind,
74    pub message: String,
75    /// The raise site, carried only across `bonk err` so a re-raised error keeps
76    /// its original location. `None` on a freshly built error — the catch site
77    /// supplies the location when it becomes an [`error_value`].
78    pub location: Option<ErrorLocation>,
79}
80
81impl DogeError {
82    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
83        DogeError {
84            kind,
85            message: message.into(),
86            location: None,
87        }
88    }
89
90    pub fn type_error(message: impl Into<String>) -> Self {
91        DogeError::new(ErrorKind::TypeError, message)
92    }
93
94    pub fn division_by_zero(message: impl Into<String>) -> Self {
95        DogeError::new(ErrorKind::DivisionByZero, message)
96    }
97
98    pub fn overflow(message: impl Into<String>) -> Self {
99        DogeError::new(ErrorKind::Overflow, message)
100    }
101
102    pub fn index_out_of_bounds(message: impl Into<String>) -> Self {
103        DogeError::new(ErrorKind::IndexOutOfBounds, message)
104    }
105
106    pub fn key_error(message: impl Into<String>) -> Self {
107        DogeError::new(ErrorKind::KeyError, message)
108    }
109
110    pub fn value_error(message: impl Into<String>) -> Self {
111        DogeError::new(ErrorKind::ValueError, message)
112    }
113
114    pub fn attr_error(message: impl Into<String>) -> Self {
115        DogeError::new(ErrorKind::AttrError, message)
116    }
117
118    pub fn io_error(message: impl Into<String>) -> Self {
119        DogeError::new(ErrorKind::IOError, message)
120    }
121}
122
123impl fmt::Display for DogeError {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        write!(f, "{}", self.message)
126    }
127}
128
129impl std::error::Error for DogeError {}
130
131/// The innards of a caught `Error` value: the category, message, and the raise
132/// site, read through `err.type` / `err.message` / `err.file` / `err.line`.
133#[derive(Debug)]
134pub struct ErrorData {
135    pub kind: ErrorKind,
136    pub message: Rc<str>,
137    pub file: Rc<str>,
138    pub line: u32,
139}
140
141/// Build the error a `bonk <expr>` raises. Re-raising a caught `Error` value
142/// (`bonk err`) preserves its type, message, and original location; any other
143/// value raises a `Bonk` whose message is the value's display form, so `bonk 5`
144/// reads `5` and `bonk "much fail"` reads `much fail` — the text `bark` prints.
145pub fn bonk_error(value: &Value) -> DogeError {
146    match value {
147        Value::Error(e) => DogeError {
148            kind: e.kind,
149            message: e.message.to_string(),
150            location: Some(ErrorLocation {
151                file: e.file.clone(),
152                line: e.line,
153            }),
154        },
155        _ => DogeError::new(ErrorKind::Bonk, value.to_string()),
156    }
157}
158
159/// The default message for an `amaze` assertion that fails without one of its own.
160const ASSERT_DEFAULT_MESSAGE: &str = "such amaze. much false.";
161
162/// Build the error a failing `amaze <cond>` raises. With a message
163/// (`amaze cond, msg`) the message value's display form becomes the error text,
164/// mirroring `bonk`; without one it takes the default doge-flavored line.
165pub fn assert_error(message: Option<&Value>) -> DogeError {
166    let text = match message {
167        Some(value) => value.to_string(),
168        None => ASSERT_DEFAULT_MESSAGE.to_string(),
169    };
170    DogeError::new(ErrorKind::AssertError, text)
171}
172
173/// The value bound by `oh no err!`: a structured `Error` carrying the caught
174/// error's type, message, and location. A re-raised error keeps its embedded
175/// location; a fresh one takes the catch site's `file`/`line`.
176pub fn error_value(err: &DogeError, file: &str, line: u32) -> Value {
177    let (file, line) = match &err.location {
178        Some(loc) => (loc.file.clone(), loc.line),
179        None => (Rc::from(file), line),
180    };
181    Value::error(err.kind, &err.message, file, line)
182}
183
184/// Read a field off a caught `Error` value. A field other than `type`,
185/// `message`, `file`, or `line` is a catchable [`ErrorKind::AttrError`].
186pub fn error_field(err: &ErrorData, name: &str) -> DogeResult {
187    match name {
188        "type" => Ok(Value::str(err.kind.as_str())),
189        "message" => Ok(Value::Str(err.message.clone())),
190        "file" => Ok(Value::Str(err.file.clone())),
191        "line" => Ok(Value::int(err.line)),
192        _ => Err(DogeError::attr_error(format!(
193            "an Error has no field {name}"
194        ))),
195    }
196}
197
198/// Enter one call: fail (catchably) if the chain is already [`RECURSION_LIMIT`]
199/// deep, otherwise record the new depth. Pairs with [`exit_call`].
200pub fn enter_call(depth: &mut usize) -> DogeResult<()> {
201    if *depth >= RECURSION_LIMIT {
202        return Err(DogeError::new(
203            ErrorKind::RecursionLimit,
204            "too much recursion — more than 1000 calls deep",
205        ));
206    }
207    *depth += 1;
208    Ok(())
209}
210
211/// Leave one call, undoing a matching [`enter_call`].
212pub fn exit_call(depth: &mut usize) {
213    *depth = depth.saturating_sub(1);
214}
215
216/// The result of any fallible runtime operation. Defaults to yielding a
217/// [`crate::Value`] since that is what almost every operator and builtin
218/// produces.
219pub type DogeResult<T = crate::Value> = Result<T, DogeError>;
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn bonk_error_message_is_the_barked_form() {
227        assert_eq!(bonk_error(&Value::int(5)).message, "5");
228        assert_eq!(bonk_error(&Value::str("much fail")).message, "much fail");
229        assert_eq!(bonk_error(&Value::int(5)).kind, ErrorKind::Bonk);
230    }
231
232    #[test]
233    fn assert_error_uses_message_or_default() {
234        let with_message = assert_error(Some(&Value::str("age much wrong")));
235        assert_eq!(with_message.kind, ErrorKind::AssertError);
236        assert_eq!(with_message.message, "age much wrong");
237
238        let without = assert_error(None);
239        assert_eq!(without.kind, ErrorKind::AssertError);
240        assert_eq!(without.message, ASSERT_DEFAULT_MESSAGE);
241    }
242
243    #[test]
244    fn re_bonking_an_error_preserves_type_and_location() {
245        let caught = error_value(&DogeError::key_error("no such key"), "main.doge", 7);
246        let re_raised = bonk_error(&caught);
247        assert_eq!(re_raised.kind, ErrorKind::KeyError);
248        assert_eq!(re_raised.message, "no such key");
249        let loc = re_raised
250            .location
251            .expect("re-raised error keeps its location");
252        assert_eq!(&*loc.file, "main.doge");
253        assert_eq!(loc.line, 7);
254    }
255
256    #[test]
257    fn error_value_carries_type_message_and_catch_site() {
258        let err = DogeError::type_error("nope");
259        match error_value(&err, "script.doge", 3) {
260            Value::Error(e) => {
261                assert_eq!(e.kind, ErrorKind::TypeError);
262                assert_eq!(&*e.message, "nope");
263                assert_eq!(&*e.file, "script.doge");
264                assert_eq!(e.line, 3);
265            }
266            other => panic!("expected an Error, got {other:?}"),
267        }
268    }
269
270    #[test]
271    fn error_value_prefers_an_embedded_location_over_the_catch_site() {
272        let raised = error_value(&DogeError::overflow("too big"), "raise.doge", 2);
273        let re_raised = error_value(&bonk_error(&raised), "catch.doge", 99);
274        match re_raised {
275            Value::Error(e) => {
276                assert_eq!(&*e.file, "raise.doge");
277                assert_eq!(e.line, 2);
278            }
279            other => panic!("expected an Error, got {other:?}"),
280        }
281    }
282
283    #[test]
284    fn error_field_reads_the_four_fields_and_rejects_others() {
285        let value = error_value(&DogeError::value_error("bad"), "f.doge", 4);
286        let Value::Error(e) = value else {
287            panic!("expected an Error");
288        };
289        assert!(matches!(error_field(&e, "type").unwrap(), Value::Str(s) if &*s == "ValueError"));
290        assert!(matches!(error_field(&e, "message").unwrap(), Value::Str(s) if &*s == "bad"));
291        assert!(matches!(error_field(&e, "file").unwrap(), Value::Str(s) if &*s == "f.doge"));
292        assert!(crate::values_equal(
293            &error_field(&e, "line").unwrap(),
294            &Value::int(4)
295        ));
296        assert_eq!(
297            error_field(&e, "nope").unwrap_err().kind,
298            ErrorKind::AttrError
299        );
300    }
301
302    #[test]
303    fn enter_call_errors_past_limit() {
304        let mut depth = 0;
305        for _ in 0..RECURSION_LIMIT {
306            enter_call(&mut depth).expect("within the limit");
307        }
308        let err = enter_call(&mut depth).expect_err("one past the limit");
309        assert_eq!(err.kind, ErrorKind::RecursionLimit);
310        assert_eq!(depth, RECURSION_LIMIT);
311    }
312
313    #[test]
314    fn exit_call_saturates_at_zero() {
315        let mut depth = 0;
316        exit_call(&mut depth);
317        assert_eq!(depth, 0);
318    }
319}