1
// Skipped under Miri: these tests compile+run wasm via wasmtime, whose
2
// Cranelift backend refuses to run under Miri.
3
#![cfg(not(miri))]
4

            
5
//! Runtime evaluation of HANDLER-CASE through the eval-mode (nomi-eval)
6
//! boundary wrapper: the catch actually fires, clauses run, condition
7
//! accessors read the live struct, and an unmatched raise propagates.
8
//! Codegen-level validation lives in `nomiscript/tests/codegen/handler_case.rs`.
9

            
10
use nms::interpreter::Interpreter;
11
use scripting::nomiscript::{Fraction, Value};
12

            
13
14
fn eval_one(src: &str) -> Value {
14
14
    let mut interp = Interpreter::new(false).unwrap();
15
14
    interp
16
14
        .eval(src)
17
14
        .unwrap_or_else(|e| panic!("eval {src:?}: {e}"))
18
14
        .into_iter()
19
14
        .next_back()
20
14
        .unwrap_or_else(|| panic!("eval {src:?} produced no value"))
21
14
}
22

            
23
1
fn eval_err(src: &str) -> String {
24
1
    let mut interp = Interpreter::new(false).unwrap();
25
1
    match interp.eval(src) {
26
        Ok(v) => panic!("eval {src:?} unexpectedly succeeded: {v:?}"),
27
1
        Err(e) => e.to_string(),
28
    }
29
1
}
30

            
31
9
fn n(v: i64) -> Value {
32
9
    Value::Number(Fraction::from_integer(v))
33
9
}
34

            
35
4
fn s(v: &str) -> Value {
36
4
    Value::String(v.to_string())
37
4
}
38

            
39
#[test]
40
1
fn basic_clause_catches_matching_raise() {
41
1
    assert_eq!(
42
1
        eval_one(r#"(handler-case (error 'oops "x") (oops (e) 7))"#),
43
1
        n(7)
44
    );
45
1
}
46

            
47
#[test]
48
1
fn no_throw_body_returns_its_value_clauses_untouched() {
49
1
    assert_eq!(eval_one("(handler-case 42 (oops (e) 7))"), n(42));
50
1
}
51

            
52
#[test]
53
1
fn first_matching_clause_wins() {
54
1
    assert_eq!(
55
1
        eval_one(r#"(handler-case (error 'two "x") (one (e) 1) (two (e) 2))"#),
56
1
        n(2),
57
    );
58
1
}
59

            
60
#[test]
61
1
fn catch_all_t_catches_unlisted_code() {
62
1
    assert_eq!(
63
1
        eval_one(r#"(handler-case (error 'whatever "x") (t (e) 99))"#),
64
1
        n(99),
65
    );
66
1
}
67

            
68
#[test]
69
1
fn unmatched_with_no_catch_all_propagates() {
70
    // The condition re-raises and escapes the handler-case; nms surfaces it
71
    // as an execution error (it carries the code in the marker chain).
72
1
    let err = eval_err(r#"(handler-case (error 'nope "x") (other (e) 1))"#);
73
1
    assert!(
74
1
        !err.is_empty(),
75
        "an unmatched handler-case must propagate the raise"
76
    );
77
1
}
78

            
79
#[test]
80
1
fn error_message_accessor_reads_the_condition() {
81
1
    assert_eq!(
82
1
        eval_one(r#"(handler-case (error 'x "the-msg") (x (e) (error-message e)))"#),
83
1
        s("the-msg"),
84
    );
85
1
}
86

            
87
#[test]
88
1
fn error_code_accessor_reads_the_upcased_code() {
89
    // The reader upcases the raised symbol, so the wire code is "X".
90
1
    assert_eq!(
91
1
        eval_one(r#"(handler-case (error 'x "m") (x (e) (error-code e)))"#),
92
1
        s("X"),
93
    );
94
1
}
95

            
96
#[test]
97
1
fn catch_all_binds_condition_for_accessors() {
98
1
    assert_eq!(
99
1
        eval_one(r#"(handler-case (error 'boom "details") (t (e) (error-message e)))"#),
100
1
        s("details"),
101
    );
102
1
}
103

            
104
#[test]
105
1
fn nested_handler_case_inner_catches_first() {
106
    // The inner handler-case catches 'inner; the outer never sees it.
107
1
    assert_eq!(
108
1
        eval_one(
109
1
            r#"(handler-case
110
1
                 (handler-case (error 'inner "x") (inner (e) 1))
111
1
                 (t (e) 2))"#
112
        ),
113
1
        n(1),
114
    );
115
1
}
116

            
117
#[test]
118
1
fn nested_handler_case_outer_catches_uncaught_inner() {
119
    // The inner clause doesn't match 'boom, so it re-raises; the outer t
120
    // catches it.
121
1
    assert_eq!(
122
1
        eval_one(
123
1
            r#"(handler-case
124
1
                 (handler-case (error 'boom "x") (other (e) 1))
125
1
                 (t (e) 2))"#
126
        ),
127
1
        n(2),
128
    );
129
1
}
130

            
131
#[test]
132
1
fn handler_case_in_effect_position_still_catches() {
133
    // In effect position (non-tail of a BEGIN) the handler-case must still
134
    // emit its catch — it must not be elided as a const-folded value.
135
1
    assert_eq!(
136
1
        eval_one(r#"(begin (handler-case (error 'x "m") (x (e) 1)) 5)"#),
137
1
        n(5),
138
    );
139
1
}
140

            
141
#[test]
142
1
fn return_from_inside_matching_clause_exits_block() {
143
    // A `(return-from)` inside a matching clause body must resolve to the
144
    // enclosing BLOCK through the handler-case frames (not branch to the
145
    // wrong target). Returns 7, not the dead tail 99.
146
1
    assert_eq!(
147
1
        eval_one(r#"(block done (handler-case (error 'x "m") (x (e) (return-from done 7))) 99)"#),
148
1
        n(7),
149
    );
150
1
}
151

            
152
#[test]
153
1
fn return_from_inside_catch_all_clause_exits_block() {
154
1
    assert_eq!(
155
1
        eval_one(r#"(block done (handler-case (error 'z "m") (t (e) (return-from done 7))) 99)"#),
156
1
        n(7),
157
    );
158
1
}
159

            
160
#[test]
161
1
fn diverging_body_let_bound_takes_clause_type() {
162
    // The body always throws, so the form's runtime type comes from the
163
    // clause (StringRef), not the body's placeholder. A let binding sizes
164
    // its local from the eval-time type, so it must be the clause type or
165
    // the StringRef value mis-fits an i32 local (invalid module).
166
1
    assert_eq!(
167
1
        eval_one(r#"(let* ((x (handler-case (error 'boom "m") (boom (e) "ok")))) x)"#),
168
1
        s("ok"),
169
    );
170
1
}
171

            
172
#[test]
173
1
fn diverging_body_let_bound_ratio_clause() {
174
1
    assert_eq!(
175
1
        eval_one(r#"(let* ((x (handler-case (error 'b "m") (b (e) 11/10)))) x)"#),
176
1
        Value::Number(Fraction::new(11, 10)),
177
    );
178
1
}