Lines
99 %
Functions
90.48 %
Branches
100 %
// Skipped under Miri: these tests compile+run wasm via wasmtime, whose
// Cranelift backend refuses to run under Miri.
#![cfg(not(miri))]
//! Runtime evaluation of HANDLER-CASE through the eval-mode (nomi-eval)
//! boundary wrapper: the catch actually fires, clauses run, condition
//! accessors read the live struct, and an unmatched raise propagates.
//! Codegen-level validation lives in `nomiscript/tests/codegen/handler_case.rs`.
use nms::interpreter::Interpreter;
use scripting::nomiscript::{Fraction, Value};
fn eval_one(src: &str) -> Value {
let mut interp = Interpreter::new(false).unwrap();
interp
.eval(src)
.unwrap_or_else(|e| panic!("eval {src:?}: {e}"))
.into_iter()
.next_back()
.unwrap_or_else(|| panic!("eval {src:?} produced no value"))
}
fn eval_err(src: &str) -> String {
match interp.eval(src) {
Ok(v) => panic!("eval {src:?} unexpectedly succeeded: {v:?}"),
Err(e) => e.to_string(),
fn n(v: i64) -> Value {
Value::Number(Fraction::from_integer(v))
fn s(v: &str) -> Value {
Value::String(v.to_string())
#[test]
fn basic_clause_catches_matching_raise() {
assert_eq!(
eval_one(r#"(handler-case (error 'oops "x") (oops (e) 7))"#),
n(7)
);
fn no_throw_body_returns_its_value_clauses_untouched() {
assert_eq!(eval_one("(handler-case 42 (oops (e) 7))"), n(42));
fn first_matching_clause_wins() {
eval_one(r#"(handler-case (error 'two "x") (one (e) 1) (two (e) 2))"#),
n(2),
fn catch_all_t_catches_unlisted_code() {
eval_one(r#"(handler-case (error 'whatever "x") (t (e) 99))"#),
n(99),
fn unmatched_with_no_catch_all_propagates() {
// The condition re-raises and escapes the handler-case; nms surfaces it
// as an execution error (it carries the code in the marker chain).
let err = eval_err(r#"(handler-case (error 'nope "x") (other (e) 1))"#);
assert!(
!err.is_empty(),
"an unmatched handler-case must propagate the raise"
fn error_message_accessor_reads_the_condition() {
eval_one(r#"(handler-case (error 'x "the-msg") (x (e) (error-message e)))"#),
s("the-msg"),
fn error_code_accessor_reads_the_upcased_code() {
// The reader upcases the raised symbol, so the wire code is "X".
eval_one(r#"(handler-case (error 'x "m") (x (e) (error-code e)))"#),
s("X"),
fn catch_all_binds_condition_for_accessors() {
eval_one(r#"(handler-case (error 'boom "details") (t (e) (error-message e)))"#),
s("details"),
fn nested_handler_case_inner_catches_first() {
// The inner handler-case catches 'inner; the outer never sees it.
eval_one(
r#"(handler-case
(handler-case (error 'inner "x") (inner (e) 1))
(t (e) 2))"#
),
n(1),
fn nested_handler_case_outer_catches_uncaught_inner() {
// The inner clause doesn't match 'boom, so it re-raises; the outer t
// catches it.
(handler-case (error 'boom "x") (other (e) 1))
fn handler_case_in_effect_position_still_catches() {
// In effect position (non-tail of a BEGIN) the handler-case must still
// emit its catch — it must not be elided as a const-folded value.
eval_one(r#"(begin (handler-case (error 'x "m") (x (e) 1)) 5)"#),
n(5),
fn return_from_inside_matching_clause_exits_block() {
// A `(return-from)` inside a matching clause body must resolve to the
// enclosing BLOCK through the handler-case frames (not branch to the
// wrong target). Returns 7, not the dead tail 99.
eval_one(r#"(block done (handler-case (error 'x "m") (x (e) (return-from done 7))) 99)"#),
n(7),
fn return_from_inside_catch_all_clause_exits_block() {
eval_one(r#"(block done (handler-case (error 'z "m") (t (e) (return-from done 7))) 99)"#),
fn diverging_body_let_bound_takes_clause_type() {
// The body always throws, so the form's runtime type comes from the
// clause (StringRef), not the body's placeholder. A let binding sizes
// its local from the eval-time type, so it must be the clause type or
// the StringRef value mis-fits an i32 local (invalid module).
eval_one(r#"(let* ((x (handler-case (error 'boom "m") (boom (e) "ok")))) x)"#),
s("ok"),
fn diverging_body_let_bound_ratio_clause() {
eval_one(r#"(let* ((x (handler-case (error 'b "m") (b (e) 11/10)))) x)"#),
Value::Number(Fraction::new(11, 10)),