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