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::T]);
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::T]);
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::T]);
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::T]);
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::T]);
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!(interp.eval("(<= 1 2 2 3)").unwrap(), vec![Value::T]);
46
1
    assert_eq!(interp.eval("(<= 1 3 2)").unwrap(), vec![Value::Nil]);
47
1
}
48

            
49
#[test]
50
1
fn test_eval_num_ge() {
51
1
    let mut interp = Interpreter::new(false).unwrap();
52
1
    assert_eq!(interp.eval("(>= 3 2 2 1)").unwrap(), vec![Value::T]);
53
1
    assert_eq!(interp.eval("(>= 1 2 3)").unwrap(), vec![Value::Nil]);
54
1
}
55

            
56
#[test]
57
1
fn test_eval_comparison_type_error() {
58
1
    let mut interp = Interpreter::new(false).unwrap();
59
1
    assert!(interp.eval("(= 1 \"one\")").is_err());
60
1
    assert!(interp.eval("(< \"a\" \"b\")").is_err());
61
1
}
62

            
63
#[test]
64
1
fn test_eval_eql_numbers() {
65
1
    let mut interp = Interpreter::new(false).unwrap();
66
1
    assert_eq!(interp.eval("(eql 1 1)").unwrap(), vec![Value::T]);
67
1
    assert_eq!(interp.eval("(eql 1 2)").unwrap(), vec![Value::Nil]);
68
1
}
69

            
70
#[test]
71
1
fn test_eval_eql_strings() {
72
1
    let mut interp = Interpreter::new(false).unwrap();
73
1
    assert_eq!(interp.eval(r#"(eql "abc" "abc")"#).unwrap(), vec![Value::T]);
74
1
    assert_eq!(
75
1
        interp.eval(r#"(eql "abc" "def")"#).unwrap(),
76
1
        vec![Value::Nil]
77
    );
78
1
}
79

            
80
#[test]
81
1
fn test_eval_eql_mixed_types() {
82
1
    let mut interp = Interpreter::new(false).unwrap();
83
1
    assert_eq!(interp.eval(r#"(eql 1 "1")"#).unwrap(), vec![Value::Nil]);
84
1
}
85

            
86
#[test]
87
1
fn test_eval_eql_nil() {
88
1
    let mut interp = Interpreter::new(false).unwrap();
89
1
    assert_eq!(interp.eval("(eql nil nil)").unwrap(), vec![Value::T]);
90
1
}
91

            
92
#[test]
93
1
fn test_eval_eql_arity_error() {
94
1
    let mut interp = Interpreter::new(false).unwrap();
95
1
    assert!(interp.eval("(eql 1)").is_err());
96
1
    assert!(interp.eval("(eql 1 2 3)").is_err());
97
1
}
98

            
99
#[test]
100
1
fn test_eval_equal_strings() {
101
1
    let mut interp = Interpreter::new(false).unwrap();
102
1
    assert_eq!(
103
1
        interp.eval(r#"(equal "hello" "hello")"#).unwrap(),
104
1
        vec![Value::T]
105
    );
106
1
    assert_eq!(
107
1
        interp.eval(r#"(equal "hello" "world")"#).unwrap(),
108
1
        vec![Value::Nil]
109
    );
110
1
}
111

            
112
#[test]
113
1
fn test_eval_equal_numbers() {
114
1
    let mut interp = Interpreter::new(false).unwrap();
115
1
    assert_eq!(interp.eval("(equal 0.5 0.5)").unwrap(), vec![Value::T]);
116
1
}
117

            
118
#[test]
119
1
fn test_eval_equal_arity_error() {
120
1
    let mut interp = Interpreter::new(false).unwrap();
121
1
    assert!(interp.eval("(equal 1)").is_err());
122
1
}
123

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

            
130
#[test]
131
1
fn test_runtime_comparison_true_is_bool() {
132
1
    let mut interp = Interpreter::new(false).unwrap();
133
1
    assert_eq!(
134
1
        interp.eval("(= (transaction-tag-count 0) 0)").unwrap(),
135
1
        vec![Value::T]
136
    );
137
1
}
138

            
139
#[test]
140
1
fn test_runtime_comparison_false_is_nil() {
141
1
    let mut interp = Interpreter::new(false).unwrap();
142
1
    assert_eq!(
143
1
        interp.eval("(< 0 (transaction-tag-count 0))").unwrap(),
144
1
        vec![Value::Nil]
145
    );
146
1
}
147

            
148
#[test]
149
1
fn test_runtime_not_is_bool() {
150
1
    let mut interp = Interpreter::new(false).unwrap();
151
    // (not <runtime-false>) → true; tag-count is 0 (falsy) so not → Bool(true).
152
1
    assert_eq!(
153
1
        interp
154
1
            .eval("(not (< 0 (transaction-tag-count 0)))")
155
1
            .unwrap(),
156
1
        vec![Value::T]
157
    );
158
1
}
159

            
160
#[test]
161
1
fn test_runtime_and_or_are_bool() {
162
1
    let mut interp = Interpreter::new(false).unwrap();
163
    // and of two runtime comparisons: tag-count 0 == 0 (true) and 0 >= 0
164
    // (true) → Bool(true).
165
1
    assert_eq!(
166
1
        interp
167
1
            .eval("(and (= (transaction-tag-count 0) 0) (>= (transaction-tag-count 0) 0))")
168
1
            .unwrap(),
169
1
        vec![Value::T]
170
    );
171
    // or short-circuits on the first truthy runtime comparison. This case
172
    // (true FIRST arg) is the regression: the old effect-only short-circuit
173
    // helper produced no output entity on the early-exit branch, so a
174
    // top-level `(or <true> …)` returned "no output entities". The
175
    // value-producing stack path yields a result on every branch.
176
1
    assert_eq!(
177
1
        interp
178
1
            .eval("(or (= (transaction-tag-count 0) 0) (< (transaction-tag-count 0) 0))")
179
1
            .unwrap(),
180
1
        vec![Value::T]
181
    );
182
    // false first, true second — exercises the short-circuit fall-through too.
183
1
    assert_eq!(
184
1
        interp
185
1
            .eval("(or (< (transaction-tag-count 0) 0) (= (transaction-tag-count 0) 0))")
186
1
            .unwrap(),
187
1
        vec![Value::T]
188
    );
189
1
}
190

            
191
#[test]
192
1
fn test_unary_runtime_and_or_over_a_count_is_true() {
193
    // A count is never nil, so even a runtime count of 0 is true
194
    // (doc/ops/0010). Phase 4 makes `and`/`or` return the count itself.
195
1
    let mut interp = Interpreter::new(false).unwrap();
196
1
    assert_eq!(
197
1
        interp.eval("(and (transaction-tag-count 0))").unwrap(),
198
1
        vec![Value::T]
199
    );
200
1
    assert_eq!(
201
1
        interp.eval("(or (transaction-tag-count 0))").unwrap(),
202
1
        vec![Value::T]
203
    );
204
1
}
205

            
206
#[test]
207
1
fn test_unary_and_or_over_a_bound_count_local_is_true() {
208
    // A bound runtime local (`WasmLocal(_, I32)`) must get the same result as a
209
    // fresh placeholder; otherwise `x` would alias the raw count local and
210
    // serialize as Number(0).
211
1
    let mut interp = Interpreter::new(false).unwrap();
212
1
    assert_eq!(
213
1
        interp
214
1
            .eval("(let* ((n (transaction-tag-count 0)) (x (and n))) x)")
215
1
            .unwrap(),
216
1
        vec![Value::T]
217
    );
218
1
    assert_eq!(
219
1
        interp
220
1
            .eval("(let* ((n (transaction-tag-count 0)) (x (or n))) x)")
221
1
            .unwrap(),
222
1
        vec![Value::T]
223
    );
224
1
}
225

            
226
#[test]
227
1
fn test_effect_position_and_or_with_ref_runtime_operand_rejected() {
228
    // A ref-typed runtime operand in EFFECT position must be REJECTED with a
229
    // structured error (same contract as IF) — nomiscript does not do implicit
230
    // truthiness for runtime refs (a runtime ref may be null, so treating it as
231
    // always-truthy would silently mis-branch). NOT a malformed wasm module,
232
    // NOT a silent truthy. `(transaction-post-date 0)` is a runtime Ratio.
233
1
    let mut interp = Interpreter::new(false).unwrap();
234
1
    let err = interp
235
1
        .eval(r#"(and (transaction-post-date 0) 1) 7"#)
236
1
        .expect_err("ref-typed and operand in effect position must error");
237
1
    assert!(
238
1
        err.to_string().contains("truth value"),
239
        "expected a structured truth-value error, got: {err}"
240
    );
241
1
    let err = interp
242
1
        .eval(r#"(or (transaction-post-date 0) 1) 7"#)
243
1
        .expect_err("ref-typed or operand in effect position must error");
244
1
    assert!(
245
1
        err.to_string().contains("truth value"),
246
        "expected a structured truth-value error, got: {err}"
247
    );
248
1
}
249

            
250
#[test]
251
1
fn test_runtime_and_or_reject_ref_typed_operand() {
252
    // A runtime `and`/`or` lowers each operand into an `if (result i32)` short-
253
    // circuit block, so a ref-typed (StringRef/Ratio/…) operand can't ride it.
254
    // It must be a structured compile error, NOT a malformed wasm module.
255
1
    let mut interp = Interpreter::new(false).unwrap();
256
1
    let err = interp
257
1
        .eval(r#"(and (= (transaction-tag-count 0) 0) "x")"#)
258
1
        .expect_err("ref-typed and operand must error");
259
1
    assert!(
260
1
        err.to_string().to_lowercase().contains("truth value") || err.to_string().contains("AND"),
261
        "expected a structured truth-value error, got: {err}"
262
    );
263
1
}
264

            
265
#[test]
266
1
fn test_runtime_nullable_ref_in_and_or_rejected_use_null_predicate() {
267
    // #58: a let-bound runtime PairRef that is nil at runtime must NOT be
268
    // silently treated as truthy by `or`/`and` (the pre-fix bug returned wrong
269
    // values). It is rejected with the same structured error as IF; the script
270
    // must test nullability explicitly via `(null? …)`.
271
1
    let mut interp = Interpreter::new(false).unwrap();
272
1
    let nullable = "(let* ((x (cdr (cons (transaction-tag-count 0) nil)))) ";
273
1
    assert!(
274
1
        interp
275
1
            .eval(&format!("{nullable} (or x 7))"))
276
1
            .unwrap_err()
277
1
            .to_string()
278
1
            .contains("truth value")
279
    );
280
1
    assert!(
281
1
        interp
282
1
            .eval(&format!("{nullable} (and x 7))"))
283
1
            .unwrap_err()
284
1
            .to_string()
285
1
            .contains("truth value")
286
    );
287
    // The `(null? …)` escape hatch yields a Bool, so the form compiles and the
288
    // null ref reads as truthy-null: `(or (null? x) …)` → Bool(true).
289
1
    assert_eq!(
290
1
        interp
291
1
            .eval(&format!(
292
1
                "{nullable} (or (null? x) (= (transaction-tag-count 0) 5)))"
293
1
            ))
294
1
            .unwrap(),
295
1
        vec![Value::T]
296
    );
297
1
}