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