1
use super::*;
2
use crate::ast::Fraction;
3

            
4
42
fn sym(s: &str) -> Expr {
5
42
    Expr::Symbol(s.to_string())
6
42
}
7
30
fn list(items: Vec<Expr>) -> Expr {
8
30
    Expr::List(items)
9
30
}
10
8
fn num(n: i64) -> Expr {
11
8
    Expr::Number(Fraction::from_integer(n))
12
8
}
13

            
14
4
fn pair(elem: PairElement) -> Option<WasmType> {
15
4
    Some(WasmType::PairRef(elem))
16
4
}
17

            
18
#[test]
19
1
fn cons_cdr_literal_car_is_that_cell() {
20
1
    let body = list(vec![sym("CONS"), num(1), sym("acc")]);
21
1
    assert_eq!(pinned_param_type(&body, "acc"), pair(PairElement::I32));
22
1
}
23

            
24
#[test]
25
1
fn cons_cdr_nonliteral_car_widens_to_anyref() {
26
1
    let body = list(vec![sym("CONS"), sym("x"), sym("acc")]);
27
1
    assert_eq!(pinned_param_type(&body, "acc"), pair(PairElement::AnyRef));
28
1
    assert_eq!(pinned_param_type(&body, "x"), None);
29
1
}
30

            
31
#[test]
32
1
fn no_cons_cdr_use_is_unconstrained() {
33
1
    let body = list(vec![sym("*"), sym("x"), num(2)]);
34
1
    assert_eq!(pinned_param_type(&body, "x"), None);
35
1
}
36

            
37
#[test]
38
1
fn string_native_arg_pins_stringref() {
39
    // (string= s "x") → s is a string.
40
1
    let body = list(vec![sym("STRING="), sym("s"), Expr::String("x".into())]);
41
1
    assert_eq!(pinned_param_type(&body, "s"), Some(WasmType::StringRef));
42
1
}
43

            
44
#[test]
45
1
fn quoted_string_native_does_not_pin() {
46
    // (quote (string= x "a")) — quoted data must NOT pin `x` to StringRef.
47
1
    let body = list(vec![
48
1
        sym("quote"),
49
1
        list(vec![sym("string="), sym("x"), Expr::String("a".into())]),
50
    ]);
51
1
    assert_eq!(pinned_param_type(&body, "x"), None);
52
1
}
53

            
54
#[test]
55
1
fn quasiquoted_string_native_does_not_pin() {
56
1
    let body = list(vec![
57
1
        sym("quasiquote"),
58
1
        list(vec![sym("string="), sym("x"), Expr::String("a".into())]),
59
    ]);
60
1
    assert_eq!(pinned_param_type(&body, "x"), None);
61
1
}
62

            
63
#[test]
64
1
fn shadowing_let_body_does_not_retype_outer_param() {
65
    // (let ((x nil)) (cons 1 x)) — cdr is the INNER `x`; outer stays None.
66
1
    let inner = list(vec![
67
1
        sym("LET"),
68
1
        list(vec![list(vec![sym("x"), Expr::Nil])]),
69
1
        list(vec![sym("CONS"), num(1), sym("x")]),
70
    ]);
71
1
    assert_eq!(pinned_param_type(&inner, "x"), None);
72
1
}
73

            
74
#[test]
75
1
fn let_initializer_cons_cdr_is_still_outer_scope() {
76
    // (let ((acc (cons 1 acc))) acc) — the init `(cons 1 acc)` references the
77
    // OUTER `acc` (the binding isn't in scope for its own init), so `acc` IS
78
    // a list accumulator. Scanning must reach the init even though the let
79
    // rebinds `acc`.
80
1
    let form = list(vec![
81
1
        sym("LET"),
82
1
        list(vec![list(vec![
83
1
            sym("acc"),
84
1
            list(vec![sym("CONS"), num(1), sym("acc")]),
85
        ])]),
86
1
        sym("acc"),
87
    ]);
88
1
    assert_eq!(pinned_param_type(&form, "acc"), pair(PairElement::I32));
89
1
}
90

            
91
#[test]
92
1
fn dolist_list_expr_cons_cdr_is_outer_scope() {
93
    // (dolist (e (cons 1 acc)) e) — the list expr references outer `acc`.
94
1
    let form = list(vec![
95
1
        sym("DOLIST"),
96
1
        list(vec![sym("e"), list(vec![sym("CONS"), num(1), sym("acc")])]),
97
1
        sym("e"),
98
    ]);
99
1
    assert_eq!(pinned_param_type(&form, "acc"), pair(PairElement::I32));
100
1
}
101

            
102
#[test]
103
1
fn lambda_binder_shadows_outer_param() {
104
1
    let body = list(vec![
105
1
        list(vec![
106
1
            sym("LAMBDA"),
107
1
            list(vec![sym("x")]),
108
1
            list(vec![sym("CONS"), num(1), sym("x")]),
109
        ]),
110
1
        sym("y"),
111
    ]);
112
1
    assert_eq!(pinned_param_type(&body, "x"), None);
113
1
}
114

            
115
#[test]
116
1
fn short_binder_body_does_not_panic_on_slice() {
117
    // Regression (AFL): a too-short binder form — `(do)`, `(let)`,
118
    // `(lambda)` — used to index `&elems[2..]` / `&elems[1..]` out of range
119
    // and panic. Param inference is best-effort, so a malformed form must
120
    // contribute no pin (return None), never crash.
121
6
    for head in ["DO", "DO*", "LET", "LET*", "DOLIST", "LAMBDA"] {
122
6
        let bare = list(vec![sym(head)]);
123
6
        assert_eq!(pinned_param_type(&bare, "x"), None, "bare ({head})");
124
    }
125
    // Nested under a lambda body, mirroring the fuzz-found shape
126
    // `(lambda (s) (do))`.
127
1
    let nested = list(vec![sym("DO")]);
128
1
    assert_eq!(pinned_param_type(&nested, "s"), None);
129
1
}
130

            
131
#[test]
132
1
fn literal_car_cells_map_to_their_slots() {
133
1
    assert_eq!(car_cell_element(&num(3)), PairElement::I32);
134
1
    assert_eq!(car_cell_element(&num(1 << 30)), PairElement::Ratio);
135
1
    assert_eq!(car_cell_element(&Expr::Nil), PairElement::Bool);
136
1
    assert_eq!(car_cell_element(&Expr::T), PairElement::Bool);
137
1
    assert_eq!(
138
1
        car_cell_element(&Expr::String("s".into())),
139
        PairElement::StringRef
140
    );
141
1
    assert_eq!(car_cell_element(&sym("v")), PairElement::AnyRef);
142
1
}