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
use nms::interpreter::Interpreter;
6
use scripting::nomiscript::Value;
7

            
8
#[test]
9
1
fn test_eval_num_eq() {
10
1
    let mut interp = Interpreter::new(false).unwrap();
11
1
    assert_eq!(interp.eval("(= 1 1 1)").unwrap(), vec![Value::Bool(true)]);
12
1
    assert_eq!(interp.eval("(= 1 2)").unwrap(), vec![Value::Nil]);
13
1
}
14

            
15
#[test]
16
1
fn test_eval_num_eq_single() {
17
1
    let mut interp = Interpreter::new(false).unwrap();
18
1
    assert_eq!(interp.eval("(= 42)").unwrap(), vec![Value::Bool(true)]);
19
1
}
20

            
21
#[test]
22
1
fn test_eval_num_neq() {
23
1
    let mut interp = Interpreter::new(false).unwrap();
24
1
    assert_eq!(interp.eval("(/= 1 2 3)").unwrap(), vec![Value::Bool(true)]);
25
1
    assert_eq!(interp.eval("(/= 1 2 1)").unwrap(), vec![Value::Nil]);
26
1
}
27

            
28
#[test]
29
1
fn test_eval_num_lt() {
30
1
    let mut interp = Interpreter::new(false).unwrap();
31
1
    assert_eq!(interp.eval("(< 1 2 3)").unwrap(), vec![Value::Bool(true)]);
32
1
    assert_eq!(interp.eval("(< 1 2 2)").unwrap(), vec![Value::Nil]);
33
1
}
34

            
35
#[test]
36
1
fn test_eval_num_gt() {
37
1
    let mut interp = Interpreter::new(false).unwrap();
38
1
    assert_eq!(interp.eval("(> 3 2 1)").unwrap(), vec![Value::Bool(true)]);
39
1
    assert_eq!(interp.eval("(> 3 3 1)").unwrap(), vec![Value::Nil]);
40
1
}
41

            
42
#[test]
43
1
fn test_eval_num_le() {
44
1
    let mut interp = Interpreter::new(false).unwrap();
45
1
    assert_eq!(
46
1
        interp.eval("(<= 1 2 2 3)").unwrap(),
47
1
        vec![Value::Bool(true)]
48
    );
49
1
    assert_eq!(interp.eval("(<= 1 3 2)").unwrap(), vec![Value::Nil]);
50
1
}
51

            
52
#[test]
53
1
fn test_eval_num_ge() {
54
1
    let mut interp = Interpreter::new(false).unwrap();
55
1
    assert_eq!(
56
1
        interp.eval("(>= 3 2 2 1)").unwrap(),
57
1
        vec![Value::Bool(true)]
58
    );
59
1
    assert_eq!(interp.eval("(>= 1 2 3)").unwrap(), vec![Value::Nil]);
60
1
}
61

            
62
#[test]
63
1
fn test_eval_comparison_type_error() {
64
1
    let mut interp = Interpreter::new(false).unwrap();
65
1
    assert!(interp.eval("(= 1 \"one\")").is_err());
66
1
    assert!(interp.eval("(< \"a\" \"b\")").is_err());
67
1
}
68

            
69
#[test]
70
1
fn test_eval_eql_numbers() {
71
1
    let mut interp = Interpreter::new(false).unwrap();
72
1
    assert_eq!(interp.eval("(eql 1 1)").unwrap(), vec![Value::Bool(true)]);
73
1
    assert_eq!(interp.eval("(eql 1 2)").unwrap(), vec![Value::Nil]);
74
1
}
75

            
76
#[test]
77
1
fn test_eval_eql_strings() {
78
1
    let mut interp = Interpreter::new(false).unwrap();
79
1
    assert_eq!(
80
1
        interp.eval(r#"(eql "abc" "abc")"#).unwrap(),
81
1
        vec![Value::Bool(true)]
82
    );
83
1
    assert_eq!(
84
1
        interp.eval(r#"(eql "abc" "def")"#).unwrap(),
85
1
        vec![Value::Nil]
86
    );
87
1
}
88

            
89
#[test]
90
1
fn test_eval_eql_mixed_types() {
91
1
    let mut interp = Interpreter::new(false).unwrap();
92
1
    assert_eq!(interp.eval(r#"(eql 1 "1")"#).unwrap(), vec![Value::Nil]);
93
1
}
94

            
95
#[test]
96
1
fn test_eval_eql_nil() {
97
1
    let mut interp = Interpreter::new(false).unwrap();
98
1
    assert_eq!(
99
1
        interp.eval("(eql nil nil)").unwrap(),
100
1
        vec![Value::Bool(true)]
101
    );
102
1
}
103

            
104
#[test]
105
1
fn test_eval_eql_arity_error() {
106
1
    let mut interp = Interpreter::new(false).unwrap();
107
1
    assert!(interp.eval("(eql 1)").is_err());
108
1
    assert!(interp.eval("(eql 1 2 3)").is_err());
109
1
}
110

            
111
#[test]
112
1
fn test_eval_equal_strings() {
113
1
    let mut interp = Interpreter::new(false).unwrap();
114
1
    assert_eq!(
115
1
        interp.eval(r#"(equal "hello" "hello")"#).unwrap(),
116
1
        vec![Value::Bool(true)]
117
    );
118
1
    assert_eq!(
119
1
        interp.eval(r#"(equal "hello" "world")"#).unwrap(),
120
1
        vec![Value::Nil]
121
    );
122
1
}
123

            
124
#[test]
125
1
fn test_eval_equal_numbers() {
126
1
    let mut interp = Interpreter::new(false).unwrap();
127
1
    assert_eq!(
128
1
        interp.eval("(equal 0.5 0.5)").unwrap(),
129
1
        vec![Value::Bool(true)]
130
    );
131
1
}
132

            
133
#[test]
134
1
fn test_eval_equal_arity_error() {
135
1
    let mut interp = Interpreter::new(false).unwrap();
136
1
    assert!(interp.eval("(equal 1)").is_err());
137
1
}
138

            
139
// --- Runtime bool serialization (WasmType::Bool) ---
140
// `(transaction-tag-count 0)` is a RUNTIME i32 (0 on the minimal input), so
141
// these comparisons/predicates can't const-fold — they exercise the runtime
142
// producers. The result must serialize as Bool/Nil (the falsy/truthy pair),
143
// not Number, mirroring the const-fold path above.
144

            
145
#[test]
146
1
fn test_runtime_comparison_true_is_bool() {
147
1
    let mut interp = Interpreter::new(false).unwrap();
148
1
    assert_eq!(
149
1
        interp.eval("(= (transaction-tag-count 0) 0)").unwrap(),
150
1
        vec![Value::Bool(true)]
151
    );
152
1
}
153

            
154
#[test]
155
1
fn test_runtime_comparison_false_is_nil() {
156
1
    let mut interp = Interpreter::new(false).unwrap();
157
1
    assert_eq!(
158
1
        interp.eval("(< 0 (transaction-tag-count 0))").unwrap(),
159
1
        vec![Value::Nil]
160
    );
161
1
}
162

            
163
#[test]
164
1
fn test_runtime_not_is_bool() {
165
1
    let mut interp = Interpreter::new(false).unwrap();
166
    // (not <runtime-false>) → true; tag-count is 0 (falsy) so not → Bool(true).
167
1
    assert_eq!(
168
1
        interp
169
1
            .eval("(not (< 0 (transaction-tag-count 0)))")
170
1
            .unwrap(),
171
1
        vec![Value::Bool(true)]
172
    );
173
1
}
174

            
175
#[test]
176
1
fn test_runtime_and_or_are_bool() {
177
1
    let mut interp = Interpreter::new(false).unwrap();
178
    // and of two runtime comparisons: tag-count 0 == 0 (true) and 0 >= 0
179
    // (true) → Bool(true).
180
1
    assert_eq!(
181
1
        interp
182
1
            .eval("(and (= (transaction-tag-count 0) 0) (>= (transaction-tag-count 0) 0))")
183
1
            .unwrap(),
184
1
        vec![Value::Bool(true)]
185
    );
186
    // or short-circuits on the first truthy runtime comparison. This case
187
    // (true FIRST arg) is the regression: the old effect-only short-circuit
188
    // helper produced no output entity on the early-exit branch, so a
189
    // top-level `(or <true> …)` returned "no output entities". The
190
    // value-producing stack path yields a result on every branch.
191
1
    assert_eq!(
192
1
        interp
193
1
            .eval("(or (= (transaction-tag-count 0) 0) (< (transaction-tag-count 0) 0))")
194
1
            .unwrap(),
195
1
        vec![Value::Bool(true)]
196
    );
197
    // false first, true second — exercises the short-circuit fall-through too.
198
1
    assert_eq!(
199
1
        interp
200
1
            .eval("(or (< (transaction-tag-count 0) 0) (= (transaction-tag-count 0) 0))")
201
1
            .unwrap(),
202
1
        vec![Value::Bool(true)]
203
    );
204
1
}
205

            
206
#[test]
207
1
fn test_unary_runtime_and_or_truthify_count_to_bool() {
208
    // A unary `(and <count>)` / `(or <count>)` is a truth-value producer: it
209
    // answers "is the count truthy". The runtime i32 count (0 on minimal
210
    // input) must serialize as Nil (falsy), not Number(0) — the unary fast
211
    // path truthifies the i32 operand to Bool, matching the multi-arg path.
212
1
    let mut interp = Interpreter::new(false).unwrap();
213
1
    assert_eq!(
214
1
        interp.eval("(and (transaction-tag-count 0))").unwrap(),
215
1
        vec![Value::Nil]
216
    );
217
1
    assert_eq!(
218
1
        interp.eval("(or (transaction-tag-count 0))").unwrap(),
219
1
        vec![Value::Nil]
220
    );
221
1
}
222

            
223
#[test]
224
1
fn test_unary_and_or_over_bound_count_local_truthify_to_bool() {
225
    // The operand is a bound runtime LOCAL (`WasmLocal(_, I32)`), not a fresh
226
    // placeholder. The eval-time mirror must treat it like codegen's
227
    // `is_bool_runtime` (which matches both WasmRuntime and WasmLocal) and
228
    // truthify it to Bool — else `x` aliases the raw count local and
229
    // serializes as Number(0) instead of Nil.
230
1
    let mut interp = Interpreter::new(false).unwrap();
231
1
    assert_eq!(
232
1
        interp
233
1
            .eval("(let* ((n (transaction-tag-count 0)) (x (and n))) x)")
234
1
            .unwrap(),
235
1
        vec![Value::Nil]
236
    );
237
1
    assert_eq!(
238
1
        interp
239
1
            .eval("(let* ((n (transaction-tag-count 0)) (x (or n))) x)")
240
1
            .unwrap(),
241
1
        vec![Value::Nil]
242
    );
243
1
}
244

            
245
#[test]
246
1
fn test_effect_position_and_or_with_ref_runtime_operand_rejected() {
247
    // A ref-typed runtime operand in EFFECT position must be REJECTED with a
248
    // structured error (same contract as IF) — nomiscript does not do implicit
249
    // truthiness for runtime refs (a runtime ref may be null, so treating it as
250
    // always-truthy would silently mis-branch). NOT a malformed wasm module,
251
    // NOT a silent truthy. `(transaction-post-date 0)` is a runtime Ratio.
252
1
    let mut interp = Interpreter::new(false).unwrap();
253
1
    let err = interp
254
1
        .eval(r#"(and (transaction-post-date 0) 1) 7"#)
255
1
        .expect_err("ref-typed and operand in effect position must error");
256
1
    assert!(
257
1
        err.to_string().contains("truth value"),
258
        "expected a structured truth-value error, got: {err}"
259
    );
260
1
    let err = interp
261
1
        .eval(r#"(or (transaction-post-date 0) 1) 7"#)
262
1
        .expect_err("ref-typed or operand in effect position must error");
263
1
    assert!(
264
1
        err.to_string().contains("truth value"),
265
        "expected a structured truth-value error, got: {err}"
266
    );
267
1
}
268

            
269
#[test]
270
1
fn test_runtime_and_or_reject_ref_typed_operand() {
271
    // A runtime `and`/`or` lowers each operand into an `if (result i32)` short-
272
    // circuit block, so a ref-typed (StringRef/Ratio/…) operand can't ride it.
273
    // It must be a structured compile error, NOT a malformed wasm module.
274
1
    let mut interp = Interpreter::new(false).unwrap();
275
1
    let err = interp
276
1
        .eval(r#"(and (= (transaction-tag-count 0) 0) "x")"#)
277
1
        .expect_err("ref-typed and operand must error");
278
1
    assert!(
279
1
        err.to_string().to_lowercase().contains("truth value") || err.to_string().contains("AND"),
280
        "expected a structured truth-value error, got: {err}"
281
    );
282
1
}
283

            
284
#[test]
285
1
fn test_runtime_nullable_ref_in_and_or_rejected_use_null_predicate() {
286
    // #58: a let-bound runtime PairRef that is nil at runtime must NOT be
287
    // silently treated as truthy by `or`/`and` (the pre-fix bug returned wrong
288
    // values). It is rejected with the same structured error as IF; the script
289
    // must test nullability explicitly via `(null? …)`.
290
1
    let mut interp = Interpreter::new(false).unwrap();
291
1
    let nullable = "(let* ((x (cdr (cons (transaction-tag-count 0) nil)))) ";
292
1
    assert!(
293
1
        interp
294
1
            .eval(&format!("{nullable} (or x 7))"))
295
1
            .unwrap_err()
296
1
            .to_string()
297
1
            .contains("truth value")
298
    );
299
1
    assert!(
300
1
        interp
301
1
            .eval(&format!("{nullable} (and x 7))"))
302
1
            .unwrap_err()
303
1
            .to_string()
304
1
            .contains("truth value")
305
    );
306
    // The `(null? …)` escape hatch yields a Bool, so the form compiles and the
307
    // null ref reads as truthy-null: `(or (null? x) …)` → Bool(true).
308
1
    assert_eq!(
309
1
        interp
310
1
            .eval(&format!(
311
1
                "{nullable} (or (null? x) (= (transaction-tag-count 0) 5)))"
312
1
            ))
313
1
            .unwrap(),
314
1
        vec![Value::Bool(true)]
315
    );
316
1
}