From ccc193194dd2b5ed9e10ccb966a8b6671acee85c Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sun, 7 Dec 2025 16:01:38 +0100 Subject: [PATCH 1/5] Correct off-by-one error in spans Now the spans are as follows: 1-1 is a single character that is at pos 1. Previously 1-2. 3-4 is is a two character token at pos 3,4. Previously 3-5. etc. --- src/builtin/functions/numbers/math.rs | 10 +-- src/builtin/functions/time_operations.rs | 8 +- src/context/add_function.rs | 16 ++-- src/parse.rs | 56 ++++--------- tests/tests.rs | 100 +++++++++++------------ 5 files changed, 81 insertions(+), 109 deletions(-) diff --git a/src/builtin/functions/numbers/math.rs b/src/builtin/functions/numbers/math.rs index 5b642f3..9cb612f 100644 --- a/src/builtin/functions/numbers/math.rs +++ b/src/builtin/functions/numbers/math.rs @@ -52,8 +52,8 @@ mod tests { assert_eq!( ctx.eval_string("(sqrt -4.0)").unwrap_err().format(&ctx), r#"ERR TypeMismatch: sqrt: cannot compute square root of negative number: -4 -:1.7-1.11: at -4 -:1.1-1.12: at (sqrt -4) +:1.7-1.10: at -4 +:1.1-1.11: at (sqrt -4) "# ); } @@ -74,9 +74,9 @@ mod tests { assert_eq!( ctx.eval_string("(expt 0 -2)").unwrap_err().format(&ctx), r#"ERR OutOfRange: expt: cannot compute with base 0 and negative exponent -:1.7-1.8: at 0 -:1.9-1.11: at -2 -:1.1-1.12: at (expt 0 -2) +:1.7-1.7: at 0 +:1.9-1.10: at -2 +:1.1-1.11: at (expt 0 -2) "# ); } diff --git a/src/builtin/functions/time_operations.rs b/src/builtin/functions/time_operations.rs index ba9c11e..ebdb7ab 100644 --- a/src/builtin/functions/time_operations.rs +++ b/src/builtin/functions/time_operations.rs @@ -296,8 +296,8 @@ mod tests { .unwrap_err() .format(ctx), r#"ERR TypeMismatch: expected (ticks . hz) pair -:1.15-1.26: at (test . 10) -:1.1-1.38: at (time-less-p '(test . 10) 1758549822) +:1.15-1.25: at (test . 10) +:1.1-1.37: at (time-less-p '(test . 10) 1758549822) "# ); @@ -306,8 +306,8 @@ mod tests { .unwrap_err() .format(ctx), r#"ERR TypeMismatch: expected integer or (ticks . hz) pair -:1.15-1.19: at test -:1.1-1.31: at (time-less-p 'test 1758549822) +:1.15-1.18: at test +:1.1-1.30: at (time-less-p 'test 1758549822) "# ); diff --git a/src/context/add_function.rs b/src/context/add_function.rs index 54cda3a..028a7a4 100644 --- a/src/context/add_function.rs +++ b/src/context/add_function.rs @@ -524,8 +524,8 @@ mod tests { ctx, r#"(sum "hh" 10)"#, r#"ERR TypeMismatch: Expected number, got: String { value: "hh" } -:1.7-1.10: at "hh" -:1.1-1.14: at (sum "hh" 10) +:1.7-1.9: at "hh" +:1.1-1.13: at (sum "hh" 10) "#, ); @@ -552,8 +552,8 @@ mod tests { ctx, r#"(let ((horse "horse"))(cats horse))"#, r#"ERR InvalidArgument: No cats found -:1.23-1.35: at (cats horse) -:1.1-1.36: at (let ((horse "horse")) (cats horse)) +:1.23-1.34: at (cats horse) +:1.1-1.35: at (let ((horse "horse")) (cats horse)) "#, ); @@ -572,7 +572,7 @@ mod tests { ctx, "(add_round 2)", r#"ERR TypeMismatch: Too few arguments -:1.1-1.14: at (add_round 2) +:1.1-1.13: at (add_round 2) "#, ); @@ -582,7 +582,7 @@ mod tests { ctx, r#"(greet "Alice" "Peter")"#, r#"ERR TypeMismatch: Too many arguments -:1.1-1.24: at (greet "Alice" "Peter") +:1.1-1.23: at (greet "Alice" "Peter") "#, ); @@ -627,14 +627,14 @@ mod tests { ctx, "(power 2 3 4)", r#"ERR TypeMismatch: Too many arguments -:1.1-1.14: at (power 2 3 4) +:1.1-1.13: at (power 2 3 4) "#, ); eval_assert_error( ctx, "(power)", r#"ERR TypeMismatch: Too few arguments -:1.1-1.8: at (power) +:1.1-1.7: at (power) "#, ); Ok(()) diff --git a/src/parse.rs b/src/parse.rs index a01bb34..200f95e 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -61,7 +61,7 @@ impl Tokenizer<'_> { file_id, chars, line: 1, - pos: 1, + pos: 0, } } @@ -73,7 +73,7 @@ impl Tokenizer<'_> { self.chars.next().inspect(|ch| { if *ch == '\n' { self.line += 1; - self.pos = 1; + self.pos = 0; } else { self.pos += 1; } @@ -82,7 +82,7 @@ impl Tokenizer<'_> { fn read_string(&mut self) -> Option { assert_eq!(self.next_char()?, '"'); - let start_pos = (self.line, self.pos); + let start_pos = (self.line, self.pos + 1); let mut output = String::new(); while let Some(ch) = self.next_char() { match ch { @@ -98,7 +98,7 @@ impl Tokenizer<'_> { format!("Unknown escape char {}", e), Span { file_id: self.file_id, - start: (self.line, self.pos - 1), + start: (self.line, self.pos), end: (self.line, self.pos), }, ))); @@ -131,7 +131,7 @@ impl Tokenizer<'_> { } fn read_num_ident(&mut self) -> Option { - let start_pos = (self.line, self.pos); + let start_pos = (self.line, self.pos + 1); let mut output = String::new(); let mut first_char = true; let mut is_int = true; @@ -206,51 +206,31 @@ impl Iterator for Tokenizer<'_> { '(' => { self.next_char()?; return Some(Token::OpenParen { - span: Span::new( - self.file_id, - (self.line, self.pos - 1), - (self.line, self.pos), - ), + span: Span::new(self.file_id, (self.line, self.pos), (self.line, self.pos)), }); } ')' => { self.next_char()?; return Some(Token::CloseParen { - span: Span::new( - self.file_id, - (self.line, self.pos - 1), - (self.line, self.pos), - ), + span: Span::new(self.file_id, (self.line, self.pos), (self.line, self.pos)), }); } '\'' => { self.next_char()?; return Some(Token::Quote { - span: Span::new( - self.file_id, - (self.line, self.pos - 1), - (self.line, self.pos), - ), + span: Span::new(self.file_id, (self.line, self.pos), (self.line, self.pos)), }); } '`' => { self.next_char()?; return Some(Token::Backtick { - span: Span::new( - self.file_id, - (self.line, self.pos - 1), - (self.line, self.pos), - ), + span: Span::new(self.file_id, (self.line, self.pos), (self.line, self.pos)), }); } '.' => { self.next_char()?; return Some(Token::Dot { - span: Span::new( - self.file_id, - (self.line, self.pos - 1), - (self.line, self.pos), - ), + span: Span::new(self.file_id, (self.line, self.pos), (self.line, self.pos)), }); } '#' => { @@ -260,18 +240,14 @@ impl Iterator for Tokenizer<'_> { return Some(Token::SharpQuote { span: Span::new( self.file_id, - (self.line, self.pos - 2), + (self.line, self.pos - 1), (self.line, self.pos), ), }); } return Some(Token::ParserError(ParserError::syntax_error( "Unknown token #. Did you mean #' ?".to_string(), - Span::new( - self.file_id, - (self.line, self.pos - 1), - (self.line, self.pos), - ), + Span::new(self.file_id, (self.line, self.pos), (self.line, self.pos)), ))); } ',' => { @@ -281,17 +257,13 @@ impl Iterator for Tokenizer<'_> { return Some(Token::Splice { span: Span::new( self.file_id, - (self.line, self.pos - 2), + (self.line, self.pos - 1), (self.line, self.pos), ), }); } return Some(Token::Comma { - span: Span::new( - self.file_id, - (self.line, self.pos - 1), - (self.line, self.pos), - ), + span: Span::new(self.file_id, (self.line, self.pos), (self.line, self.pos)), }); } '"' => { diff --git a/tests/tests.rs b/tests/tests.rs index 4f21911..098b948 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -66,15 +66,15 @@ fn test_comparison_of_numbers() -> Result<(), Error> { tulisp_assert! { program: "(let ((a 10)) (> a))", error: r#"ERR OutOfRange: > requires at least 2 arguments -:1.15-1.20: at (> a) -:1.1-1.21: at (let ((a 10)) (> a)) +:1.15-1.19: at (> a) +:1.1-1.20: at (let ((a 10)) (> a)) "# } tulisp_assert! { program: r#"(> 10 "hello")"#, error: r#"ERR TypeMismatch: Expected number, found: "hello" -:1.8-1.14: at "hello" -:1.1-1.15: at (> 10 "hello") +:1.8-1.13: at "hello" +:1.1-1.14: at (> 10 "hello") "# } @@ -91,8 +91,8 @@ fn test_comparison_of_numbers() -> Result<(), Error> { tulisp_assert! { program: "(let ((a 10)) (>= a))", error: r#"ERR OutOfRange: >= requires at least 2 arguments -:1.15-1.21: at (>= a) -:1.1-1.22: at (let ((a 10)) (>= a)) +:1.15-1.20: at (>= a) +:1.1-1.21: at (let ((a 10)) (>= a)) "# } @@ -109,8 +109,8 @@ fn test_comparison_of_numbers() -> Result<(), Error> { tulisp_assert! { program: "(let ((a 10)) (< a))", error: r#"ERR OutOfRange: < requires at least 2 arguments -:1.15-1.20: at (< a) -:1.1-1.21: at (let ((a 10)) (< a)) +:1.15-1.19: at (< a) +:1.1-1.20: at (let ((a 10)) (< a)) "# } @@ -127,8 +127,8 @@ fn test_comparison_of_numbers() -> Result<(), Error> { tulisp_assert! { program: "(let ((a 10)) (<= a))", error: r#"ERR OutOfRange: <= requires at least 2 arguments -:1.15-1.21: at (<= a) -:1.1-1.22: at (let ((a 10)) (<= a)) +:1.15-1.20: at (<= a) +:1.1-1.21: at (let ((a 10)) (<= a)) "# } @@ -326,7 +326,7 @@ fn test_defun() -> Result<(), Error> { tulisp_assert! { program: "(defun j (&rest x y) nil) (j)", error: r#"ERR TypeMismatch: Too many &rest parameters -:1.1-1.26: at (defun j (&rest x y) nil) +:1.1-1.25: at (defun j (&rest x y) nil) "# } tulisp_assert! { @@ -348,13 +348,13 @@ fn test_defun() -> Result<(), Error> { tulisp_assert! { program: "(defun add (x y) (+ x y)) (add 10)", error: r#"ERR TypeMismatch: Too few arguments -:1.27-1.35: at (add 10) +:1.27-1.34: at (add 10) "# } tulisp_assert! { program: "(defun add (x y) (+ x y)) (add 10 20 30)", error: r#"ERR TypeMismatch: Too many arguments -:1.27-1.41: at (add 10 20 30) +:1.27-1.40: at (add 10 20 30) "# } tulisp_assert! { @@ -378,13 +378,13 @@ fn test_defun() -> Result<(), Error> { tulisp_assert! { program: "(defmacro inc (var) (list 'setq var (list '+ 1 var))) (let ((x 4)) (inc))", error: r#"ERR TypeMismatch: Too few arguments -:1.69-1.74: at (inc) +:1.69-1.73: at (inc) "# } tulisp_assert! { program: "(defmacro inc (var) (list 'setq var (list '+ 1 var))) (let ((x 4)) (inc 4 5))", error: r#"ERR TypeMismatch: Too many arguments -:1.69-1.78: at (inc 4 5) +:1.69-1.77: at (inc 4 5) "# } tulisp_assert! { @@ -469,15 +469,15 @@ fn test_eval() -> Result<(), Error> { tulisp_assert! { program: "(let ((y j) (j 10)) (funcall j))", error: r#"ERR TypeMismatch: Variable definition is void: j -:1.10-1.11: at j -:1.1-1.33: at (let ((y j) (j 10)) (funcall j)) +:1.10-1.10: at j +:1.1-1.32: at (let ((y j) (j 10)) (funcall j)) "# } tulisp_assert! { program: "(let ((j 10)) (+ j j))(+ j j)", error: r#"ERR TypeMismatch: Variable definition is void: j -:1.26-1.27: at j -:1.23-1.30: at (+ j j) +:1.26-1.26: at j +:1.23-1.29: at (+ j j) "# } Ok(()) @@ -488,7 +488,7 @@ fn test_strings() -> Result<(), Error> { tulisp_assert! { program: r##"(concat 'hello 'world)"##, error: r#"ERR TypeMismatch: Not a string: hello -:1.1-1.23: at (concat 'hello 'world) +:1.1-1.22: at (concat 'hello 'world) "# } tulisp_assert! { program: r##"(concat "hello" " world")"##, result: r#""hello world""# } @@ -527,13 +527,13 @@ fn test_cons() -> Result<(), Error> { tulisp_assert! { program: "(cons 1)", error: r#"ERR TypeMismatch: cons requires exactly 2 arguments -:1.1-1.9: at (cons 1) +:1.1-1.8: at (cons 1) "# }; tulisp_assert! { program: "(cons 1 2 3)", error: r#"ERR TypeMismatch: cons requires exactly 2 arguments -:1.1-1.13: at (cons 1 2 3) +:1.1-1.12: at (cons 1 2 3) "# }; Ok(()) @@ -552,13 +552,13 @@ fn test_quote() -> Result<(), Error> { tulisp_assert! { program: "(quote)", error: r#"ERR TypeMismatch: quote: expected one argument -:1.1-1.8: at (quote) +:1.1-1.7: at (quote) "# }; tulisp_assert! { program: "(quote 1 2)", error: r#"ERR TypeMismatch: quote: expected one argument -:1.1-1.12: at (quote 1 2) +:1.1-1.11: at (quote 1 2) "# }; Ok(()) @@ -612,9 +612,9 @@ fn test_lists() -> Result<(), Error> { (append items '(10))) "#, error: r#"ERR TypeMismatch: Variable definition is void: items -:3.23-3.28: at items -:3.15-3.35: at (append items '(10)) -:2.9-3.36: at (setq items (append items '(10))) +:3.23-3.27: at items +:3.15-3.34: at (append items '(10)) +:2.9-3.35: at (setq items (append items '(10))) "# } @@ -714,8 +714,8 @@ fn test_backquotes() -> Result<(), Error> { tulisp_assert! { program: r#"`(1 2 ,,(+ 10 20))"#, error: r#"ERR TypeMismatch: Unquote without backquote -:1.7-1.8: at ,,(+ 10 20) -:1.1-1.2: at `(1 2 ,,(+ 10 20)) +:1.7-1.7: at ,,(+ 10 20) +:1.1-1.1: at `(1 2 ,,(+ 10 20)) "#, } @@ -727,7 +727,7 @@ fn test_math() -> Result<(), Error> { tulisp_assert! { program: "(/ 10 0)", error: r#"ERR Undefined: Division by zero -:1.1-1.9: at (/ 10 0) +:1.1-1.8: at (/ 10 0) "#, } tulisp_assert! { @@ -737,8 +737,8 @@ fn test_math() -> Result<(), Error> { tulisp_assert! { program: "(let ((a 10) (b 0)) (/ a b))", error: r#"ERR Undefined: Division by zero -:1.21-1.28: at (/ a b) -:1.1-1.29: at (let ((a 10) (b 0)) (/ a b)) +:1.21-1.27: at (/ a b) +:1.1-1.28: at (let ((a 10) (b 0)) (/ a b)) "#, } @@ -779,13 +779,13 @@ fn test_rounding_operations() -> Result<(), Error> { tulisp_assert! { program: "(fround)", error: r#"ERR MissingArgument: fround: expected 1 argument. -:1.1-1.9: at (fround) +:1.1-1.8: at (fround) "#, } tulisp_assert! { program: "(fround 3.14 3.14)", error: r#"ERR MissingArgument: fround: expected only 1 argument. -:1.1-1.19: at (fround 3.14 3.14) +:1.1-1.18: at (fround 3.14 3.14) "#, } @@ -809,22 +809,22 @@ fn test_let() -> Result<(), Error> { (append kk (+ vv jj 1))) "#, error: r#"ERR TypeMismatch: Variable definition is void: kk -:4.19-4.21: at kk -:4.11-4.34: at (append kk (+ vv jj 1)) -:2.9-4.35: at (let ((vv (+ 55 1)) (jj 20)) (append kk (+ vv jj 1))) +:4.19-4.20: at kk +:4.11-4.33: at (append kk (+ vv jj 1)) +:2.9-4.34: at (let ((vv (+ 55 1)) (jj 20)) (append kk (+ vv jj 1))) "# } tulisp_assert! { program: "(let ((22 (+ 55 1)) (jj 20)) (+ vv jj 1))", error: r#"ERR TypeMismatch: Expected Symbol: Can't assign to 22 -:1.8-1.10: at 22 -:1.1-1.42: at (let ((22 (+ 55 1)) (jj 20)) (+ vv jj 1)) +:1.8-1.9: at 22 +:1.1-1.41: at (let ((22 (+ 55 1)) (jj 20)) (+ vv jj 1)) "# } tulisp_assert! { program: "(let (18 (vv (+ 55 1)) (jj 20)) (+ vv jj 1))", error: r#"ERR SyntaxError: varitems inside a let-varlist should be a var or a binding: 18 -:1.1-1.45: at (let (18 (vv (+ 55 1)) (jj 20)) (+ vv jj 1)) +:1.1-1.44: at (let (18 (vv (+ 55 1)) (jj 20)) (+ vv jj 1)) "# } @@ -958,8 +958,8 @@ fn test_sort() -> Result<(), Error> { tulisp_assert! { program: r#"(sort '("sort" "hello" "a" "world") '>)"#, error: r#"ERR TypeMismatch: Expected number, found: "world" -:1.29-1.35: at "world" -:1.1-1.40: at (sort '("sort" "hello" "a" "world") '>) +:1.29-1.34: at "world" +:1.1-1.39: at (sort '("sort" "hello" "a" "world") '>) "#, } tulisp_assert! { @@ -973,14 +973,14 @@ fn test_sort() -> Result<(), Error> { tulisp_assert! { program: "(sort '(20 10 30 15 45) '<<)", error: r#"ERR TypeMismatch: Variable definition is void: << -:1.26-1.28: at << -:1.1-1.29: at (sort '(20 10 30 15 45) '<<) +:1.26-1.27: at << +:1.1-1.28: at (sort '(20 10 30 15 45) '<<) "# } tulisp_assert! { program: "(sort '(20 10 30 15 45))", error: r#"ERR TypeMismatch: Too few arguments -:1.1-1.25: at (sort '(20 10 30 15 45)) +:1.1-1.24: at (sort '(20 10 30 15 45)) "#, } tulisp_assert! { @@ -1144,7 +1144,7 @@ fn test_typed_iter() -> Result<(), Error> { ctx: ctx, program: "(add_ints 20)", error: r#"ERR TypeMismatch: Expected a list, got 20 -:1.1-1.14: at (add_ints 20) +:1.1-1.13: at (add_ints 20) "# } Ok(()) @@ -1196,8 +1196,8 @@ fn test_any() -> Result<(), Error> { ctx: ctx, program: "(get_int 55)", error: r#"ERR TypeMismatch: Expected Any(Rc): 55 -:1.10-1.12: at 55 -:1.1-1.13: at (get_int 55) +:1.10-1.11: at 55 +:1.1-1.12: at (get_int 55) "# } Ok(()) @@ -1217,8 +1217,8 @@ fn test_load() -> Result<(), Error> { ctx: ctx, program: r#"(load "tests/bad-load.lisp")"#, error: r#"ERR ParsingError: Unexpected closing parenthesis -tests/bad-load.lisp:1.9-1.10: at nil -:1.1-1.29: at (load "tests/bad-load.lisp") +tests/bad-load.lisp:1.9-1.9: at nil +:1.1-1.28: at (load "tests/bad-load.lisp") "# } From cbc23d40964cf1bd90a4c2f0d7f7df9d57ffc851 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sun, 7 Dec 2025 16:16:18 +0100 Subject: [PATCH 2/5] Improve consistency of TypeMismatch error message formatting --- src/context/add_function.rs | 2 +- src/value.rs | 14 +++++++------- tests/tests.rs | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/context/add_function.rs b/src/context/add_function.rs index 028a7a4..f3e8809 100644 --- a/src/context/add_function.rs +++ b/src/context/add_function.rs @@ -523,7 +523,7 @@ mod tests { eval_assert_error( ctx, r#"(sum "hh" 10)"#, - r#"ERR TypeMismatch: Expected number, got: String { value: "hh" } + r#"ERR TypeMismatch: Expected number, got: "hh" :1.7-1.9: at "hh" :1.1-1.13: at (sum "hh" 10) "#, diff --git a/src/value.rs b/src/value.rs index 0b87923..db02320 100644 --- a/src/value.rs +++ b/src/value.rs @@ -634,7 +634,7 @@ impl TulispValue { } _ => Err(Error::new( ErrorKind::TypeMismatch, - format!("Expected symbol: {}", self), + format!("Expected symbol, got: {}", self), )), } } @@ -645,7 +645,7 @@ impl TulispValue { TulispValue::Float { value, .. } => Ok(*value), t => Err(Error::new( ErrorKind::TypeMismatch, - format!("Expected number, got: {:?}", t), + format!("Expected number, got: {}", t), )), } } @@ -657,7 +657,7 @@ impl TulispValue { TulispValue::Int { value, .. } => Ok(*value as f64), t => Err(Error::new( ErrorKind::TypeMismatch, - format!("Expected number, got: {:?}", t), + format!("Expected number, got: {}", t), )), } } @@ -668,7 +668,7 @@ impl TulispValue { TulispValue::Int { value, .. } => Ok(*value), t => Err(Error::new( ErrorKind::TypeMismatch, - format!("Expected integer: {:?}", t), + format!("Expected integer: {}", t), )), } } @@ -680,7 +680,7 @@ impl TulispValue { TulispValue::Int { value, .. } => Ok(*value), t => Err(Error::new( ErrorKind::TypeMismatch, - format!("Expected number, got {:?}", t), + format!("Expected number, got {}", t), )), } } @@ -752,7 +752,7 @@ impl TulispValue { TulispValue::String { value, .. } => Ok(value.to_owned()), _ => Err(Error::new( ErrorKind::TypeMismatch, - format!("Expected string: {}", self), + format!("Expected string, got: {}", self), )), } } @@ -763,7 +763,7 @@ impl TulispValue { TulispValue::Any(value) => Ok(value.clone()), _ => Err(Error::new( ErrorKind::TypeMismatch, - format!("Expected Any(Rc): {}", self), + format!("Expected Any(Rc), got: {}", self), )), } } diff --git a/tests/tests.rs b/tests/tests.rs index 098b948..cd8c603 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1195,7 +1195,7 @@ fn test_any() -> Result<(), Error> { tulisp_assert! { ctx: ctx, program: "(get_int 55)", - error: r#"ERR TypeMismatch: Expected Any(Rc): 55 + error: r#"ERR TypeMismatch: Expected Any(Rc), got: 55 :1.10-1.11: at 55 :1.1-1.12: at (get_int 55) "# From c3f5a3d09c5eae785d1ec7105628d97b82b6dbb8 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sun, 7 Dec 2025 16:16:49 +0100 Subject: [PATCH 3/5] Prevent duplicate spans in error backtrace --- src/context/add_function.rs | 8 ++++++++ src/error.rs | 3 +++ 2 files changed, 11 insertions(+) diff --git a/src/context/add_function.rs b/src/context/add_function.rs index f3e8809..5dade88 100644 --- a/src/context/add_function.rs +++ b/src/context/add_function.rs @@ -548,6 +548,14 @@ mod tests { ); eval_assert_equal(ctx, r#"(let ((a "stop"))(cats "dog" a "cat"))"#, "nil"); + eval_assert_error( + ctx, + r#"(cats 1 2 3)"#, + r#"ERR TypeMismatch: Expected string, got: 1 +:1.7-1.7: at 1 +:1.1-1.12: at (cats 1 2 3) +"#, + ); eval_assert_error( ctx, r#"(let ((horse "horse"))(cats horse))"#, diff --git a/src/error.rs b/src/error.rs index 4e02a4c..a1465f6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -80,6 +80,9 @@ impl Error { } } pub fn with_trace(mut self, span: TulispObject) -> Self { + if self.backtrace.last().map_or(false, |last| last.eq(&span)) { + return self; + } self.backtrace.push(span); self } From d5662ee724277b759794135c309188fa6532a579 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sun, 7 Dec 2025 17:51:18 +0100 Subject: [PATCH 4/5] Add typed constructors for Error to simplify creation --- src/builtin/functions/common.rs | 57 +++--- .../functions/comparison_of_strings.rs | 20 +-- src/builtin/functions/conditionals.rs | 13 +- src/builtin/functions/errors.rs | 19 +- src/builtin/functions/functions.rs | 73 +++----- src/builtin/functions/hash_table.rs | 5 +- .../numbers/arithmetic_operations.rs | 16 +- .../numbers/comparison_of_numbers.rs | 22 ++- src/builtin/functions/numbers/math.rs | 14 +- .../functions/numbers/rounding_operations.rs | 18 +- src/builtin/functions/time_operations.rs | 51 +++--- src/builtin/macros.rs | 7 +- src/cons.rs | 16 +- src/context.rs | 5 +- src/context/add_function.rs | 7 +- src/error.rs | 122 +++++++++---- src/eval.rs | 29 +-- src/lists.rs | 28 +-- src/macros.rs | 16 +- src/object.rs | 10 +- src/parse.rs | 58 ++---- src/value.rs | 165 +++++++----------- tests/tests.rs | 7 +- 23 files changed, 329 insertions(+), 449 deletions(-) diff --git a/src/builtin/functions/common.rs b/src/builtin/functions/common.rs index 53a468a..8894219 100644 --- a/src/builtin/functions/common.rs +++ b/src/builtin/functions/common.rs @@ -1,4 +1,4 @@ -use crate::{Error, ErrorKind, TulispContext, TulispObject, TulispValue}; +use crate::{Error, TulispContext, TulispObject, TulispValue}; pub(crate) fn eval_1_arg_special_form( ctx: &mut TulispContext, @@ -8,22 +8,19 @@ pub(crate) fn eval_1_arg_special_form( lambda: fn(&mut TulispContext, &TulispObject, &TulispObject) -> Result, ) -> Result { if args.null() { - return Err(Error::new( - ErrorKind::MissingArgument, - if has_rest { - format!("{}: expected at least 1 argument.", name) - } else { - format!("{}: expected 1 argument.", name) - }, - )); + return Err(Error::missing_argument(if has_rest { + format!("{}: expected at least 1 argument.", name) + } else { + format!("{}: expected 1 argument.", name) + })); } args.car_and_then(|arg1| { args.cdr_and_then(|rest| { if !has_rest && !rest.null() { - return Err(Error::new( - ErrorKind::MissingArgument, - format!("{}: expected only 1 argument.", name), - )); + return Err(Error::missing_argument(format!( + "{}: expected only 1 argument.", + name + ))); } lambda(ctx, arg1, rest) }) @@ -43,33 +40,27 @@ pub(crate) fn eval_2_arg_special_form( ) -> Result, ) -> Result { let TulispValue::List { cons: args, .. } = &*args.inner_ref() else { - return Err(Error::new( - ErrorKind::MissingArgument, - if has_rest { - format!("{}: expected at least 2 arguments.", name) - } else { - format!("{}: expected 2 arguments.", name) - }, - )); + return Err(Error::missing_argument(if has_rest { + format!("{}: expected at least 2 arguments.", name) + } else { + format!("{}: expected 2 arguments.", name) + })); }; if args.cdr().null() { - return Err(Error::new( - ErrorKind::MissingArgument, - if has_rest { - format!("{}: expected at least 2 arguments.", name) - } else { - format!("{}: expected 2 arguments.", name) - }, - )); + return Err(Error::missing_argument(if has_rest { + format!("{}: expected at least 2 arguments.", name) + } else { + format!("{}: expected 2 arguments.", name) + })); } let arg1 = args.car(); args.cdr().car_and_then(|arg2| { args.cdr().cdr_and_then(|rest| { if !has_rest && !rest.null() { - return Err(Error::new( - ErrorKind::MissingArgument, - format!("{}: expected only 2 arguments.", name), - )); + return Err(Error::missing_argument(format!( + "{}: expected only 2 arguments.", + name + ))); } lambda(ctx, arg1, arg2, rest) }) diff --git a/src/builtin/functions/comparison_of_strings.rs b/src/builtin/functions/comparison_of_strings.rs index 11ed107..b53690e 100644 --- a/src/builtin/functions/comparison_of_strings.rs +++ b/src/builtin/functions/comparison_of_strings.rs @@ -1,4 +1,4 @@ -use crate::{Error, ErrorKind, TulispContext, TulispObject, TulispValue, destruct_eval_bind}; +use crate::{Error, TulispContext, TulispObject, TulispValue, destruct_eval_bind}; fn string_cmp( ctx: &mut TulispContext, @@ -13,15 +13,15 @@ fn string_cmp( (TulispValue::String { value: string1 }, TulispValue::String { value: string2 }) => { Ok(oper(string1, string2).into()) } - (_, _) => Err(Error::new( - ErrorKind::TypeMismatch, - "Both arguments need to be strings".to_string(), - ) - .with_trace(if string1.stringp() { - args.cadr()? - } else { - args.car()? - })), + (_, _) => Err( + Error::type_mismatch("Both arguments need to be strings".to_string()).with_trace( + if string1.stringp() { + args.cadr()? + } else { + args.car()? + }, + ), + ), } } diff --git a/src/builtin/functions/conditionals.rs b/src/builtin/functions/conditionals.rs index 244eafc..5801458 100644 --- a/src/builtin/functions/conditionals.rs +++ b/src/builtin/functions/conditionals.rs @@ -1,5 +1,5 @@ use crate::{ - Error, ErrorKind, TulispContext, TulispObject, TulispValue, + Error, TulispContext, TulispObject, TulispValue, builtin::functions::common::eval_2_arg_special_form, destruct_bind, destruct_eval_bind, eval::{eval_and_then, eval_basic}, @@ -49,8 +49,7 @@ pub(crate) fn add(ctx: &mut TulispContext) { // Constructs for combining conditions fn not(ctx: &mut TulispContext, args: &TulispObject) -> Result { if args.cdr_and_then(|x| Ok(!x.null()))? { - return Err(Error::new( - ErrorKind::SyntaxError, + return Err(Error::syntax_error( "not: expected one argument".to_string(), )); } @@ -180,10 +179,10 @@ fn build_binding( }; if length(&binding)? > 2 { - return Err(Error::new( - ErrorKind::SyntaxError, - format!("`let` bindings can have only one value-form {}", &binding), - )); + return Err(Error::syntax_error(format!( + "`let` bindings can have only one value-form {}", + &binding + ))); } let var = binding.car()?; diff --git a/src/builtin/functions/errors.rs b/src/builtin/functions/errors.rs index 6195637..4a6de69 100644 --- a/src/builtin/functions/errors.rs +++ b/src/builtin/functions/errors.rs @@ -1,12 +1,9 @@ -use crate::{Error, ErrorKind, TulispContext, TulispObject, destruct_bind}; +use crate::{Error, ErrorKind, TulispContext, destruct_bind}; pub(crate) fn add(ctx: &mut TulispContext) { ctx.add_special_form("error", |ctx, args| { destruct_bind!((msg) = args); - Err(Error::new( - ErrorKind::LispError, - ctx.eval(&msg)?.as_string()?, - )) + Err(Error::lisp_error(ctx.eval(&msg)?.as_string()?)) }); ctx.add_special_form("catch", |ctx, args| { @@ -15,18 +12,16 @@ pub(crate) fn add(ctx: &mut TulispContext) { if let Err(ref e) = res { let tag = ctx.eval(&tag)?; if let ErrorKind::Throw(obj) = e.kind_ref() - && let Ok(true) = obj.car_and_then(|e_tag| Ok(e_tag.eq(&tag))) { - return obj.cdr(); - } + && let Ok(true) = obj.car_and_then(|e_tag| Ok(e_tag.eq(&tag))) + { + return obj.cdr(); + } } res }); ctx.add_special_form("throw", |ctx, args| { destruct_bind!((tag value) = args); - Err(Error::new( - ErrorKind::Throw(TulispObject::cons(ctx.eval(&tag)?, ctx.eval(&value)?)), - String::new(), - )) + Err(Error::throw(ctx.eval(&tag)?, ctx.eval(&value)?)) }); } diff --git a/src/builtin/functions/functions.rs b/src/builtin/functions/functions.rs index 25dd97f..7406630 100644 --- a/src/builtin/functions/functions.rs +++ b/src/builtin/functions/functions.rs @@ -5,7 +5,6 @@ use crate::context::Scope; use crate::context::TulispContext; use crate::destruct_eval_bind; use crate::error::Error; -use crate::error::ErrorKind; use crate::eval::DummyEval; use crate::eval::Eval; use crate::eval::eval; @@ -117,8 +116,7 @@ pub(crate) fn add(ctx: &mut TulispContext) { if prefix.stringp() { prefix.as_string()? } else { - return Err(Error::new( - ErrorKind::TypeMismatch, + return Err(Error::type_mismatch( "gensym: prefix must be a string".to_string(), )); } @@ -148,10 +146,7 @@ pub(crate) fn add(ctx: &mut TulispContext) { match ele.as_string() { Ok(ref s) => ret.push_str(s), _ => { - return Err(Error::new( - ErrorKind::TypeMismatch, - format!("Not a string: {}", ele), - )); + return Err(Error::type_mismatch(format!("Not a string: {}", ele))); } } } @@ -178,8 +173,7 @@ pub(crate) fn add(ctx: &mut TulispContext) { continue; } let Some(next_arg) = args.next() else { - return Err(Error::new( - ErrorKind::MissingArgument, + return Err(Error::missing_argument( "format has missing args".to_string(), )); }; @@ -191,10 +185,10 @@ pub(crate) fn add(ctx: &mut TulispContext) { 'd' => output.push_str(&next_arg.try_int()?.to_string()), 'f' => output.push_str(&next_arg.try_float()?.to_string()), _ => { - return Err(Error::new( - ErrorKind::SyntaxError, - format!("Invalid format operation: %{}", ch), - )); + return Err(Error::syntax_error(format!( + "Invalid format operation: %{}", + ch + ))); } } } @@ -230,15 +224,13 @@ pub(crate) fn add(ctx: &mut TulispContext) { fn setq(ctx: &mut TulispContext, args: &TulispObject) -> Result { let value = args.cdr_and_then(|args| { if args.null() { - return Err(Error::new( - ErrorKind::TypeMismatch, + return Err(Error::type_mismatch( "setq requires exactly 2 arguments".to_string(), )); } args.cdr_and_then(|x| { if !x.null() { - return Err(Error::new( - ErrorKind::TypeMismatch, + return Err(Error::type_mismatch( "setq requires exactly 2 arguments".to_string(), )); } @@ -253,15 +245,13 @@ pub(crate) fn add(ctx: &mut TulispContext) { fn set(ctx: &mut TulispContext, args: &TulispObject) -> Result { let value = args.cdr_and_then(|args| { if args.null() { - return Err(Error::new( - ErrorKind::TypeMismatch, + return Err(Error::type_mismatch( "setq requires exactly 2 arguments".to_string(), )); } args.cdr_and_then(|x| { if !x.null() { - return Err(Error::new( - ErrorKind::TypeMismatch, + return Err(Error::type_mismatch( "setq requires exactly 2 arguments".to_string(), )); } @@ -278,8 +268,7 @@ pub(crate) fn add(ctx: &mut TulispContext) { fn impl_let(ctx: &mut TulispContext, args: &TulispObject) -> Result { destruct_bind!((varlist &rest rest) = args); if !rest.consp() { - return Err(Error::new( - ErrorKind::TypeMismatch, + return Err(Error::type_mismatch( "let: expected varlist and body".to_string(), )); } @@ -290,26 +279,19 @@ pub(crate) fn add(ctx: &mut TulispContext) { } else if varitem.consp() { destruct_bind!((&optional name value &rest rest) = varitem); if name.null() { - return Err(Error::new( - ErrorKind::Undefined, - "let varitem requires name".to_string(), - )); + return Err(Error::undefined("let varitem requires name".to_string())); } if !rest.null() { - return Err(Error::new( - ErrorKind::Undefined, + return Err(Error::undefined( "let varitem has too many values".to_string(), )); } local.set(name, eval(ctx, &value)?)?; } else { - return Err(Error::new( - ErrorKind::SyntaxError, - format!( - "varitems inside a let-varlist should be a var or a binding: {}", - varitem - ), - )); + return Err(Error::syntax_error(format!( + "varitems inside a let-varlist should be a var or a binding: {}", + varitem + ))); }; } @@ -513,15 +495,13 @@ pub(crate) fn add(ctx: &mut TulispContext) { fn impl_cons(ctx: &mut TulispContext, args: &TulispObject) -> Result { let cdr = args.cdr_and_then(|args| { if args.null() { - return Err(Error::new( - ErrorKind::TypeMismatch, + return Err(Error::type_mismatch( "cons requires exactly 2 arguments".to_string(), )); } args.cdr_and_then(|x| { if !x.null() { - return Err(Error::new( - ErrorKind::TypeMismatch, + return Err(Error::type_mismatch( "cons requires exactly 2 arguments".to_string(), )); } @@ -632,14 +612,11 @@ pub(crate) fn add(ctx: &mut TulispContext) { match args.cdr_and_then(|x| Ok(x.null())) { Err(err) => return Err(err), Ok(false) => { - return Err(Error::new( - ErrorKind::TypeMismatch, - format!( - "Expected exatly 1 argument for {}. Got args: {}", - stringify!($name), - args - ), - )) + return Err(Error::type_mismatch(format!( + "Expected exatly 1 argument for {}. Got args: {}", + stringify!($name), + args + ))) } Ok(true) => {} } diff --git a/src/builtin/functions/hash_table.rs b/src/builtin/functions/hash_table.rs index 5cefb7c..ba966ed 100644 --- a/src/builtin/functions/hash_table.rs +++ b/src/builtin/functions/hash_table.rs @@ -1,4 +1,4 @@ -use crate::{Error, ErrorKind, TulispAny, TulispContext, TulispObject, destruct_eval_bind}; +use crate::{Error, TulispAny, TulispContext, TulispObject, destruct_eval_bind}; use std::{cell::RefCell, collections::HashMap, rc::Rc}; struct TulispObjectEql(TulispObject); @@ -41,8 +41,7 @@ impl std::fmt::Display for HashTable { pub(crate) fn add(ctx: &mut TulispContext) { ctx.add_special_form("make-hash-table", |_ctx, args| { if !args.null() { - return Err(Error::new( - ErrorKind::InvalidArgument, + return Err(Error::invalid_argument( "make-hash-table: expected no arguments.".to_string(), ) .with_trace(args.clone())); diff --git a/src/builtin/functions/numbers/arithmetic_operations.rs b/src/builtin/functions/numbers/arithmetic_operations.rs index a8c515e..502b1ba 100644 --- a/src/builtin/functions/numbers/arithmetic_operations.rs +++ b/src/builtin/functions/numbers/arithmetic_operations.rs @@ -1,6 +1,6 @@ use crate::builtin::functions::functions::reduce_with; +use crate::destruct_eval_bind; use crate::eval::eval; -use crate::{ErrorKind, destruct_eval_bind}; use crate::{Error, TulispContext, TulispObject, TulispValue}; @@ -21,8 +21,7 @@ pub(crate) fn add(ctx: &mut TulispContext) { reduce_with(ctx, args, binary_ops!(std::ops::Sub::sub)) } } else { - Err(Error::new( - ErrorKind::MissingArgument, + Err(Error::missing_argument( "Call to `sub` without any arguments".to_string(), )) } @@ -43,10 +42,7 @@ pub(crate) fn add(ctx: &mut TulispContext) { if *ele.inner_ref() == TulispValue::from(0) || *ele.inner_ref() == TulispValue::from(0.0) { - return Err(Error::new( - ErrorKind::Undefined, - "Division by zero".to_string(), - )); + return Err(Error::undefined("Division by zero".to_string())); } } reduce_with(ctx, rest, binary_ops!(std::ops::Div::div)) @@ -58,8 +54,7 @@ pub(crate) fn add(ctx: &mut TulispContext) { match &*number.inner_ref() { TulispValue::Int { value } => Ok((*value + 1).into()), TulispValue::Float { value } => Ok((*value + 1.0).into()), - _ => Err(Error::new( - ErrorKind::TypeMismatch, + _ => Err(Error::type_mismatch( "expected a number as argument.".to_string(), )), } @@ -70,8 +65,7 @@ pub(crate) fn add(ctx: &mut TulispContext) { match &*number.inner_ref() { TulispValue::Int { value } => Ok((*value - 1).into()), TulispValue::Float { value } => Ok((*value - 1.0).into()), - _ => Err(Error::new( - ErrorKind::TypeMismatch, + _ => Err(Error::type_mismatch( "expected a number as argument.".to_string(), )), } diff --git a/src/builtin/functions/numbers/comparison_of_numbers.rs b/src/builtin/functions/numbers/comparison_of_numbers.rs index 9a9a1e9..c2a27b5 100644 --- a/src/builtin/functions/numbers/comparison_of_numbers.rs +++ b/src/builtin/functions/numbers/comparison_of_numbers.rs @@ -28,27 +28,25 @@ macro_rules! compare_impl { ($name:ident, $symbol:literal) => { fn $name(ctx: &mut TulispContext, args: &TulispObject) -> Result { if args.null() || args.cdr_and_then(|x| Ok(x.null()))? { - return Err(Error::new( - crate::ErrorKind::OutOfRange, - format!("{} requires at least 2 arguments", $symbol), - )); + return Err(Error::out_of_range(format!( + "{} requires at least 2 arguments", + $symbol + ))); } args.car_and_then(|x| { ctx.eval_and_then(x, |ctx, first| { if !first.numberp() { - return Err(Error::new( - crate::ErrorKind::TypeMismatch, - format!("Expected number, found: {first}"), - ) + return Err(Error::type_mismatch(format!( + "Expected number, found: {first}" + )) .with_trace(args.car()?)); } args.cadr_and_then(|x| { ctx.eval_and_then(x, |ctx, second| { if !second.numberp() { - return Err(Error::new( - crate::ErrorKind::TypeMismatch, - format!("Expected number, found: {second}"), - ) + return Err(Error::type_mismatch(format!( + "Expected number, found: {second}" + )) .with_trace(args.cadr()?)); } if compare_ops!(std::cmp::PartialOrd::$name)(first, second)? { diff --git a/src/builtin/functions/numbers/math.rs b/src/builtin/functions/numbers/math.rs index 9cb612f..12d7bb5 100644 --- a/src/builtin/functions/numbers/math.rs +++ b/src/builtin/functions/numbers/math.rs @@ -7,13 +7,10 @@ pub(crate) fn add(ctx: &mut TulispContext) { let val: f64 = arg.try_float()?; // TODO: switch to return NaN for negative inputs. if val < 0.0 { - return Err(crate::Error::new( - crate::ErrorKind::TypeMismatch, - format!( - "sqrt: cannot compute square root of negative number: {}", - val - ), - ) + return Err(crate::Error::type_mismatch(format!( + "sqrt: cannot compute square root of negative number: {}", + val + )) .with_trace(arg)); } Ok(val.sqrt().into()) @@ -27,8 +24,7 @@ pub(crate) fn add(ctx: &mut TulispContext) { // TODO: switch to return Inf for 0^negative. if base_val == 0.0 && exponent_val < 0.0 { - return Err(crate::Error::new( - crate::ErrorKind::OutOfRange, + return Err(crate::Error::out_of_range( "expt: cannot compute with base 0 and negative exponent".to_string(), ) .with_trace(base) diff --git a/src/builtin/functions/numbers/rounding_operations.rs b/src/builtin/functions/numbers/rounding_operations.rs index 0930e43..f252430 100644 --- a/src/builtin/functions/numbers/rounding_operations.rs +++ b/src/builtin/functions/numbers/rounding_operations.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use crate::{ - Error, ErrorKind, TulispContext, TulispObject, TulispValue, + Error, TulispContext, TulispObject, TulispValue, builtin::functions::common::eval_1_arg_special_form, eval::eval_and_then, }; @@ -12,10 +12,10 @@ pub(crate) fn add(ctx: &mut TulispContext) { if x.floatp() { Ok(f64::round(x.as_float().unwrap()).into()) } else { - Err(Error::new( - ErrorKind::TypeMismatch, - format!("Expected float for fround. Got: {}", x), - )) + Err(Error::type_mismatch(format!( + "Expected float for fround. Got: {}", + x + ))) } }) }) @@ -28,10 +28,10 @@ pub(crate) fn add(ctx: &mut TulispContext) { if x.floatp() { Ok(f64::trunc(x.as_float().unwrap()).into()) } else { - Err(Error::new( - ErrorKind::TypeMismatch, - format!("Expected float for ftruncate. Got: {}", x), - )) + Err(Error::type_mismatch(format!( + "Expected float for ftruncate. Got: {}", + x + ))) } }) }) diff --git a/src/builtin/functions/time_operations.rs b/src/builtin/functions/time_operations.rs index ebdb7ab..9f8f6b6 100644 --- a/src/builtin/functions/time_operations.rs +++ b/src/builtin/functions/time_operations.rs @@ -1,14 +1,13 @@ -use crate::{Error, ErrorKind, TulispContext, TulispObject, destruct_eval_bind}; +use crate::{Error, TulispContext, TulispObject, destruct_eval_bind}; use std::time::Duration; pub(crate) fn add(ctx: &mut TulispContext) { ctx.add_special_form("current-time", |_ctx, args| { if !args.null() { - return Err(Error::new( - ErrorKind::SyntaxError, - "current-time takes no arguments".to_string(), - ) - .with_trace(args.clone())); + return Err( + Error::syntax_error("current-time takes no arguments".to_string()) + .with_trace(args.clone()), + ); } let usec_since_epoch = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -49,27 +48,22 @@ pub(crate) fn add(ctx: &mut TulispContext) { if let Ok(ticks) = obj.as_int() { Ok((ticks, 1)) } else { - Err( - Error::new(ErrorKind::TypeMismatch, "expected integer".to_string()) - .with_trace(obj.clone()), - ) + Err(Error::type_mismatch("expected integer".to_string()).with_trace(obj.clone())) } } else if let Some(cons) = obj.as_list_cons() { if let (Ok(ticks), Ok(hz)) = (cons.car().as_int(), cons.cdr().as_int()) { Ok((ticks, hz)) } else { - Err(Error::new( - ErrorKind::TypeMismatch, - "expected (ticks . hz) pair".to_string(), + Err( + Error::type_mismatch("expected (ticks . hz) pair".to_string()) + .with_trace(obj.clone()), ) - .with_trace(obj.clone())) } } else { - Err(Error::new( - ErrorKind::TypeMismatch, - "expected integer or (ticks . hz) pair".to_string(), + Err( + Error::type_mismatch("expected integer or (ticks . hz) pair".to_string()) + .with_trace(obj.clone()), ) - .with_trace(obj.clone())) } } @@ -136,8 +130,7 @@ pub(crate) fn add(ctx: &mut TulispContext) { } '.' => { if !prefix.is_empty() || has_comma || has_dot { - return Err(Error::new( - ErrorKind::SyntaxError, + return Err(Error::syntax_error( "Invalid format operation: '.' allowed only in the first place." .to_string(), ) @@ -148,8 +141,7 @@ pub(crate) fn add(ctx: &mut TulispContext) { } ',' => { if !prefix.is_empty() || has_comma || has_dot { - return Err(Error::new( - ErrorKind::SyntaxError, + return Err(Error::syntax_error( "Invalid format operation: ',' allowed only in the first place." .to_string(), ) @@ -159,19 +151,16 @@ pub(crate) fn add(ctx: &mut TulispContext) { continue; } _ => { - return Err(Error::new( - ErrorKind::SyntaxError, - format!("Invalid format operation: %{}", ch), - )); + return Err(Error::syntax_error(format!( + "Invalid format operation: %{}", + ch + ))); } }; let padding = if !prefix.is_empty() { prefix.parse::().map_err(|_| { - Error::new( - ErrorKind::SyntaxError, - format!("Invalid padding number: {}", prefix), - ) - .with_trace(format_string.clone()) + Error::syntax_error(format!("Invalid padding number: {}", prefix)) + .with_trace(format_string.clone()) })? } else { 0 diff --git a/src/builtin/macros.rs b/src/builtin/macros.rs index 4cbc724..5d1282c 100644 --- a/src/builtin/macros.rs +++ b/src/builtin/macros.rs @@ -1,4 +1,3 @@ -use crate::ErrorKind; use crate::TulispObject; use crate::TulispValue; use crate::context::TulispContext; @@ -39,15 +38,13 @@ fn thread_last(_ctx: &mut TulispContext, vv: &TulispObject) -> Result Result { if !args.consp() { - return Err(Error::new( - ErrorKind::TypeMismatch, + return Err(Error::type_mismatch( "quote: expected one argument".to_string(), )); } args.cdr_and_then(|cdr| { if !cdr.null() { - return Err(Error::new( - ErrorKind::TypeMismatch, + return Err(Error::type_mismatch( "quote: expected one argument".to_string(), )); } diff --git a/src/cons.rs b/src/cons.rs index 96f7a08..2948c9a 100644 --- a/src/cons.rs +++ b/src/cons.rs @@ -4,7 +4,6 @@ use crate::TulispContext; use crate::TulispObject; use crate::TulispValue; use crate::error::Error; -use crate::error::ErrorKind; use crate::eval::eval_basic; use crate::object::Span; @@ -50,10 +49,7 @@ impl Cons { }); last.with_span(span); } else { - return Err(Error::new( - ErrorKind::TypeMismatch, - "Cons: unable to push".to_string(), - )); + return Err(Error::type_mismatch("Cons: unable to push".to_string())); } Ok(()) } @@ -78,10 +74,7 @@ impl Cons { self.cdr = val.deep_copy()?; } } else { - return Err(Error::new( - ErrorKind::TypeMismatch, - format!("Unable to append: {}", val), - )); + return Err(Error::type_mismatch(format!("Unable to append: {}", val))); } Ok(()) } @@ -184,10 +177,7 @@ impl> Iterator for Iter { self.iter.next().map(|vv| { vv.clone().try_into().map_err(|_| { let tid = std::any::type_name::(); - Error::new( - ErrorKind::TypeMismatch, - format!("Iter<{}> can't handle {}", tid, vv), - ) + Error::type_mismatch(format!("Iter<{}> can't handle {}", tid, vv)) }) }) } diff --git a/src/context.rs b/src/context.rs index d0c49ac..e83c585 100644 --- a/src/context.rs +++ b/src/context.rs @@ -240,10 +240,7 @@ impl TulispContext { /// value. pub fn eval_file(&mut self, filename: &str) -> Result { let contents = fs::read_to_string(filename).map_err(|e| { - Error::new( - crate::ErrorKind::Undefined, - format!("Unable to read file: {filename}. Error: {e}"), - ) + Error::undefined(format!("Unable to read file: {filename}. Error: {e}")) })?; self.filenames.push(filename.to_owned()); diff --git a/src/context/add_function.rs b/src/context/add_function.rs index 5dade88..e87cc18 100644 --- a/src/context/add_function.rs +++ b/src/context/add_function.rs @@ -511,7 +511,7 @@ mod upto_5_args { #[cfg(test)] mod tests { use crate::test_utils::{eval_assert_equal, eval_assert_error}; - use crate::{Error, ErrorKind, Rest, TulispContext, TulispObject}; + use crate::{Error, Rest, TulispContext, TulispObject}; #[test] fn test_add_functions_only_rest() -> Result<(), crate::Error> { @@ -540,10 +540,7 @@ mod tests { return Ok(false.into()); } } - Err(crate::Error::new( - ErrorKind::InvalidArgument, - "No cats found".into(), - )) + Err(crate::Error::invalid_argument("No cats found")) }, ); diff --git a/src/error.rs b/src/error.rs index a1465f6..3f33307 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,38 +1,82 @@ use crate::{TulispContext, TulispObject}; -#[derive(Debug, Clone)] -pub enum ErrorKind { - InvalidArgument, - LispError, - MissingArgument, - NotImplemented, - OutOfRange, - ParsingError, - SyntaxError, - Throw(TulispObject), - TypeMismatch, - Undefined, - Uninitialized, +macro_rules! replace_expr { + ($_t:ty, $sub:ident) => { + $sub + }; } -impl std::fmt::Display for ErrorKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ErrorKind::InvalidArgument => f.write_str("InvalidArgument"), - ErrorKind::LispError => f.write_str("LispError"), - ErrorKind::MissingArgument => f.write_str("MissingArgument"), - ErrorKind::NotImplemented => f.write_str("NotImplemented"), - ErrorKind::OutOfRange => f.write_str("OutOfRange"), - ErrorKind::ParsingError => f.write_str("ParsingError"), - ErrorKind::SyntaxError => f.write_str("SyntaxError"), - ErrorKind::Throw(args) => write!(f, "Throw{}", args), - ErrorKind::TypeMismatch => f.write_str("TypeMismatch"), - ErrorKind::Undefined => f.write_str("Undefined"), - ErrorKind::Uninitialized => f.write_str("Uninitialized"), +/// A macro for defining the `ErrorKind` enum, the `Display` implementation for +/// it, and the constructors for the `Error` struct. +macro_rules! ErrorKind { + ($( + ($kind:ident$(($param:ty))? $(, $vis:vis $ctor:ident)?) + ),* $(,)?) => { + /// The kind of error that occurred. + #[derive(Debug, Clone)] + pub enum ErrorKind { + $( + $kind$(( $param ))?, + )* } - } + + impl std::fmt::Display for ErrorKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + $( + Self::$kind$((replace_expr!($param, vv)))? => { + write!( + f, + "{}", + stringify!($kind) + $(.to_owned() + "(" + &replace_expr!($param, vv).to_string() + ")")? + ) + }, + )* + } + } + } + + /// Constructors for [`Error`]. + impl Error { + $( + $( + #[doc = concat!( + "Creates a new [`Error`] with the `", + stringify!($kind), + "` kind and the given description." + )] + $vis fn $ctor(desc: impl Into) -> crate::error::Error { + Self { + kind: ErrorKind::$kind, + desc: desc.into(), + backtrace: vec![], + } + } + )? + )* + } + }; } +ErrorKind!( + (InvalidArgument, pub invalid_argument), + (LispError, pub lisp_error), + (NotImplemented, pub not_implemented), + (OutOfRange, pub out_of_range), + (TypeMismatch, pub type_mismatch), + (MissingArgument, pub(crate) missing_argument), + (Undefined, pub(crate) undefined), + (Uninitialized, pub(crate) uninitialized), + (ParsingError, pub(crate) parsing_error), + (SyntaxError, pub(crate) syntax_error), + (Throw(TulispObject)), // Custom constructor below +); + +/// Represents an error that occurred during Tulisp evaluation. +/// +/// Use [format](crate::Error::format) to produce a formatted representation of the error +/// including backtraces and source code spans. #[derive(Debug, Clone)] pub struct Error { kind: ErrorKind, @@ -41,6 +85,15 @@ pub struct Error { } impl Error { + /// Creates a new `Throw` error with the given tag and value. + pub fn throw(tag: TulispObject, value: TulispObject) -> Self { + Self { + kind: ErrorKind::Throw(TulispObject::cons(tag, value)), + desc: String::new(), + backtrace: vec![], + } + } + fn format_span(&self, ctx: &TulispContext, object: &TulispObject) -> String { if let Some(span) = object.span() { let filename = ctx.get_filename(span.file_id); @@ -53,6 +106,7 @@ impl Error { } } + /// Formats the error into a human-readable string, including backtrace information. pub fn format(&self, ctx: &TulispContext) -> String { let mut span_str = format!("ERR {}: {}", self.kind, self.desc); for span in &self.backtrace { @@ -72,13 +126,7 @@ impl Error { } impl Error { - pub fn new(kind: ErrorKind, desc: String) -> Self { - Error { - kind, - desc, - backtrace: vec![], - } - } + /// Adds a trace span to the error's backtrace. pub fn with_trace(mut self, span: TulispObject) -> Self { if self.backtrace.last().map_or(false, |last| last.eq(&span)) { return self; @@ -87,7 +135,7 @@ impl Error { self } - #[allow(dead_code)] + /// Returns the kind of the error. pub fn kind(&self) -> ErrorKind { self.kind.clone() } @@ -96,7 +144,7 @@ impl Error { &self.kind } - #[allow(dead_code)] + /// Returns the description of the error. pub fn desc(&self) -> String { self.desc.to_owned() } diff --git a/src/eval.rs b/src/eval.rs index 5c7d91b..70426e1 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -1,9 +1,5 @@ use crate::{ - TulispObject, TulispValue, - context::TulispContext, - error::{Error, ErrorKind}, - list, - value::DefunParams, + TulispObject, TulispValue, context::TulispContext, error::Error, list, value::DefunParams, }; pub(crate) trait Evaluator { @@ -63,18 +59,12 @@ fn zip_function_args( E::eval(ctx, &vv, &mut eval_result)?; eval_result.take().unwrap_or(vv) } else { - return Err(Error::new( - ErrorKind::TypeMismatch, - "Too few arguments".to_string(), - )); + return Err(Error::type_mismatch("Too few arguments".to_string())); }; param.param.set_scope(val)?; } if args_iter.next().is_some() { - return Err(Error::new( - ErrorKind::TypeMismatch, - "Too many arguments".to_string(), - )); + return Err(Error::type_mismatch("Too many arguments".to_string())); } Ok(()) } @@ -128,10 +118,7 @@ pub(crate) fn funcall( let expanded = macroexpand(ctx, list!(func.clone() ,@args.clone())?)?; eval(ctx, &expanded) } - _ => Err(Error::new( - ErrorKind::Undefined, - format!("function is void: {}", func), - )), + _ => Err(Error::undefined(format!("function is void: {}", func))), } } @@ -289,16 +276,12 @@ pub(crate) fn eval_basic<'a>( Some(eval_back_quote(ctx, value.clone()).map_err(|e| e.with_trace(expr.clone()))?); } TulispValue::Unquote { .. } => { - return Err(Error::new( - ErrorKind::TypeMismatch, + return Err(Error::type_mismatch( "Unquote without backquote".to_string(), )); } TulispValue::Splice { .. } => { - return Err(Error::new( - ErrorKind::TypeMismatch, - "Splice without backquote".to_string(), - )); + return Err(Error::type_mismatch("Splice without backquote".to_string())); } TulispValue::Sharpquote { value, .. } => { *result = Some(value.clone()); diff --git a/src/lists.rs b/src/lists.rs index c9bff46..d90a786 100644 --- a/src/lists.rs +++ b/src/lists.rs @@ -1,5 +1,5 @@ use crate::{ - Error, ErrorKind, TulispContext, TulispObject, + Error, TulispContext, TulispObject, eval::{DummyEval, eval, funcall}, list, }; @@ -9,7 +9,7 @@ pub fn length(list: &TulispObject) -> Result { list.base_iter() .count() .try_into() - .map_err(|e: _| Error::new(ErrorKind::OutOfRange, format!("{}", e))) + .map_err(|e: _| Error::out_of_range(format!("{}", e))) } /// Returns the last link in the given list. @@ -18,10 +18,10 @@ pub fn last(list: &TulispObject, n: Option) -> Result return Ok(list.clone()); } if !list.consp() { - return Err(Error::new( - ErrorKind::TypeMismatch, - format!("expected list, got: {}", list), - )); + return Err(Error::type_mismatch(format!( + "expected list, got: {}", + list + ))); } // TODO: emacs' implementation uses the `safe-length` function for this, but @@ -30,10 +30,10 @@ pub fn last(list: &TulispObject, n: Option) -> Result let len = length(list)?; if let Some(n) = n { if n < 0 { - return Err(Error::new( - ErrorKind::OutOfRange, - format!("n must be positive. got: {}", n), - )); + return Err(Error::out_of_range(format!( + "n must be positive. got: {}", + n + ))); } if n < len { return nthcdr(len - n, list.clone()); @@ -93,10 +93,10 @@ pub fn assoc( testfn: Option, ) -> Result { if !alist.listp() { - return Err(Error::new( - ErrorKind::TypeMismatch, - format!("expected alist. got: {}", alist), - )); + return Err(Error::type_mismatch(format!( + "expected alist. got: {}", + alist + ))); } if let Some(testfn) = testfn { let pred = eval(ctx, &testfn)?; diff --git a/src/macros.rs b/src/macros.rs index f96770c..d208a3d 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -116,8 +116,8 @@ fn main() -> Result<(), Error> { macro_rules! destruct_bind { (@reqr $vv:ident, $var:ident) => { if !$vv.consp() { - return Err($crate::Error::new( - $crate::ErrorKind::TypeMismatch,"Too few arguments".to_string() + return Err($crate::Error::type_mismatch( + "Too few arguments".to_string() )); } let $var = $vv.car()?; @@ -130,8 +130,8 @@ macro_rules! destruct_bind { (@reqr $vv:ident,) => {}; (@no-rest $vv:ident) => { if !$vv.null() { - return Err($crate::Error::new( - $crate::ErrorKind::TypeMismatch,"Too many arguments".to_string() + return Err($crate::Error::type_mismatch( + "Too many arguments".to_string() )); } }; @@ -176,8 +176,8 @@ macro_rules! destruct_bind { macro_rules! destruct_eval_bind { (@reqr $ctx:ident, $vv:ident, $var:ident) => { if !$vv.consp() { - return Err($crate::Error::new( - $crate::ErrorKind::TypeMismatch,"Too few arguments".to_string() + return Err($crate::Error::type_mismatch( + "Too few arguments".to_string() )); } let $var = $vv.car_and_then(|x| $ctx.eval(x))?; @@ -190,8 +190,8 @@ macro_rules! destruct_eval_bind { (@reqr $ctx:ident, $vv:ident,) => {}; (@no-rest $ctx:ident, $vv:ident) => { if !$vv.null() { - return Err($crate::Error::new( - $crate::ErrorKind::TypeMismatch,"Too many arguments".to_string() + return Err($crate::Error::type_mismatch( + "Too many arguments".to_string() )); } }; diff --git a/src/object.rs b/src/object.rs index 8d4609f..5563203 100644 --- a/src/object.rs +++ b/src/object.rs @@ -1,5 +1,5 @@ use crate::{ - ErrorKind, TulispValue, + TulispValue, cons::{self, Cons}, error::Error, value::TulispAny, @@ -168,10 +168,10 @@ impl TulispObject { /// ``` pub fn iter>(&self) -> Result, Error> { if !self.listp() { - return Err(Error::new( - ErrorKind::TypeMismatch, - format!("Expected a list, got {}", self.fmt_string()), - )); + return Err(Error::type_mismatch(format!( + "Expected a list, got {}", + self.fmt_string() + ))); } Ok(cons::Iter::new(self.base_iter())) } diff --git a/src/parse.rs b/src/parse.rs index 200f95e..e428e82 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, fmt::Write, iter::Peekable, str::Chars}; use crate::{ - Error, ErrorKind, TulispContext, TulispObject, TulispValue, + Error, TulispContext, TulispObject, TulispValue, eval::{eval, macroexpand}, object::Span, }; @@ -315,10 +315,8 @@ impl Parser<'_, '_> { let mut got_dot = false; loop { let Some(token) = self.tokenizer.peek() else { - return Err( - Error::new(ErrorKind::ParsingError, "Unclosed list".to_string()) - .with_trace(TulispObject::nil().with_span(Some(start_span))), - ); + return Err(Error::parsing_error("Unclosed list".to_string()) + .with_trace(TulispObject::nil().with_span(Some(start_span)))); }; match token { Token::CloseParen { span: end_span } => { @@ -352,8 +350,7 @@ impl Parser<'_, '_> { end: end_span.end, })); } else { - return Err(Error::new( - ErrorKind::ParsingError, + return Err(Error::parsing_error( "Expected only one item in list after dot.".to_string(), ) .with_trace(next)); @@ -381,8 +378,7 @@ impl Parser<'_, '_> { }; match token { Token::OpenParen { span } => self.parse_list(span).map(Some), - Token::CloseParen { span } => Err(Error::new( - ErrorKind::ParsingError, + Token::CloseParen { span } => Err(Error::parsing_error( "Unexpected closing parenthesis".to_string(), ) .with_trace(TulispValue::Nil.into_ref(Some(span)))), @@ -390,11 +386,8 @@ impl Parser<'_, '_> { let next = match self.parse_value()? { Some(next) => next, None => { - return Err(Error::new( - ErrorKind::ParsingError, - "Unexpected EOF".to_string(), - ) - .with_trace(TulispValue::Nil.into_ref(Some(span)))); + return Err(Error::parsing_error("Unexpected EOF".to_string()) + .with_trace(TulispValue::Nil.into_ref(Some(span)))); } }; Ok(Some( @@ -405,31 +398,22 @@ impl Parser<'_, '_> { let next = match self.parse_value()? { Some(next) => next, None => { - return Err(Error::new( - ErrorKind::ParsingError, - "Unexpected EOF".to_string(), - ) - .with_trace(TulispValue::Nil.into_ref(Some(span)))); + return Err(Error::parsing_error("Unexpected EOF".to_string()) + .with_trace(TulispValue::Nil.into_ref(Some(span)))); } }; Ok(Some( TulispValue::Backquote { value: next }.into_ref(Some(span)), )) } - Token::Dot { span } => Err(Error::new( - ErrorKind::ParsingError, - "Unexpected dot".to_string(), - ) - .with_trace(TulispValue::Nil.into_ref(Some(span)))), + Token::Dot { span } => Err(Error::parsing_error("Unexpected dot".to_string()) + .with_trace(TulispValue::Nil.into_ref(Some(span)))), Token::Comma { span } => { let next = match self.parse_value()? { Some(next) => next, None => { - return Err(Error::new( - ErrorKind::ParsingError, - "Unexpected EOF".to_string(), - ) - .with_trace(TulispValue::Nil.into_ref(Some(span)))); + return Err(Error::parsing_error("Unexpected EOF".to_string()) + .with_trace(TulispValue::Nil.into_ref(Some(span)))); } }; Ok(Some( @@ -440,11 +424,8 @@ impl Parser<'_, '_> { let next = match self.parse_value()? { Some(next) => next, None => { - return Err(Error::new( - ErrorKind::ParsingError, - "Unexpected EOF".to_string(), - ) - .with_trace(TulispValue::Nil.into_ref(Some(span)))); + return Err(Error::parsing_error("Unexpected EOF".to_string()) + .with_trace(TulispValue::Nil.into_ref(Some(span)))); } }; Ok(Some( @@ -486,11 +467,10 @@ impl Parser<'_, '_> { } } })), - Token::ParserError(err) => Err(Error::new( - ErrorKind::ParsingError, - format!("{:?} {}", err.kind, err.desc), - ) - .with_trace(TulispValue::Nil.into_ref(Some(err.span)))), + Token::ParserError(err) => { + Err(Error::parsing_error(format!("{:?} {}", err.kind, err.desc)) + .with_trace(TulispValue::Nil.into_ref(Some(err.span)))) + } } } diff --git a/src/value.rs b/src/value.rs index db02320..c36d6f8 100644 --- a/src/value.rs +++ b/src/value.rs @@ -2,7 +2,7 @@ use crate::{ TulispContext, TulispObject, cons::{self, Cons}, context::Scope, - error::{Error, ErrorKind}, + error::Error, object::Span, }; use std::{ @@ -51,8 +51,7 @@ impl TryFrom for DefunParams { fn try_from(params: TulispObject) -> Result { if !params.listp() { - return Err(Error::new( - ErrorKind::SyntaxError, + return Err(Error::syntax_error( "Parameter list needs to be a list".to_string(), )); } @@ -78,8 +77,7 @@ impl TryFrom for DefunParams { }); if is_rest { if params_iter.next().is_some() { - return Err(Error::new( - ErrorKind::TypeMismatch, + return Err(Error::type_mismatch( "Too many &rest parameters".to_string(), )); } @@ -114,10 +112,10 @@ impl SymbolBindings { #[inline(always)] pub(crate) fn set(&mut self, to_set: TulispObject) -> Result<(), Error> { if self.constant { - return Err(Error::new( - ErrorKind::Undefined, - format!("Can't set constant symbol: {}", self.name), - )); + return Err(Error::undefined(format!( + "Can't set constant symbol: {}", + self.name + ))); } if self.items.is_empty() { self.has_global = true; @@ -131,10 +129,10 @@ impl SymbolBindings { #[inline(always)] pub(crate) fn set_global(&mut self, to_set: TulispObject) -> Result<(), Error> { if self.constant { - return Err(Error::new( - ErrorKind::Undefined, - format!("Can't set constant symbol: {}", self.name), - )); + return Err(Error::undefined(format!( + "Can't set constant symbol: {}", + self.name + ))); } self.has_global = true; if self.items.is_empty() { @@ -148,10 +146,10 @@ impl SymbolBindings { #[inline(always)] pub(crate) fn set_scope(&mut self, to_set: TulispObject) -> Result<(), Error> { if self.constant { - return Err(Error::new( - ErrorKind::Undefined, - format!("Can't set constant symbol: {}", self.name), - )); + return Err(Error::undefined(format!( + "Can't set constant symbol: {}", + self.name + ))); } self.items.push(to_set); Ok(()) @@ -170,10 +168,10 @@ impl SymbolBindings { #[inline(always)] pub(crate) fn unset(&mut self) -> Result<(), Error> { if self.items.is_empty() { - return Err(Error::new( - ErrorKind::Uninitialized, - format!("Can't unbind from unassigned symbol: {}", self.name), - )); + return Err(Error::uninitialized(format!( + "Can't unbind from unassigned symbol: {}", + self.name + ))); } self.items.pop(); Ok(()) @@ -187,10 +185,10 @@ impl SymbolBindings { #[inline(always)] pub(crate) fn get(&self) -> Result { if self.items.is_empty() { - return Err(Error::new( - ErrorKind::TypeMismatch, - format!("Variable definition is void: {}", self.name), - )); + return Err(Error::type_mismatch(format!( + "Variable definition is void: {}", + self.name + ))); } Ok(self.items.last().unwrap().clone()) } @@ -334,10 +332,7 @@ impl PartialEq for TulispValue { /// Formats tulisp lists non-recursively. fn fmt_list(mut vv: TulispObject, f: &mut std::fmt::Formatter<'_>) -> Result<(), Error> { if let Err(e) = f.write_char('(') { - return Err(Error::new( - ErrorKind::Undefined, - format!("When trying to 'fmt': {}", e), - )); + return Err(Error::undefined(format!("When trying to 'fmt': {}", e))); }; let mut add_space = false; loop { @@ -345,29 +340,21 @@ fn fmt_list(mut vv: TulispObject, f: &mut std::fmt::Formatter<'_>) -> Result<(), if !add_space { add_space = true; } else if let Err(e) = f.write_char(' ') { - return Err(Error::new( - ErrorKind::Undefined, - format!("When trying to 'fmt': {}", e), - )); + return Err(Error::undefined(format!("When trying to 'fmt': {}", e))); }; - write!(f, "{}", vv.car()?).map_err(|e| { - Error::new(ErrorKind::Undefined, format!("When trying to 'fmt': {}", e)) - })?; + write!(f, "{}", vv.car()?) + .map_err(|e| Error::undefined(format!("When trying to 'fmt': {}", e)))?; if rest.null() { break; } else if !rest.consp() { - write!(f, " . {}", rest).map_err(|e| { - Error::new(ErrorKind::Undefined, format!("When trying to 'fmt': {}", e)) - })?; + write!(f, " . {}", rest) + .map_err(|e| Error::undefined(format!("When trying to 'fmt': {}", e)))?; break; }; vv = rest; } if let Err(e) = f.write_char(')') { - return Err(Error::new( - ErrorKind::Undefined, - format!("When trying to 'fmt': {}", e), - )); + return Err(Error::undefined(format!("When trying to 'fmt': {}", e))); }; Ok(()) } @@ -434,8 +421,7 @@ impl TulispValue { { value.set(to_set) } else { - Err(Error::new( - ErrorKind::TypeMismatch, + Err(Error::type_mismatch( "Can bind values only to Symbols".to_string(), )) } @@ -447,8 +433,7 @@ impl TulispValue { { value.set_global(to_set) } else { - Err(Error::new( - ErrorKind::TypeMismatch, + Err(Error::type_mismatch( "Can bind values only to Symbols".to_string(), )) } @@ -460,10 +445,9 @@ impl TulispValue { { value.set_scope(to_set) } else { - Err(Error::new( - ErrorKind::TypeMismatch, - format!("Expected Symbol: Can't assign to {self}"), - )) + Err(Error::type_mismatch(format!( + "Expected Symbol: Can't assign to {self}" + ))) } } @@ -486,8 +470,7 @@ impl TulispValue { { value.unset() } else { - Err(Error::new( - ErrorKind::TypeMismatch, + Err(Error::type_mismatch( "Can unbind only from Symbols".to_string(), )) } @@ -528,8 +511,7 @@ impl TulispValue { } value.get() } else { - Err(Error::new( - ErrorKind::TypeMismatch, + Err(Error::type_mismatch( "Can get only from Symbols".to_string(), )) } @@ -583,10 +565,7 @@ impl TulispValue { *self = TulispValue::List { cons, ctxobj }; Ok(()) } else { - Err(Error::new( - ErrorKind::TypeMismatch, - "unable to push".to_string(), - )) + Err(Error::type_mismatch("unable to push".to_string())) } } @@ -606,10 +585,7 @@ impl TulispValue { } Ok(()) } else { - Err(Error::new( - ErrorKind::TypeMismatch, - format!("unable to append: {}", val), - )) + Err(Error::type_mismatch(format!("unable to append: {}", val))) } } @@ -632,10 +608,10 @@ impl TulispValue { TulispValue::Symbol { value } | TulispValue::LexicalBinding { value, .. } => { Ok(value.name.to_string()) } - _ => Err(Error::new( - ErrorKind::TypeMismatch, - format!("Expected symbol, got: {}", self), - )), + _ => Err(Error::type_mismatch(format!( + "Expected symbol, got: {}", + self + ))), } } @@ -643,10 +619,7 @@ impl TulispValue { pub(crate) fn as_float(&self) -> Result { match self { TulispValue::Float { value, .. } => Ok(*value), - t => Err(Error::new( - ErrorKind::TypeMismatch, - format!("Expected number, got: {}", t), - )), + t => Err(Error::type_mismatch(format!("Expected number, got: {}", t))), } } @@ -655,10 +628,7 @@ impl TulispValue { match self { TulispValue::Float { value, .. } => Ok(*value), TulispValue::Int { value, .. } => Ok(*value as f64), - t => Err(Error::new( - ErrorKind::TypeMismatch, - format!("Expected number, got: {}", t), - )), + t => Err(Error::type_mismatch(format!("Expected number, got: {}", t))), } } @@ -666,10 +636,7 @@ impl TulispValue { pub(crate) fn as_int(&self) -> Result { match self { TulispValue::Int { value, .. } => Ok(*value), - t => Err(Error::new( - ErrorKind::TypeMismatch, - format!("Expected integer: {}", t), - )), + t => Err(Error::type_mismatch(format!("Expected integer: {}", t))), } } @@ -678,10 +645,7 @@ impl TulispValue { match self { TulispValue::Float { value, .. } => Ok(value.trunc() as i64), TulispValue::Int { value, .. } => Ok(*value), - t => Err(Error::new( - ErrorKind::TypeMismatch, - format!("Expected number, got {}", t), - )), + t => Err(Error::type_mismatch(format!("Expected number, got {}", t))), } } @@ -750,10 +714,10 @@ impl TulispValue { pub(crate) fn as_string(&self) -> Result { match self { TulispValue::String { value, .. } => Ok(value.to_owned()), - _ => Err(Error::new( - ErrorKind::TypeMismatch, - format!("Expected string, got: {}", self), - )), + _ => Err(Error::type_mismatch(format!( + "Expected string, got: {}", + self + ))), } } @@ -761,10 +725,10 @@ impl TulispValue { pub(crate) fn as_any(&self) -> Result, Error> { match self { TulispValue::Any(value) => Ok(value.clone()), - _ => Err(Error::new( - ErrorKind::TypeMismatch, - format!("Expected Any(Rc), got: {}", self), - )), + _ => Err(Error::type_mismatch(format!( + "Expected Any(Rc), got: {}", + self + ))), } } @@ -894,8 +858,8 @@ macro_rules! make_cxr_and_then { match self { TulispValue::List { cons, .. } => cons.$($step)+(func), TulispValue::Nil => Ok(Out::default()), - _ => Err(Error::new( - ErrorKind::TypeMismatch, + _ => Err(Error::type_mismatch( + format!("cxr: Not a Cons: {}", self), )), @@ -914,10 +878,7 @@ impl TulispValue { match self { TulispValue::List { cons, .. } => step(cons), TulispValue::Nil => Ok(TulispObject::nil()), - _ => Err(Error::new( - ErrorKind::TypeMismatch, - format!("cxr: Not a Cons: {}", self), - )), + _ => Err(Error::type_mismatch(format!("cxr: Not a Cons: {}", self))), } } @@ -963,10 +924,7 @@ impl TulispValue { match self { TulispValue::List { cons, .. } => func(cons.car()), TulispValue::Nil => Ok(Out::default()), - _ => Err(Error::new( - ErrorKind::TypeMismatch, - format!("cxr: Not a Cons: {}", self), - )), + _ => Err(Error::type_mismatch(format!("cxr: Not a Cons: {}", self))), } } @@ -978,10 +936,7 @@ impl TulispValue { match self { TulispValue::List { cons, .. } => func(cons.cdr()), TulispValue::Nil => Ok(Out::default()), - _ => Err(Error::new( - ErrorKind::TypeMismatch, - format!("cxr: Not a Cons: {}", self), - )), + _ => Err(Error::type_mismatch(format!("cxr: Not a Cons: {}", self))), } } diff --git a/tests/tests.rs b/tests/tests.rs index cd8c603..f2d43e3 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1178,12 +1178,7 @@ fn test_any() -> Result<(), Error> { inp.downcast::() .map(|vv| vv.value) - .map_err(|_| { - Error::new( - tulisp::ErrorKind::TypeMismatch, - "Not the any thing we wanted.".to_owned(), - ) - }) + .map_err(|_| Error::type_mismatch("Not the any thing we wanted.".to_owned())) .map(TulispObject::from) }); From 1b1ad99672493121415847efd03b25d708b4dbcc Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sun, 7 Dec 2025 18:06:18 +0100 Subject: [PATCH 5/5] Add tests for error handling with catch/throw and error function Also fix a formatting issue in Error::format. --- src/builtin/functions/errors.rs | 27 +++++++++++++++++++++++++++ src/error.rs | 10 +++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/builtin/functions/errors.rs b/src/builtin/functions/errors.rs index 4a6de69..3560a70 100644 --- a/src/builtin/functions/errors.rs +++ b/src/builtin/functions/errors.rs @@ -25,3 +25,30 @@ pub(crate) fn add(ctx: &mut TulispContext) { Err(Error::throw(ctx.eval(&tag)?, ctx.eval(&value)?)) }); } + +#[cfg(test)] +mod tests { + use crate::TulispContext; + use crate::test_utils::{eval_assert_equal, eval_assert_error}; + + #[test] + fn test_error_handling() { + let mut ctx = TulispContext::new(); + eval_assert_equal(&mut ctx, "(catch 'my-tag (throw 'my-tag 42))", "42"); + eval_assert_error( + &mut ctx, + "(catch 'my-tag (throw 'other-tag 42))", + r#"ERR Throw((other-tag . 42)): +:1.16-1.36: at (throw 'other-tag 42) +:1.1-1.37: at (catch 'my-tag (throw 'other-tag 42)) +"#, + ); + eval_assert_error( + &mut ctx, + r#"(error "Something went wrong!")"#, + r#"ERR LispError: Something went wrong! +:1.1-1.31: at (error "Something went wrong!") +"#, + ); + } +} diff --git a/src/error.rs b/src/error.rs index 3f33307..d3a13f5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -108,7 +108,15 @@ impl Error { /// Formats the error into a human-readable string, including backtrace information. pub fn format(&self, ctx: &TulispContext) -> String { - let mut span_str = format!("ERR {}: {}", self.kind, self.desc); + let mut span_str = format!( + "ERR {}:{}", + self.kind, + if self.desc.is_empty() { + String::new() + } else { + format!(" {}", self.desc) + } + ); for span in &self.backtrace { let prefix = self.format_span(ctx, span); if prefix.is_empty() {