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

            
37
10
fn n(v: i64) -> Value {
38
10
    Value::Number(Fraction::from_integer(v))
39
10
}
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 `nil` arm is selected. The branches are
91
    // booleans, so the result carries `WasmType::Bool` and the runtime
92
    // serializer surfaces `nil` 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 nil)"),
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 nil)"),
107
        Value::T
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!(eval_one("(= (transaction-tag-count 0) 0)"), Value::T);
117
1
}
118

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

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

            
151
#[test]
152
1
fn no_else_runtime_if_with_count_then_is_nil_when_false() {
153
    // The true case's widened count is only visible from phase 3 (doc/ops/0010).
154
1
    assert_eq!(
155
1
        eval_one("(if (= (transaction-tag-count 0) 9999) (transaction-tag-count 0))"),
156
        Value::Nil
157
    );
158
1
}
159

            
160
#[test]
161
1
fn no_else_runtime_if_with_bool_then_keeps_nil() {
162
    // A no-else runtime IF whose then-branch is a Bool keeps Nil/Bool fidelity:
163
    // the missing else ≡ nil is typed `Bool`. Test false → nil.
164
1
    assert_eq!(
165
1
        eval_one("(if (= (transaction-tag-count 0) 9999) (= (transaction-tag-count 0) 0))"),
166
        Value::Nil
167
    );
168
1
}
169

            
170
#[test]
171
1
fn bare_runtime_cond_serialize_once() {
172
1
    assert_eq!(
173
1
        eval_one("(cond ((= (transaction-tag-count 0) 9999) 5) (t 6))"),
174
1
        n(6)
175
    );
176
1
}
177

            
178
#[test]
179
1
fn tagbody_promoted_local_runtime_if_serializes_once() {
180
    // The earlier "unknown value type" reproducer: a go-loop tagbody
181
    // promotes `n` to a runtime local, then a value-position IF reads it.
182
1
    assert_eq!(
183
1
        eval_one(
184
1
            "(let* ((n 0)) (tagbody loop (setf n (+ n 1)) (when (< n 1) (go loop))) \
185
1
             (if (= n 99) 5 6))"
186
        ),
187
1
        n(6)
188
    );
189
1
}
190

            
191
#[test]
192
1
fn diverging_if_test_fires_instead_of_const_folding() {
193
    // A `(return-from)` in IF test position transfers control before any
194
    // condition is produced — the IF must NOT const-fold to a branch. The
195
    // block yields the return-from value, never the then/else arms.
196
1
    assert_eq!(eval_one("(block out (if (return-from out 7) 1 2))"), n(7));
197
1
}
198

            
199
#[test]
200
1
fn diverging_if_test_with_dead_tail_after() {
201
1
    assert_eq!(
202
1
        eval_one("(block out (if (return-from out 7) 1 2) 99)"),
203
1
        n(7)
204
    );
205
1
}
206

            
207
#[test]
208
1
fn diverging_cond_test_fires_instead_of_const_folding() {
209
1
    assert_eq!(
210
1
        eval_one("(block out (cond ((return-from out 7) 1)) 9)"),
211
1
        n(7)
212
    );
213
1
}
214

            
215
#[test]
216
1
fn diverging_cond_test_with_catch_all_clause() {
217
1
    assert_eq!(
218
1
        eval_one("(block out (cond ((return-from out 7) 1) (t 2)) 9)"),
219
1
        n(7)
220
    );
221
1
}
222

            
223
#[test]
224
1
fn cond_runtime_test_side_effect_runs_exactly_once() {
225
    // The first clause test increments `n` and compares; its compile-time
226
    // side effect must reach the symbol table exactly once (the suffix
227
    // IF-chain is eval'd a single time). n ends at 1, so the catch-all
228
    // returns 1 — not 2 (double-applied) and not an error.
229
1
    assert_eq!(
230
1
        eval_one("(let* ((n 0)) (cond ((begin (setf n (+ n 1)) (= n 99)) 1) (t n)))"),
231
1
        n(1)
232
    );
233
1
}
234

            
235
#[test]
236
1
fn if_runtime_test_side_effect_runs_exactly_once() {
237
    // setf in a runtime-test IF must apply once, not twice (classify on a
238
    // clone, emit once). The runtime test compares against a host value.
239
1
    assert_eq!(
240
1
        eval_one(
241
1
            "(let* ((n 0)) \
242
1
             (if (begin (setf n (+ n 1)) (= (transaction-tag-count 0) n)) 0 0) n)"
243
        ),
244
1
        n(1)
245
    );
246
1
}
247

            
248
#[test]
249
1
fn cond_const_false_clause_between_runtime_clauses_in_effect_position() {
250
    // Effect-position COND with a const-false clause (nil test) sitting
251
    // between two runtime clauses. The open-guard-block count must track the
252
    // runtime clauses actually emitted, not the raw clause index — else the
253
    // closing `block_end`s over-count and the module fails to validate. The
254
    // cond is a prefix (effect) form; the program returns the trailing X.
255
1
    let mut interp = Interpreter::new(false).unwrap();
256
1
    let wasm = interp
257
1
        .compile_to_wasm(
258
1
            "(cond ((= (transaction-tag-count 0) 1) (debug \"a\")) \
259
1
                   (nil (debug \"never\")) \
260
1
                   ((= (transaction-tag-count 0) 2) (debug \"b\"))) \
261
1
             7",
262
        )
263
1
        .unwrap_or_else(|e| panic!("compile: {e}"));
264
1
    assert_eq!(interp.run_wasm(&wasm).unwrap(), n(7));
265
1
}