1
//! CAR/CDR/CONS list-construction extras, plus LENGTH / APPEND / PAIR?
2
//! (const-fold + runtime pair-chain paths).
3

            
4
use super::common::{compile_and_validate, compile_expect_error, wrap_with_runtime_ratio};
5

            
6
#[test]
7
1
fn car_of_static_pair() {
8
1
    compile_and_validate("(car (cons 1 nil))");
9
1
}
10

            
11
#[test]
12
1
fn cdr_of_static_pair() {
13
1
    compile_and_validate("(cdr (cons 1 (cons 2 nil)))");
14
1
}
15

            
16
#[test]
17
1
fn list_constructor_with_constants() {
18
1
    compile_and_validate("(list 1 2 3)");
19
1
}
20

            
21
#[test]
22
1
fn list_with_strings() {
23
1
    compile_and_validate("(list \"a\" \"b\" \"c\")");
24
1
}
25

            
26
#[test]
27
1
fn list_empty_is_nil() {
28
1
    compile_and_validate("(list)");
29
1
}
30

            
31
#[test]
32
1
fn cons_chain_three_levels() {
33
1
    compile_and_validate("(cons 1 (cons 2 (cons 3 (cons 4 nil))))");
34
1
}
35

            
36
#[test]
37
1
fn car_after_cons_of_quoted_constant() {
38
1
    compile_and_validate("(car '(1 2 3))");
39
1
}
40

            
41
#[test]
42
1
fn cdr_after_cons_of_quoted_constant() {
43
1
    compile_and_validate("(cdr '(1 2 3))");
44
1
}
45

            
46
#[test]
47
1
fn car_cdr_of_quoted_constant_at_value_position() {
48
    // Quoted-datum CAR/CDR lowers on BOTH compile surfaces: a numeric element
49
    // folds to its value; a quoted tail or a compound/symbol head renders as a
50
    // datum string. The eval-with-type path used to trap on the quoted tail.
51
1
    compile_and_validate("(cdr '(1 2 3))");
52
1
    compile_and_validate("(car '(1 2 3))");
53
1
    compile_and_validate("(car (cdr '(1 2 3)))");
54
1
    compile_and_validate("(car '((1 2) 3))");
55
1
    compile_and_validate("(car '(x y))");
56
1
    compile_and_validate("(cdr '(a b c))");
57
1
}
58

            
59
#[test]
60
1
fn reverse_cons_of_constant_list_at_value_position() {
61
    // Same divergence class as CAR/CDR: a constant / runtime-builder list
62
    // through REVERSE or CONS folds to a datum that the eval-with-type stack
63
    // handler used to reject. Both surfaces now lower it.
64
1
    compile_and_validate("(reverse '(1 2 3))");
65
1
    compile_and_validate("(reverse (list 1 2 3))");
66
1
    compile_and_validate("(cons 0 '(1 2 3))");
67
1
    compile_and_validate("(cons 0 (list 1 2 3))");
68
1
    compile_and_validate("(cons 1 2)");
69
    // All-constant APPEND folds to a datum on both surfaces (incl. a symbol
70
    // list, which renders to its printed form rather than trapping).
71
1
    compile_and_validate("(append '(1 2) '(3))");
72
1
    compile_and_validate("(append '(a b) '(c))");
73
1
}
74

            
75
#[test]
76
1
fn map_over_static_list() {
77
1
    compile_and_validate("(defun double (x) (* x 2)) (map (function double) '(1 2 3))");
78
1
}
79

            
80
#[test]
81
1
fn reverse_static_list() {
82
1
    compile_and_validate("(reverse '(1 2 3))");
83
1
}
84

            
85
#[test]
86
1
fn reverse_empty_list() {
87
1
    compile_and_validate("(reverse '())");
88
1
}
89

            
90
#[test]
91
1
fn map_with_let_bound_lambda_over_static_list() {
92
    // Runtime closure dispatch on a literal list — rewrites per-element
93
    // funcall via call_ref.
94
1
    compile_and_validate("(let ((double (lambda (x) (* x 2)))) (map double '(1 2 3)))");
95
1
}
96

            
97
#[test]
98
1
fn map_with_let_bound_lambda_over_runtime_pair_chain() {
99
    // Runtime closure + runtime PairRef list — full reverse-build-then-
100
    // reverse loop.
101
1
    compile_and_validate(&wrap_with_runtime_ratio(
102
1
        "(let ((double (lambda (n) (* n 2)))) \
103
1
         (map double (cons X (cons (transaction-post-date 1) nil))))",
104
1
    ));
105
1
}
106

            
107
#[test]
108
1
fn map_static_fn_over_runtime_pair_chain() {
109
    // Static defun + runtime PairRef list — exercise the runtime-list
110
    // path with a non-closure callee.
111
1
    compile_and_validate(&wrap_with_runtime_ratio(
112
1
        "(defun double (n) (* n 2)) \
113
1
         (map (function double) (cons X (cons (transaction-post-date 1) nil)))",
114
1
    ));
115
1
}
116

            
117
#[test]
118
1
fn reverse_runtime_pair_chain() {
119
    // (cons X (cons (transaction-post-date 1) ...)) builds a runtime
120
    // PairRef<ratio> chain — reverse must walk the cells via the
121
    // pair_new accumulator loop, not constant-fold.
122
1
    compile_and_validate(&wrap_with_runtime_ratio(
123
1
        "(reverse (cons X (cons (transaction-post-date 1) nil)))",
124
1
    ));
125
1
}
126

            
127
#[test]
128
1
fn filter_constant_fold_with_static_pred() {
129
1
    compile_and_validate("(defun pos? (x) (< 0 x)) (filter (function pos?) '(1 -1 2 -2 3))");
130
1
}
131

            
132
#[test]
133
1
fn filter_with_let_bound_lambda_over_static_list() {
134
    // Runtime closure dispatch over a literal list — per-element
135
    // FUNCALL → call_ref + truth-flag prepend loop.
136
1
    compile_and_validate("(let ((pos? (lambda (x) (< 0 x)))) (filter pos? '(1 -1 2 -2 3)))");
137
1
}
138

            
139
#[test]
140
1
fn filter_static_fn_over_runtime_pair_chain() {
141
    // Static predicate + runtime PairRef list — exercises the
142
    // runtime-list path with the predicate routed through FUNCALL.
143
1
    compile_and_validate(&wrap_with_runtime_ratio(
144
1
        "(defun pos? (x) (< 0 x)) \
145
1
         (filter (function pos?) (cons X (cons (transaction-post-date 1) nil)))",
146
1
    ));
147
1
}
148

            
149
#[test]
150
1
fn filter_with_let_bound_lambda_over_runtime_pair_chain() {
151
    // Runtime closure + runtime PairRef — full reverse-build-then-
152
    // reverse loop with conditional prepend.
153
1
    compile_and_validate(&wrap_with_runtime_ratio(
154
1
        "(let ((pos? (lambda (x) (< 0 x)))) \
155
1
         (filter pos? (cons X (cons (transaction-post-date 1) nil))))",
156
1
    ));
157
1
}
158

            
159
#[test]
160
1
fn fold_constant_fold_sum() {
161
1
    compile_and_validate("(defun add (a b) (+ a b)) (fold (function add) 0 '(1 2 3 4))");
162
1
}
163

            
164
#[test]
165
1
fn fold_with_let_bound_lambda_over_static_list() {
166
    // Runtime closure threading the accumulator through the literal
167
    // path's per-element FUNCALL → call_ref.
168
1
    compile_and_validate(
169
1
        "(let ((acc-add (lambda (acc x) (+ acc x)))) (fold acc-add 0 '(1 2 3 4)))",
170
    );
171
1
}
172

            
173
#[test]
174
1
fn fold_static_fn_over_runtime_pair_chain() {
175
    // Static fn + runtime PairRef list — runtime-list FOLD path.
176
1
    compile_and_validate(&wrap_with_runtime_ratio(
177
1
        "(defun add (a b) (+ a b)) \
178
1
         (fold (function add) 0 (cons X (cons (transaction-post-date 1) nil)))",
179
1
    ));
180
1
}
181

            
182
#[test]
183
1
fn fold_with_let_bound_lambda_over_runtime_pair_chain() {
184
1
    compile_and_validate(&wrap_with_runtime_ratio(
185
1
        "(let ((acc-add (lambda (acc x) (+ acc x)))) \
186
1
         (fold acc-add 0 (cons X (cons (transaction-post-date 1) nil))))",
187
1
    ));
188
1
}
189

            
190
// LENGTH / APPEND / PAIR? — previously phantom natives (registered
191
// symbols + documented, no codegen handler). Regression for that hole.
192

            
193
#[test]
194
1
fn length_constant_folds() {
195
1
    compile_and_validate("(length '(1 2 3))");
196
1
}
197

            
198
#[test]
199
1
fn length_in_index_arithmetic() {
200
    // ADR-0028: LENGTH is an Index (a count), so it composes with Index
201
    // arithmetic — `(+ 1 (length …))` is Index + Index → Index.
202
1
    compile_and_validate("(+ 1 (length '(1 2 3)))");
203
1
}
204

            
205
#[test]
206
1
fn length_runtime_in_index_arithmetic() {
207
    // A runtime LENGTH is a runtime Index and composes with an integer literal
208
    // under Index arithmetic (it does not implicitly become a Scalar).
209
1
    compile_and_validate(&wrap_with_runtime_ratio(
210
1
        "(+ (length (cons X (cons (transaction-post-date 1) nil))) 1)",
211
1
    ));
212
1
}
213

            
214
#[test]
215
1
fn length_runtime_pair_chain() {
216
1
    compile_and_validate(&wrap_with_runtime_ratio(
217
1
        "(length (cons X (cons (transaction-post-date 1) nil)))",
218
1
    ));
219
1
}
220

            
221
#[test]
222
1
fn length_runtime_compared_to_constant() {
223
    // The canonical use: `(= (length …) N)` on a runtime chain.
224
1
    compile_and_validate(&wrap_with_runtime_ratio(
225
1
        "(= (length (cons X (cons (transaction-post-date 1) nil))) 2)",
226
1
    ));
227
1
}
228

            
229
#[test]
230
1
fn append_constant_folds() {
231
1
    compile_and_validate("(append '(1 2) '(3 4))");
232
1
}
233

            
234
#[test]
235
1
fn append_runtime_chain_with_nil() {
236
1
    compile_and_validate(&wrap_with_runtime_ratio(
237
1
        "(append (cons X (cons (transaction-post-date 1) nil)) nil)",
238
1
    ));
239
1
}
240

            
241
/// General concat: a runtime list followed by a NON-empty constant list. The
242
/// constant operand is materialized into a runtime `$pair` chain (the cell
243
/// type is monomorphic), so this is no longer rejected.
244
#[test]
245
1
fn append_runtime_chain_then_constant_list() {
246
1
    compile_and_validate(&wrap_with_runtime_ratio("(append (cons X nil) '(1 2))"));
247
1
}
248

            
249
/// Mirror: a constant list followed by a runtime chain.
250
#[test]
251
1
fn append_constant_list_then_runtime_chain() {
252
1
    compile_and_validate(&wrap_with_runtime_ratio("(append '(1 2) (cons X nil))"));
253
1
}
254

            
255
/// A constant string list materialized alongside a runtime chain — string
256
/// members lower through the cons builder as data.
257
#[test]
258
1
fn append_constant_string_list_with_runtime_chain() {
259
1
    compile_and_validate(&wrap_with_runtime_ratio(
260
1
        "(append (cons (transaction-post-date 1) nil) '(7 8))",
261
1
    ));
262
1
}
263

            
264
/// Round-2 regression: a constant list whose members aren't runtime-
265
/// representable (symbols — no runtime value anywhere in the language) must
266
/// give the standard "cannot compile to WASM stack value" error, NOT the
267
/// misleading "Undefined symbol A" that came from re-evaluating the datum as a
268
/// variable reference. The members are quoted before materialization so a
269
/// symbol surfaces the clean error. (Full symbol-list support is a separate
270
/// slice.)
271
#[test]
272
1
fn append_constant_symbol_list_gives_clean_error() {
273
    use super::common::compile_expect_error;
274
1
    let err = compile_expect_error(&wrap_with_runtime_ratio("(append '(a b) (cons X nil))"));
275
1
    assert!(
276
1
        !err.contains("Undefined symbol"),
277
        "expected a clean stack-value error, got: {err}"
278
    );
279
1
    assert!(
280
1
        err.contains("cannot compile") || err.contains("WASM stack value"),
281
        "got: {err}"
282
    );
283
1
}
284

            
285
#[test]
286
1
fn pair_p_true_on_constant_list() {
287
1
    compile_and_validate("(pair? '(1 2 3))");
288
1
}
289

            
290
#[test]
291
1
fn pair_p_false_on_nil() {
292
1
    compile_and_validate("(pair? nil)");
293
1
}
294

            
295
#[test]
296
1
fn pair_p_runtime_chain() {
297
1
    compile_and_validate(&wrap_with_runtime_ratio("(pair? (cons X nil))"));
298
1
}
299

            
300
#[test]
301
1
fn pair_p_in_value_position() {
302
1
    compile_and_validate("(defun is-pair (x) (pair? x)) (is-pair '(1))");
303
1
}
304

            
305
// MAP / FILTER / FOLD with a callback whose body isn't constant-foldable
306
// (produces a runtime value or a side effect). The const-folders used to
307
// either crash (FOLD: serialize a placeholder with no stack producer) or
308
// silently bake the placeholder in / drop the side effect (MAP/FILTER). They
309
// now detect a runtime callback result and route to per-element runtime
310
// lowering. Pre-existing bug class, fixed together.
311

            
312
#[test]
313
1
fn fold_nil_seed_build_list_over_runtime_list() {
314
    // nil-seeded accumulator built into a list over a runtime chain — the
315
    // accumulator is sized from the body's PairRef return, nil seeds to a
316
    // null pair (was: "init type bool doesn't match accumulator type").
317
1
    compile_and_validate(&wrap_with_runtime_ratio(
318
1
        "(fold (lambda (acc x) (cons x acc)) nil (cons X nil))",
319
1
    ));
320
1
}
321

            
322
#[test]
323
1
fn fold_print_body_runs_at_runtime() {
324
1
    compile_and_validate("(fold (lambda (acc x) (print x)) nil '(1 2))");
325
1
}
326

            
327
#[test]
328
1
fn map_print_body_emits_per_element() {
329
1
    let with = compile_and_validate("(map (lambda (x) (print \"yyyyyyyy\")) '(1 2 3))");
330
1
    let without = compile_and_validate("(map (lambda (x) x) '(1 2 3))");
331
1
    assert!(
332
1
        with.len() > without.len(),
333
        "map per-element print dropped: {} vs {}",
334
        with.len(),
335
        without.len()
336
    );
337
1
}
338

            
339
#[test]
340
1
fn filter_print_body_emits_per_element() {
341
1
    let with = compile_and_validate("(filter (lambda (x) (print \"yyyyyyyy\")) '(1 2 3))");
342
1
    let without = compile_and_validate("(filter (lambda (x) #t) '(1 2 3))");
343
1
    assert!(
344
1
        with.len() > without.len(),
345
        "filter per-element print dropped: {} vs {}",
346
        with.len(),
347
        without.len()
348
    );
349
1
}
350

            
351
#[test]
352
1
fn map_runtime_body_over_constant_list() {
353
1
    compile_and_validate("(map (lambda (x) (transaction-post-date 0)) '(1 2))");
354
1
}
355

            
356
#[test]
357
1
fn filter_runtime_predicate_over_constant_list() {
358
1
    compile_and_validate("(filter (lambda (x) (transaction-is-multi-currency 0)) '(1 2))");
359
1
}
360

            
361
// Runtime-bail edge cases (HOF review round). Each was rejected/mis-lowered
362
// after the initial const-fold→runtime bail; now handled.
363

            
364
/// FOLD forced onto the runtime path (side-effecting body) with a NON-nil
365
/// constant-list accumulator seed. The seed is materialized into a runtime
366
/// `$pair` chain (was: "cannot compile to WASM stack value: '(1)").
367
#[test]
368
1
fn fold_runtime_body_with_constant_list_seed() {
369
1
    compile_and_validate("(fold (lambda (acc x) (print x) acc) '(1) '(2))");
370
1
}
371

            
372
/// MAP whose per-element results have DIFFERENT types widens the result chain
373
/// to `AnyRef` (ADR-0025) instead of erroring "heterogeneous result elements".
374
#[test]
375
1
fn map_heterogeneous_results_widen_to_anyref() {
376
1
    compile_and_validate("(map (lambda (x) (if (= x 1) (print x) (tag-name 0))) '(1 2))");
377
1
}
378

            
379
/// FILTER over a heterogeneous constant list (ratio + string) widens the kept
380
/// chain to `AnyRef` instead of erroring "heterogeneous element types".
381
#[test]
382
1
fn filter_heterogeneous_list_widens_to_anyref() {
383
1
    compile_and_validate("(filter (lambda (x) (print x)) '(1 \"a\"))");
384
1
}
385

            
386
/// MAP over MULTIPLE constant lists with a non-constant-foldable body lowers
387
/// row-by-row at runtime (was: "requires exactly one list argument").
388
#[test]
389
1
fn map_multi_list_runtime_body() {
390
1
    compile_and_validate("(map (lambda (x y) (print x)) '(1 2) '(3 4))");
391
1
}
392

            
393
/// Multi-list MAP that IS constant-foldable still folds.
394
#[test]
395
1
fn map_multi_list_constant_folds() {
396
1
    compile_and_validate("(map (lambda (a b) (+ a b)) '(1 2) '(3 4))");
397
1
}
398

            
399
/// ADR-0028: a FOLD over a let-bound closure, bound in turn in a `let`, sizes
400
/// the binding local from the COMPILED (closure-sig) accumulator type. The
401
/// eval side can't see the closure sig and falls back to the integer seed's
402
/// type (Index); without sizing from codegen the binding would `local.set` a
403
/// Ratio value into an I32 local — invalid wasm.
404
#[test]
405
1
fn fold_closure_result_bound_in_let_compiles() {
406
1
    compile_and_validate(
407
1
        "(let* ((f (lambda (a x) (+ a x))) \
408
1
                (acc (fold f 0 (cons (transaction-post-date 0) nil)))) \
409
1
           acc)",
410
    );
411
1
}
412

            
413
/// A wrong-arity closure used as a FILTER predicate over an EMPTY list is still
414
/// arity-checked — no element reaches `compile_call_ref`, so the empty-list
415
/// fast path validates the predicate's arity itself.
416
#[test]
417
1
fn filter_wrong_arity_closure_over_empty_list_errors() {
418
1
    let err = compile_expect_error("(let ((f (lambda (a b) (< a b)))) (filter f '()))");
419
1
    assert!(
420
1
        err.to_lowercase().contains("arity") || err.contains("closure") || err.contains("argument"),
421
        "expected an arity error, got: {err}"
422
    );
423
1
}