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::{Fraction, Value};
7

            
8
// LIST
9

            
10
#[test]
11
1
fn test_eval_list_empty() {
12
1
    let mut interp = Interpreter::new(false).unwrap();
13
1
    let results = interp.eval("(list)").unwrap();
14
1
    assert_eq!(results, vec![Value::String("()".to_string())]);
15
1
}
16

            
17
#[test]
18
1
fn test_eval_list_numbers() {
19
1
    let mut interp = Interpreter::new(false).unwrap();
20
1
    let results = interp.eval("(list 1 2 3)").unwrap();
21
1
    assert_eq!(results, vec![Value::String("(1 2 3)".to_string())]);
22
1
}
23

            
24
#[test]
25
1
fn test_eval_list_mixed() {
26
1
    let mut interp = Interpreter::new(false).unwrap();
27
1
    let results = interp.eval(r#"(list 1 "hello" #t)"#).unwrap();
28
1
    assert_eq!(results, vec![Value::String("(1 hello #T)".to_string())]);
29
1
}
30

            
31
// CONS
32

            
33
#[test]
34
1
fn test_eval_cons_onto_list() {
35
1
    let mut interp = Interpreter::new(false).unwrap();
36
1
    let results = interp.eval("(cons 1 (list 2 3))").unwrap();
37
1
    assert_eq!(results, vec![Value::String("(1 2 3)".to_string())]);
38
1
}
39

            
40
#[test]
41
1
fn test_eval_cons_onto_nil() {
42
1
    let mut interp = Interpreter::new(false).unwrap();
43
1
    let results = interp.eval("(cons 1 nil)").unwrap();
44
1
    assert_eq!(results, vec![Value::String("(1)".to_string())]);
45
1
}
46

            
47
#[test]
48
1
fn test_eval_cons_arity_error() {
49
1
    let mut interp = Interpreter::new(false).unwrap();
50
1
    assert!(interp.eval("(cons 1)").is_err());
51
1
    assert!(interp.eval("(cons 1 2 3)").is_err());
52
1
}
53

            
54
#[test]
55
1
fn test_eval_numeric_literal_in_list_stays_ratio() {
56
    // A numeric literal is a dimensionless Ratio in EVERY context — including a
57
    // pair cell — never an i32 count. Reclassifying an integer literal to I32
58
    // when it rides a list cell was an illegal implicit Ratio→count conversion:
59
    // the `5` came back from CAR as a count and arithmetic refused it. These
60
    // exercise the RUNTIME pair path (a runtime element forces the chain
61
    // runtime), so the literal `5` must survive as a usable Ratio.
62
1
    let mut interp = Interpreter::new(false).unwrap();
63
    // CAR of a runtime list holding `5` then arithmetic on it.
64
1
    assert_eq!(
65
1
        interp
66
1
            .eval("(let* ((p (cons 5 (cons (transaction-post-date 0) nil)))) (+ (car p) 1))")
67
1
            .unwrap(),
68
1
        vec![Value::Number(Fraction::from_integer(6))]
69
    );
70
    // a mixed literal+runtime list no longer miscompiles to Bool; CAR is 5.
71
1
    assert_eq!(
72
1
        interp
73
1
            .eval("(car (list 5 (transaction-post-date 0)))")
74
1
            .unwrap(),
75
1
        vec![Value::Number(Fraction::from_integer(5))]
76
    );
77
1
}
78

            
79
// CAR over a list whose element is a RUNTIME value (#57). `(list <runtime>)`
80
// used to embed the runtime placeholder inside a quoted compile-time list, so
81
// CAR extracted a bare placeholder with no stack value -> invalid wasm. Now
82
// `list` desugars a runtime element to the CONS chain, so every element type
83
// round-trips through the $pair stack path.
84

            
85
#[test]
86
1
fn test_eval_car_of_list_with_runtime_i32() {
87
    // tag-count is 0 on the minimal input; the i32 COUNT element keeps Number.
88
1
    let mut interp = Interpreter::new(false).unwrap();
89
1
    assert_eq!(
90
1
        interp
91
1
            .eval("(car (list (transaction-tag-count 0)))")
92
1
            .unwrap(),
93
1
        vec![Value::Number(Fraction::from_integer(0))]
94
    );
95
1
}
96

            
97
#[test]
98
1
fn test_eval_car_of_list_with_runtime_ratio() {
99
1
    let mut interp = Interpreter::new(false).unwrap();
100
1
    assert_eq!(
101
1
        interp
102
1
            .eval("(car (list (transaction-post-date 0)))")
103
1
            .unwrap(),
104
1
        vec![Value::Number(Fraction::from_integer(0))]
105
    );
106
1
}
107

            
108
#[test]
109
1
fn test_eval_car_of_list_with_runtime_bool() {
110
    // A bool element rides its own PairElement::Bool slot, so CAR recovers a
111
    // truth value: (= 0 0) -> Bool(true), (< 0 0) -> Nil — NOT Number(1)/(0).
112
1
    let mut interp = Interpreter::new(false).unwrap();
113
1
    assert_eq!(
114
1
        interp
115
1
            .eval("(car (list (= (transaction-tag-count 0) 0)))")
116
1
            .unwrap(),
117
1
        vec![Value::Bool(true)]
118
    );
119
1
    assert_eq!(
120
1
        interp
121
1
            .eval("(car (list (< 0 (transaction-tag-count 0))))")
122
1
            .unwrap(),
123
1
        vec![Value::Nil]
124
    );
125
1
}
126

            
127
#[test]
128
1
fn test_eval_cdr_then_car_of_multi_runtime_list() {
129
    // Two runtime bool elements; CDR then CAR reaches the second cell.
130
1
    let mut interp = Interpreter::new(false).unwrap();
131
1
    assert_eq!(
132
1
        interp
133
1
            .eval(
134
1
                "(car (cdr (list (= (transaction-tag-count 0) 0) \
135
1
                 (< 0 (transaction-tag-count 0)))))"
136
            )
137
1
            .unwrap(),
138
1
        vec![Value::Nil]
139
    );
140
1
}
141

            
142
#[test]
143
1
fn test_eval_do_accumulator_of_runtime_cons_car() {
144
    // A `do` whose accumulator step is `(cons <host-fn-call> acc)` must size the
145
    // accumulator local from the (eval-resolved) cons car's element type. The
146
    // car is a not-yet-reduced `(transaction-… i)` List form; the pair-element
147
    // walker now evals it on a clone instead of falling to the scalar default
148
    // (which mis-sized `acc` and failed CONS cdr typing). Pre-existing bug
149
    // surfaced while completing the PairElement::Bool work.
150
1
    let mut interp = Interpreter::new(false).unwrap();
151
    // i32 car: CAR of the accumulated list keeps Number.
152
1
    assert_eq!(
153
1
        interp
154
1
            .eval(
155
1
                "(car (do ((i 0 (+ i 1)) \
156
1
                 (acc nil (cons (transaction-tag-count 0) acc))) \
157
1
                 ((= i 3) acc)))"
158
            )
159
1
            .unwrap(),
160
1
        vec![Value::Number(Fraction::from_integer(0))]
161
    );
162
    // bool car: CAR recovers a truth value (Bool), not Number.
163
1
    assert_eq!(
164
1
        interp
165
1
            .eval(
166
1
                "(car (do ((i 0 (+ i 1)) \
167
1
                 (acc nil (cons (= (transaction-tag-count 0) 0) acc))) \
168
1
                 ((= i 3) acc)))"
169
            )
170
1
            .unwrap(),
171
1
        vec![Value::Bool(true)]
172
    );
173
1
}
174

            
175
#[test]
176
1
fn test_eval_runtime_list_applies_element_side_effects_once() {
177
    // A runtime `(list …)` desugars to a CONS chain built from the
178
    // already-resolved elements, NOT a re-eval of the source args — so a SETF
179
    // side effect in an element runs exactly once. `n` ends at 1, not 2.
180
1
    let mut interp = Interpreter::new(false).unwrap();
181
1
    assert_eq!(
182
1
        interp
183
1
            .eval("(let* ((n 0)) (list (transaction-tag-count 0) (setf n (+ n 1))) n)")
184
1
            .unwrap(),
185
1
        vec![Value::Number(Fraction::from_integer(1))]
186
    );
187
1
}
188

            
189
#[test]
190
1
fn test_eval_runtime_list_honors_shadowed_cons() {
191
    // The runtime `(list …)` lowering builds its `$pair` chain by calling the
192
    // CONS builder DIRECTLY, not by synthesizing a `(CONS …)` form — so a user
193
    // `(defun cons …)` can't hijack list construction. With `cons` shadowed,
194
    // `(list <runtime>)` still builds a real pair whose CAR is the element.
195
1
    let mut interp = Interpreter::new(false).unwrap();
196
1
    assert_eq!(
197
1
        interp
198
1
            .eval("(defun cons (a b) 99) (car (list (transaction-tag-count 0)))")
199
1
            .unwrap(),
200
1
        vec![Value::Number(Fraction::from_integer(0))]
201
    );
202
1
}
203

            
204
#[test]
205
1
fn test_eval_apply_honors_shadowed_list() {
206
    // APPLY's `(list …)` spread fast-path must only fire for the BUILTIN list.
207
    // A user `(defun list …)` shadows it (the call dispatcher gives a user
208
    // function priority), so `(apply f (list 5))` must call the user `list`
209
    // (→ 10), not spread `5`. 10 isn't a valid arg-list → structured error.
210
1
    let mut shadowed = Interpreter::new(false).unwrap();
211
1
    assert!(
212
1
        shadowed
213
1
            .eval(
214
1
                "(defun list (x) (* x 2)) (defun f (a) (+ a 100)) \
215
1
                 (apply (function f) (list 5))"
216
1
            )
217
1
            .is_err()
218
    );
219
    // Unshadowed (fresh interpreter — the defun above persists per-interp): the
220
    // fast-path spreads → f(5) = 105.
221
1
    let mut normal = Interpreter::new(false).unwrap();
222
1
    assert_eq!(
223
1
        normal
224
1
            .eval("(defun f (a) (+ a 100)) (apply (function f) (list 5))")
225
1
            .unwrap(),
226
1
        vec![Value::Number(Fraction::from_integer(105))]
227
    );
228
1
}
229

            
230
#[test]
231
1
fn test_eval_apply_honors_same_call_list_rebind() {
232
    // The builtin-`LIST` check for APPLY's spread fast-path must run AFTER the
233
    // leading args evaluate (left-to-right), so a leading arg that rebinds
234
    // `list` is visible. Here a `(defun list …)` leading arg shadows it, so the
235
    // final `(list 5)` calls the user `list` (→ 10) and 10 isn't a valid
236
    // arg-list → structured error, NOT a silent spread of `5`.
237
1
    let mut interp = Interpreter::new(false).unwrap();
238
1
    assert!(
239
1
        interp
240
1
            .eval("(defun f (a b) b) (apply (function f) (defun list (x) (* x 2)) (list 5))")
241
1
            .is_err()
242
    );
243
1
}
244

            
245
// QUOTE
246

            
247
#[test]
248
1
fn test_eval_quote_number() {
249
1
    let mut interp = Interpreter::new(false).unwrap();
250
1
    let results = interp.eval("(quote 42)").unwrap();
251
1
    assert_eq!(results, vec![Value::Number(Fraction::from_integer(42))]);
252
1
}
253

            
254
#[test]
255
1
fn test_eval_quote_reader_sugar() {
256
1
    let mut interp = Interpreter::new(false).unwrap();
257
1
    let results = interp.eval("'42").unwrap();
258
1
    assert_eq!(results, vec![Value::Number(Fraction::from_integer(42))]);
259
1
}
260

            
261
#[test]
262
1
fn test_eval_quote_string() {
263
1
    let mut interp = Interpreter::new(false).unwrap();
264
1
    let results = interp.eval("(quote \"hello\")").unwrap();
265
1
    assert_eq!(results, vec![Value::String("hello".to_string())]);
266
1
}
267

            
268
// DESCRIBE
269

            
270
#[test]
271
1
fn test_eval_describe_function() {
272
1
    let mut interp = Interpreter::new(false).unwrap();
273
1
    let results = interp
274
1
        .eval(r#"(defun sum (a b) "Adds two numbers" (+ a b)) (describe sum)"#)
275
1
        .unwrap();
276
1
    match &results[0] {
277
1
        Value::String(s) => {
278
1
            assert!(s.contains("SUM is a function"), "got: {s}");
279
1
            assert!(s.contains("Lambda:"), "got: {s}");
280
1
            assert!(s.contains("Adds two numbers"), "got: {s}");
281
        }
282
        _ => panic!("expected string, got {:?}", results[0]),
283
    }
284
1
}
285

            
286
#[test]
287
1
fn test_eval_describe_variable() {
288
1
    let mut interp = Interpreter::new(false).unwrap();
289
1
    let results = interp
290
1
        .eval(r#"(defvar *x* 42 "The answer") (describe *x*)"#)
291
1
        .unwrap();
292
1
    match &results[0] {
293
1
        Value::String(s) => {
294
1
            assert!(s.contains("*X* is a variable"), "got: {s}");
295
1
            assert!(s.contains("Value: 42"), "got: {s}");
296
1
            assert!(s.contains("The answer"), "got: {s}");
297
        }
298
        _ => panic!("expected string, got {:?}", results[0]),
299
    }
300
1
}
301

            
302
// Nested calls
303

            
304
#[test]
305
1
fn test_nested_comparison_with_subtraction() {
306
1
    let mut interp = Interpreter::new(false).unwrap();
307
1
    assert_eq!(
308
1
        interp.eval("(= 1 (- 3 2))").unwrap(),
309
1
        vec![Value::Bool(true)]
310
    );
311
1
}
312

            
313
#[test]
314
1
fn test_nested_addition_with_multiplication() {
315
1
    let mut interp = Interpreter::new(false).unwrap();
316
1
    assert_eq!(
317
1
        interp.eval("(+ 1 (* 2 3))").unwrap(),
318
1
        vec![Value::Number(Fraction::from_integer(7))]
319
    );
320
1
}
321

            
322
#[test]
323
1
fn test_nested_subtraction_multiple_args() {
324
1
    let mut interp = Interpreter::new(false).unwrap();
325
1
    assert_eq!(
326
1
        interp.eval("(- 10 (+ 1 2) (- 5 2))").unwrap(),
327
1
        vec![Value::Number(Fraction::from_integer(4))]
328
    );
329
1
}
330

            
331
#[test]
332
1
fn test_nested_addition_both_sides() {
333
1
    let mut interp = Interpreter::new(false).unwrap();
334
1
    assert_eq!(
335
1
        interp.eval("(+ (+ 1 2) (+ 3 4))").unwrap(),
336
1
        vec![Value::Number(Fraction::from_integer(10))]
337
    );
338
1
}
339

            
340
#[test]
341
1
fn test_deeply_nested_addition() {
342
1
    let mut interp = Interpreter::new(false).unwrap();
343
1
    assert_eq!(
344
1
        interp.eval("(+ 1 (+ 2 (+ 3 4)))").unwrap(),
345
1
        vec![Value::Number(Fraction::from_integer(10))]
346
    );
347
1
}
348

            
349
#[test]
350
1
fn test_nested_calls_with_defun() {
351
1
    let mut interp = Interpreter::new(false).unwrap();
352
1
    assert_eq!(
353
1
        interp
354
1
            .eval("(defun double (x) (* x 2)) (+ (double 3) (double 4))")
355
1
            .unwrap(),
356
1
        vec![Value::Number(Fraction::from_integer(14))]
357
    );
358
1
}
359

            
360
#[test]
361
1
fn test_nested_equal_with_arithmetic() {
362
1
    let mut interp = Interpreter::new(false).unwrap();
363
1
    assert_eq!(
364
1
        interp.eval("(equal (+ 1 1) (- 4 2))").unwrap(),
365
1
        vec![Value::Bool(true)]
366
    );
367
1
}
368

            
369
#[test]
370
1
fn test_nested_funcall_with_arithmetic() {
371
1
    let mut interp = Interpreter::new(false).unwrap();
372
1
    assert_eq!(
373
1
        interp.eval("(funcall + (- 10 3) (* 2 3))").unwrap(),
374
1
        vec![Value::Number(Fraction::from_integer(13))]
375
    );
376
1
}