use std::fmt;
use std::rc::Rc;
use crate::value::Value;
pub const RECURSION_LIMIT: usize = 1000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorKind {
TypeError,
DivisionByZero,
Overflow,
IndexOutOfBounds,
KeyError,
ValueError,
IOError,
AttrError,
Bonk,
AssertError,
RecursionLimit,
}
impl ErrorKind {
pub fn as_str(self) -> &'static str {
match self {
ErrorKind::TypeError => "TypeError",
ErrorKind::DivisionByZero => "DivisionByZero",
ErrorKind::Overflow => "Overflow",
ErrorKind::IndexOutOfBounds => "IndexOutOfBounds",
ErrorKind::KeyError => "KeyError",
ErrorKind::ValueError => "ValueError",
ErrorKind::IOError => "IOError",
ErrorKind::AttrError => "AttrError",
ErrorKind::Bonk => "Bonk",
ErrorKind::AssertError => "AssertError",
ErrorKind::RecursionLimit => "RecursionLimit",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ErrorLocation {
pub file: Rc<str>,
pub line: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DogeError {
pub kind: ErrorKind,
pub message: String,
pub location: Option<ErrorLocation>,
}
impl DogeError {
pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
DogeError {
kind,
message: message.into(),
location: None,
}
}
pub fn type_error(message: impl Into<String>) -> Self {
DogeError::new(ErrorKind::TypeError, message)
}
pub fn division_by_zero(message: impl Into<String>) -> Self {
DogeError::new(ErrorKind::DivisionByZero, message)
}
pub fn overflow(message: impl Into<String>) -> Self {
DogeError::new(ErrorKind::Overflow, message)
}
pub fn index_out_of_bounds(message: impl Into<String>) -> Self {
DogeError::new(ErrorKind::IndexOutOfBounds, message)
}
pub fn key_error(message: impl Into<String>) -> Self {
DogeError::new(ErrorKind::KeyError, message)
}
pub fn value_error(message: impl Into<String>) -> Self {
DogeError::new(ErrorKind::ValueError, message)
}
pub fn attr_error(message: impl Into<String>) -> Self {
DogeError::new(ErrorKind::AttrError, message)
}
pub fn io_error(message: impl Into<String>) -> Self {
DogeError::new(ErrorKind::IOError, message)
}
}
impl fmt::Display for DogeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for DogeError {}
#[derive(Debug)]
pub struct ErrorData {
pub kind: ErrorKind,
pub message: Rc<str>,
pub file: Rc<str>,
pub line: u32,
}
pub fn bonk_error(value: &Value) -> DogeError {
match value {
Value::Error(e) => DogeError {
kind: e.kind,
message: e.message.to_string(),
location: Some(ErrorLocation {
file: e.file.clone(),
line: e.line,
}),
},
_ => DogeError::new(ErrorKind::Bonk, value.to_string()),
}
}
const ASSERT_DEFAULT_MESSAGE: &str = "such amaze. much false.";
pub fn assert_error(message: Option<&Value>) -> DogeError {
let text = match message {
Some(value) => value.to_string(),
None => ASSERT_DEFAULT_MESSAGE.to_string(),
};
DogeError::new(ErrorKind::AssertError, text)
}
pub fn error_value(err: &DogeError, file: &str, line: u32) -> Value {
let (file, line) = match &err.location {
Some(loc) => (loc.file.clone(), loc.line),
None => (Rc::from(file), line),
};
Value::error(err.kind, &err.message, file, line)
}
pub fn error_field(err: &ErrorData, name: &str) -> DogeResult {
match name {
"type" => Ok(Value::str(err.kind.as_str())),
"message" => Ok(Value::Str(err.message.clone())),
"file" => Ok(Value::Str(err.file.clone())),
"line" => Ok(Value::int(err.line)),
_ => Err(DogeError::attr_error(format!(
"an Error has no field {name}"
))),
}
}
pub fn enter_call(depth: &mut usize) -> DogeResult<()> {
if *depth >= RECURSION_LIMIT {
return Err(DogeError::new(
ErrorKind::RecursionLimit,
"too much recursion — more than 1000 calls deep",
));
}
*depth += 1;
Ok(())
}
pub fn exit_call(depth: &mut usize) {
*depth = depth.saturating_sub(1);
}
pub type DogeResult<T = crate::Value> = Result<T, DogeError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bonk_error_message_is_the_barked_form() {
assert_eq!(bonk_error(&Value::int(5)).message, "5");
assert_eq!(bonk_error(&Value::str("much fail")).message, "much fail");
assert_eq!(bonk_error(&Value::int(5)).kind, ErrorKind::Bonk);
}
#[test]
fn assert_error_uses_message_or_default() {
let with_message = assert_error(Some(&Value::str("age much wrong")));
assert_eq!(with_message.kind, ErrorKind::AssertError);
assert_eq!(with_message.message, "age much wrong");
let without = assert_error(None);
assert_eq!(without.kind, ErrorKind::AssertError);
assert_eq!(without.message, ASSERT_DEFAULT_MESSAGE);
}
#[test]
fn re_bonking_an_error_preserves_type_and_location() {
let caught = error_value(&DogeError::key_error("no such key"), "main.doge", 7);
let re_raised = bonk_error(&caught);
assert_eq!(re_raised.kind, ErrorKind::KeyError);
assert_eq!(re_raised.message, "no such key");
let loc = re_raised
.location
.expect("re-raised error keeps its location");
assert_eq!(&*loc.file, "main.doge");
assert_eq!(loc.line, 7);
}
#[test]
fn error_value_carries_type_message_and_catch_site() {
let err = DogeError::type_error("nope");
match error_value(&err, "script.doge", 3) {
Value::Error(e) => {
assert_eq!(e.kind, ErrorKind::TypeError);
assert_eq!(&*e.message, "nope");
assert_eq!(&*e.file, "script.doge");
assert_eq!(e.line, 3);
}
other => panic!("expected an Error, got {other:?}"),
}
}
#[test]
fn error_value_prefers_an_embedded_location_over_the_catch_site() {
let raised = error_value(&DogeError::overflow("too big"), "raise.doge", 2);
let re_raised = error_value(&bonk_error(&raised), "catch.doge", 99);
match re_raised {
Value::Error(e) => {
assert_eq!(&*e.file, "raise.doge");
assert_eq!(e.line, 2);
}
other => panic!("expected an Error, got {other:?}"),
}
}
#[test]
fn error_field_reads_the_four_fields_and_rejects_others() {
let value = error_value(&DogeError::value_error("bad"), "f.doge", 4);
let Value::Error(e) = value else {
panic!("expected an Error");
};
assert!(matches!(error_field(&e, "type").unwrap(), Value::Str(s) if &*s == "ValueError"));
assert!(matches!(error_field(&e, "message").unwrap(), Value::Str(s) if &*s == "bad"));
assert!(matches!(error_field(&e, "file").unwrap(), Value::Str(s) if &*s == "f.doge"));
assert!(crate::values_equal(
&error_field(&e, "line").unwrap(),
&Value::int(4)
));
assert_eq!(
error_field(&e, "nope").unwrap_err().kind,
ErrorKind::AttrError
);
}
#[test]
fn enter_call_errors_past_limit() {
let mut depth = 0;
for _ in 0..RECURSION_LIMIT {
enter_call(&mut depth).expect("within the limit");
}
let err = enter_call(&mut depth).expect_err("one past the limit");
assert_eq!(err.kind, ErrorKind::RecursionLimit);
assert_eq!(depth, RECURSION_LIMIT);
}
#[test]
fn exit_call_saturates_at_zero() {
let mut depth = 0;
exit_call(&mut depth);
assert_eq!(depth, 0);
}
}