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
//! Regression tests for two IF / COND miscompiles surfaced while wiring
6
//! handler-case, both exercised end-to-end through script-mode `process`
7
//! (which `Interpreter::eval` drives) so the *value* is checked, not just
8
//! module validity:
9
//!
10
//! 1. **Let-bound runtime boolean branched at runtime.** A test that
11
//!    resolves to `WasmLocal(_, I32)` (a let-bound comparison result) must
12
//!    take the runtime `if` path, not const-fold to the then-branch. The
13
//!    compile paths used to match only `WasmRuntime(I32)`, so the else
14
//!    branch was unreachable — a wrong-branch miscompile that still
15
//!    type-checked.
16
//!
17
//! 2. **Bare top-level runtime IF / COND serialized once.** The
18
//!    value-producing compile slot used to serialize *each branch*
19
//!    independently, double-advancing the compile-time output cursor; only
20
//!    one branch runs, so the decoder hit a garbage entity slot ("unknown
21
//!    value type"). The fix collapses both arms to one stack value and
22
//!    serializes a single result.
23

            
24
use nms::interpreter::Interpreter;
25
use scripting::nomiscript::{Fraction, Value};
26

            
27
22
fn eval_one(src: &str) -> Value {
28
22
    let mut interp = Interpreter::new(false).unwrap();
29
22
    interp
30
22
        .eval(src)
31
22
        .unwrap_or_else(|e| panic!("eval {src:?}: {e}"))
32
22
        .into_iter()
33
22
        .next_back()
34
22
        .unwrap_or_else(|| panic!("eval {src:?} produced no value"))
35
22
}
36

            
37
12
fn n(v: i64) -> Value {
38
12
    Value::Number(Fraction::from_integer(v))
39
12
}
40

            
41
#[test]
42
1
fn let_bound_runtime_boolean_takes_else_branch() {
43
    // `p` is a let-bound comparison → WasmLocal(_, I32). n=1 so (= n 99) is
44
    // false; the IF must branch to the else arm at runtime and yield "b".
45
    // The pre-fix const-fold path always returned the then-branch "a".
46
1
    assert_eq!(
47
1
        eval_one(r#"(let* ((n 0)) (setf n 1) (let* ((p (= n 99))) (if p "a" "b")))"#),
48
1
        Value::String("b".to_string())
49
    );
50
1
}
51

            
52
#[test]
53
1
fn let_bound_runtime_boolean_takes_then_branch() {
54
1
    assert_eq!(
55
1
        eval_one(r#"(let* ((n 0)) (setf n 1) (let* ((p (= n 1))) (if p "a" "b")))"#),
56
1
        Value::String("a".to_string())
57
    );
58
1
}
59

            
60
#[test]
61
1
fn let_bound_runtime_boolean_cond_takes_else_branch() {
62
1
    assert_eq!(
63
1
        eval_one(r#"(let* ((n 0)) (setf n 1) (let* ((p (= n 99))) (cond (p "a") (t "b"))))"#),
64
1
        Value::String("b".to_string())
65
    );
66
1
}
67

            
68
#[test]
69
1
fn bare_runtime_if_ratio_branches_serialize_once() {
70
    // `(transaction-tag-count 0)` is a runtime i32; the comparison drives a
71
    // bare top-level IF with ratio branches. Pre-fix this double-serialized
72
    // and the decoder reported "unknown value type". tag-count is 0 on the
73
    // minimal input, so the test is false → else arm → 6.
74
1
    assert_eq!(
75
1
        eval_one("(if (= (transaction-tag-count 0) 9999) 5 6)"),
76
1
        n(6)
77
    );
78
1
}
79

            
80
#[test]
81
1
fn bare_runtime_if_string_branches_serialize_once() {
82
1
    assert_eq!(
83
1
        eval_one(r#"(if (= (transaction-tag-count 0) 9999) "a" "b")"#),
84
1
        Value::String("b".to_string())
85
    );
86
1
}
87

            
88
#[test]
89
1
fn bare_runtime_if_bool_branches_select_false_arm() {
90
    // The condition is false, so the `#f` arm is selected. The branches are
91
    // booleans, so the result carries `WasmType::Bool` and the runtime
92
    // serializer surfaces `#f` as `Nil` (matching the const-fold path), not
93
    // `Number(0)`. This locks in the WasmType::Bool fix for the i32/bool
94
    // serialization conflation.
95
1
    assert_eq!(
96
1
        eval_one("(if (= (transaction-tag-count 0) 9999) #t #f)"),
97
        Value::Nil
98
    );
99
1
}
100

            
101
#[test]
102
1
fn bare_runtime_if_bool_branches_select_true_arm() {
103
    // The condition is true (tag-count 0 == 0), so the `#t` arm is selected
104
    // and serializes as `Bool(true)`, not `Number(1)`.
105
1
    assert_eq!(
106
1
        eval_one("(if (= (transaction-tag-count 0) 0) #t #f)"),
107
        Value::Bool(true)
108
    );
109
1
}
110

            
111
#[test]
112
1
fn bare_runtime_comparison_serializes_as_bool() {
113
    // A bare runtime comparison is a `WasmType::Bool`: a false result is `Nil`,
114
    // not `Number(0)`.
115
1
    assert_eq!(eval_one("(= (transaction-tag-count 0) 9999)"), Value::Nil);
116
1
    assert_eq!(
117
1
        eval_one("(= (transaction-tag-count 0) 0)"),
118
        Value::Bool(true)
119
    );
120
1
}
121

            
122
#[test]
123
1
fn let_bound_if_with_bool_branches_via_runtime_prefix_block() {
124
    // The eval-time type mirrors (let-body tail inference, block/handler peeks)
125
    // must classify bool/nil literals as `Bool`, matching `compile_for_stack`.
126
    // Here the inner `let` body has a runtime prefix (the comparison) before a
127
    // `#t` tail, so the body's type is inferred via `infer_runtime_type`; if it
128
    // returned I32 while codegen pushes Bool, the IF branches unify to AnyRef
129
    // and the outer binding local mis-sizes. The condition is true, so `x` is
130
    // `#t` → Bool(true).
131
1
    assert_eq!(
132
1
        eval_one(
133
1
            "(let* ((x (if (= (transaction-tag-count 0) 0) \
134
1
                           (let* ((dummy (transaction-tag-count 0))) #t) \
135
1
                           #f))) \
136
1
               x)"
137
        ),
138
        Value::Bool(true)
139
    );
140
1
}
141

            
142
#[test]
143
1
fn runtime_if_with_empty_begin_branch_serializes_nil() {
144
    // An empty `(begin)` ≡ nil. Its stack producer must report `WasmType::Bool`
145
    // so it serializes as Nil (not Number(0)) and unifies homogeneously with
146
    // the other Bool-typed `#t` branch of a runtime IF. tag-count is 0 → test
147
    // true → the empty-begin arm → Nil.
148
1
    assert_eq!(
149
1
        eval_one("(if (= (transaction-tag-count 0) 0) (begin) #t)"),
150
        Value::Nil
151
    );
152
1
}
153

            
154
#[test]
155
1
fn no_else_runtime_if_with_count_then_keeps_number() {
156
    // A no-else runtime IF whose then-branch is a raw i32 COUNT must keep
157
    // Number fidelity. The missing else ≡ nil is typed `I32` (mirroring the
158
    // count), so `unify_if_type` stays homogeneous (I32) instead of widening
159
    // to AnyRef — which would box the count behind a non-null marker and lose
160
    // its value. tag-count is 0 here, so the test is true → the count `0`.
161
1
    assert_eq!(
162
1
        eval_one("(if (= (transaction-tag-count 0) 0) (transaction-tag-count 0))"),
163
1
        n(0)
164
    );
165
    // Test false → missing else fires → nil. An I32-typed nil serializes as
166
    // the count's falsy form (Number(0)), not Bool.
167
1
    assert_eq!(
168
1
        eval_one("(if (= (transaction-tag-count 0) 9999) (transaction-tag-count 0))"),
169
1
        n(0)
170
    );
171
1
}
172

            
173
#[test]
174
1
fn no_else_runtime_if_with_bool_then_keeps_nil() {
175
    // A no-else runtime IF whose then-branch is a Bool keeps Nil/Bool fidelity:
176
    // the missing else ≡ nil is typed `Bool`. Test false → nil.
177
1
    assert_eq!(
178
1
        eval_one("(if (= (transaction-tag-count 0) 9999) (= (transaction-tag-count 0) 0))"),
179
        Value::Nil
180
    );
181
1
}
182

            
183
#[test]
184
1
fn bare_runtime_cond_serialize_once() {
185
1
    assert_eq!(
186
1
        eval_one("(cond ((= (transaction-tag-count 0) 9999) 5) (t 6))"),
187
1
        n(6)
188
    );
189
1
}
190

            
191
#[test]
192
1
fn tagbody_promoted_local_runtime_if_serializes_once() {
193
    // The earlier "unknown value type" reproducer: a go-loop tagbody
194
    // promotes `n` to a runtime local, then a value-position IF reads it.
195
1
    assert_eq!(
196
1
        eval_one(
197
1
            "(let* ((n 0)) (tagbody loop (setf n (+ n 1)) (when (< n 1) (go loop))) \
198
1
             (if (= n 99) 5 6))"
199
        ),
200
1
        n(6)
201
    );
202
1
}
203

            
204
#[test]
205
1
fn diverging_if_test_fires_instead_of_const_folding() {
206
    // A `(return-from)` in IF test position transfers control before any
207
    // condition is produced — the IF must NOT const-fold to a branch. The
208
    // block yields the return-from value, never the then/else arms.
209
1
    assert_eq!(eval_one("(block out (if (return-from out 7) 1 2))"), n(7));
210
1
}
211

            
212
#[test]
213
1
fn diverging_if_test_with_dead_tail_after() {
214
1
    assert_eq!(
215
1
        eval_one("(block out (if (return-from out 7) 1 2) 99)"),
216
1
        n(7)
217
    );
218
1
}
219

            
220
#[test]
221
1
fn diverging_cond_test_fires_instead_of_const_folding() {
222
1
    assert_eq!(
223
1
        eval_one("(block out (cond ((return-from out 7) 1)) 9)"),
224
1
        n(7)
225
    );
226
1
}
227

            
228
#[test]
229
1
fn diverging_cond_test_with_catch_all_clause() {
230
1
    assert_eq!(
231
1
        eval_one("(block out (cond ((return-from out 7) 1) (t 2)) 9)"),
232
1
        n(7)
233
    );
234
1
}
235

            
236
#[test]
237
1
fn cond_runtime_test_side_effect_runs_exactly_once() {
238
    // The first clause test increments `n` and compares; its compile-time
239
    // side effect must reach the symbol table exactly once (the suffix
240
    // IF-chain is eval'd a single time). n ends at 1, so the catch-all
241
    // returns 1 — not 2 (double-applied) and not an error.
242
1
    assert_eq!(
243
1
        eval_one("(let* ((n 0)) (cond ((begin (setf n (+ n 1)) (= n 99)) 1) (t n)))"),
244
1
        n(1)
245
    );
246
1
}
247

            
248
#[test]
249
1
fn if_runtime_test_side_effect_runs_exactly_once() {
250
    // setf in a runtime-test IF must apply once, not twice (classify on a
251
    // clone, emit once). The runtime test compares against a host value.
252
1
    assert_eq!(
253
1
        eval_one(
254
1
            "(let* ((n 0)) \
255
1
             (if (begin (setf n (+ n 1)) (= (transaction-tag-count 0) n)) 0 0) n)"
256
        ),
257
1
        n(1)
258
    );
259
1
}
260

            
261
#[test]
262
1
fn cond_const_false_clause_between_runtime_clauses_in_effect_position() {
263
    // Effect-position COND with a const-false clause (nil test) sitting
264
    // between two runtime clauses. The open-guard-block count must track the
265
    // runtime clauses actually emitted, not the raw clause index — else the
266
    // closing `block_end`s over-count and the module fails to validate. The
267
    // cond is a prefix (effect) form; the program returns the trailing X.
268
1
    let mut interp = Interpreter::new(false).unwrap();
269
1
    let wasm = interp
270
1
        .compile_to_wasm(
271
1
            "(cond ((= (transaction-tag-count 0) 1) (debug \"a\")) \
272
1
                   (nil (debug \"never\")) \
273
1
                   ((= (transaction-tag-count 0) 2) (debug \"b\"))) \
274
1
             7",
275
        )
276
1
        .unwrap_or_else(|e| panic!("compile: {e}"));
277
1
    assert_eq!(interp.run_wasm(&wasm).unwrap(), n(7));
278
1
}