1
use crate::ast::{Expr, PairElement, WasmType};
2

            
3
use super::infer::{chain_pair_element, infer_pair_element};
4

            
5
23
fn int(n: i64) -> Expr {
6
23
    Expr::Number(crate::ast::Fraction::from_integer(n))
7
23
}
8

            
9
#[test]
10
1
fn chain_of_integer_literals_is_an_index_chain() {
11
1
    assert_eq!(
12
1
        chain_pair_element(&[int(1), int(2)]).unwrap(),
13
        PairElement::I32
14
    );
15
1
}
16

            
17
#[test]
18
1
fn chain_integer_literals_cross_to_a_ratio_neighbor() {
19
1
    let half = Expr::Number(crate::ast::Fraction::new(1, 2));
20
1
    assert_eq!(
21
1
        chain_pair_element(&[half, int(3)]).unwrap(),
22
        PairElement::Ratio
23
    );
24
1
    assert_eq!(
25
1
        chain_pair_element(&[Expr::WasmRuntime(WasmType::Ratio), int(3)]).unwrap(),
26
        PairElement::Ratio
27
    );
28
1
}
29

            
30
#[test]
31
1
fn chain_integer_literal_beyond_i31_takes_a_ratio_cell() {
32
3
    for big in [1 << 30, i64::from(i32::MAX), -(1 << 30) - 1] {
33
3
        assert_eq!(
34
3
            chain_pair_element(&[int(big), int(2)]).unwrap(),
35
            PairElement::Ratio,
36
            "literal {big}"
37
        );
38
3
        assert_eq!(
39
3
            chain_pair_element(&[int(big), Expr::WasmRuntime(WasmType::I32)]).unwrap(),
40
            PairElement::AnyRef,
41
            "literal {big} beside a runtime index"
42
        );
43
    }
44
1
    assert_eq!(
45
1
        chain_pair_element(&[int((1 << 30) - 1), int(-(1 << 30))]).unwrap(),
46
        PairElement::I32
47
    );
48
1
}
49

            
50
#[test]
51
1
fn chain_of_bool_and_nil_uses_bool() {
52
1
    assert_eq!(
53
1
        chain_pair_element(&[Expr::T, Expr::Nil]).unwrap(),
54
        PairElement::Bool
55
    );
56
1
}
57

            
58
#[test]
59
1
fn chain_integer_literal_adopts_runtime_index_on_either_side() {
60
1
    let index = Expr::WasmRuntime(WasmType::I32);
61
1
    assert_eq!(
62
1
        chain_pair_element(&[index.clone(), int(2)]).unwrap(),
63
        PairElement::I32
64
    );
65
1
    assert_eq!(
66
1
        chain_pair_element(&[int(2), index]).unwrap(),
67
        PairElement::I32
68
    );
69
1
}
70

            
71
#[test]
72
1
fn chain_literal_tail_disagreeing_with_runtime_head_widens_to_anyref() {
73
1
    let scalar = Expr::WasmRuntime(WasmType::Ratio);
74
1
    assert_eq!(
75
1
        chain_pair_element(&[scalar, Expr::String("a".to_string())]).unwrap(),
76
        PairElement::AnyRef
77
    );
78
1
}
79

            
80
#[test]
81
1
fn chain_integer_literal_against_non_numeric_slot_widens_to_anyref() {
82
1
    assert_eq!(
83
1
        chain_pair_element(&[int(1), Expr::Nil]).unwrap(),
84
        PairElement::AnyRef
85
    );
86
1
    assert_eq!(
87
1
        chain_pair_element(&[Expr::String("a".to_string()), int(1)]).unwrap(),
88
        PairElement::AnyRef
89
    );
90
1
}
91

            
92
#[test]
93
1
fn chain_fractional_literal_against_runtime_index_widens_to_anyref() {
94
1
    let half = Expr::Number(crate::ast::Fraction::new(1, 2));
95
1
    assert_eq!(
96
1
        chain_pair_element(&[Expr::WasmRuntime(WasmType::I32), half]).unwrap(),
97
        PairElement::AnyRef
98
    );
99
1
}
100

            
101
#[test]
102
1
fn chain_of_unrepresentable_datum_or_nothing_uses_anyref() {
103
1
    assert_eq!(chain_pair_element(&[]).unwrap(), PairElement::AnyRef);
104
1
    assert_eq!(
105
1
        chain_pair_element(&[Expr::Symbol("A".to_string())]).unwrap(),
106
        PairElement::AnyRef
107
    );
108
1
}
109

            
110
#[test]
111
1
fn chain_rejects_runtime_element_that_cannot_ride_a_pair_cell() {
112
1
    let nested = Expr::WasmRuntime(WasmType::PairRef(PairElement::I32));
113
1
    assert!(chain_pair_element(&[nested]).is_err());
114
1
}
115

            
116
/// `infer_pair_element` decides the static PairElement from a
117
/// `(cons car cdr)` form, balancing the car's compile-time type
118
/// against the cdr's. The legacy integration test reached this
119
/// via `Symbol::new("BAL").with_value(Expr::WasmRuntime(Commodity))`
120
/// in the symbol table; the unit form exercises the function
121
/// directly so the contract is locked in without needing a
122
/// script-mode Commodity producer.
123
#[test]
124
1
fn commodity_car_with_nil_cdr_is_commodity_pair() {
125
1
    let car = Expr::WasmRuntime(WasmType::Commodity);
126
1
    let elem = infer_pair_element(&car, &Expr::Nil).unwrap();
127
1
    assert_eq!(elem, PairElement::Commodity);
128
1
}
129

            
130
#[test]
131
1
fn commodity_car_with_pair_of_i32_cdr_widens_to_anyref() {
132
    // ADR-0025 escape hatch: when the car and cdr disagree on element
133
    // type, CONS widens to `PairElement::AnyRef` (canonical Common Lisp
134
    // heterogeneous `cons`) rather than rejecting. The strict typed path
135
    // still applies when both arms agree.
136
1
    let car = Expr::WasmRuntime(WasmType::Commodity);
137
1
    let cdr = Expr::WasmRuntime(WasmType::PairRef(PairElement::I32));
138
1
    let elem = infer_pair_element(&car, &cdr).expect("heterogeneous CONS widens to AnyRef");
139
1
    assert_eq!(elem, PairElement::AnyRef);
140
1
}
141

            
142
#[test]
143
1
fn ratio_car_with_pair_of_ratio_cdr_resolves_to_ratio_pair() {
144
1
    let car = Expr::WasmRuntime(WasmType::Ratio);
145
1
    let cdr = Expr::WasmRuntime(WasmType::PairRef(PairElement::Ratio));
146
1
    let elem = infer_pair_element(&car, &cdr).unwrap();
147
1
    assert_eq!(elem, PairElement::Ratio);
148
1
}
149

            
150
#[test]
151
1
fn integer_number_car_with_nil_cdr_is_an_index_cell() {
152
1
    let elem = infer_pair_element(&int(1), &Expr::Nil).unwrap();
153
1
    assert_eq!(elem, PairElement::I32);
154
1
}
155

            
156
#[test]
157
1
fn integer_number_car_adopts_a_ratio_cdr() {
158
1
    let cdr = Expr::WasmRuntime(WasmType::PairRef(PairElement::Ratio));
159
1
    assert_eq!(
160
1
        infer_pair_element(&int(1), &cdr).unwrap(),
161
        PairElement::Ratio
162
    );
163
1
}
164

            
165
#[test]
166
1
fn nil_car_takes_the_bool_slot_and_widens() {
167
1
    let index_cdr = Expr::WasmRuntime(WasmType::PairRef(PairElement::I32));
168
1
    assert_eq!(
169
1
        infer_pair_element(&Expr::Nil, &index_cdr).unwrap(),
170
        PairElement::AnyRef
171
    );
172
1
    let bool_cdr = Expr::WasmRuntime(WasmType::PairRef(PairElement::Bool));
173
1
    assert_eq!(
174
1
        infer_pair_element(&Expr::Nil, &bool_cdr).unwrap(),
175
        PairElement::Bool
176
    );
177
1
}
178

            
179
#[test]
180
1
fn runtime_car_unifies_with_quoted_list_cdr_members() {
181
2
    let quoted = |members: Vec<Expr>| Expr::Quote(Box::new(Expr::List(members)));
182
1
    let index = Expr::WasmRuntime(WasmType::I32);
183
1
    assert_eq!(
184
1
        infer_pair_element(&index, &quoted(vec![int(1), int(2)])).unwrap(),
185
        PairElement::I32
186
    );
187
1
    assert_eq!(
188
1
        infer_pair_element(&index, &quoted(vec![Expr::Nil])).unwrap(),
189
        PairElement::AnyRef
190
    );
191
1
}
192

            
193
#[test]
194
1
fn constant_car_without_slot_adopts_typed_cdr_or_is_rejected() {
195
1
    let symbol = Expr::Quote(Box::new(Expr::Symbol("A".to_string())));
196
1
    let cdr = Expr::WasmRuntime(WasmType::PairRef(PairElement::StringRef));
197
1
    assert_eq!(
198
1
        infer_pair_element(&symbol, &cdr).unwrap(),
199
        PairElement::StringRef
200
    );
201
1
    assert!(infer_pair_element(&symbol, &Expr::Nil).is_err());
202
1
}
203

            
204
#[test]
205
1
fn runtime_car_that_cannot_ride_a_cell_is_rejected() {
206
1
    let nested = Expr::WasmRuntime(WasmType::PairRef(PairElement::I32));
207
1
    assert!(infer_pair_element(&nested, &Expr::Nil).is_err());
208
1
}
209

            
210
#[test]
211
1
fn fractional_number_car_with_nil_cdr_uses_ratio_default() {
212
1
    let car = Expr::Number(crate::ast::Fraction::new(1, 2));
213
1
    let elem = infer_pair_element(&car, &Expr::Nil).unwrap();
214
1
    assert_eq!(elem, PairElement::Ratio);
215
1
}
216

            
217
#[test]
218
1
fn bool_car_with_nil_cdr_uses_bool_slot() {
219
    // A bool literal car rides PairElement::Bool (not I32), so CAR recovers a
220
    // truth value that serializes as Nil/Bool rather than Number.
221
1
    let car = Expr::T;
222
1
    let elem = infer_pair_element(&car, &Expr::Nil).unwrap();
223
1
    assert_eq!(elem, PairElement::Bool);
224
1
}
225

            
226
#[test]
227
1
fn string_car_with_nil_cdr_uses_stringref() {
228
1
    let car = Expr::String("hi".to_string());
229
1
    let elem = infer_pair_element(&car, &Expr::Nil).unwrap();
230
1
    assert_eq!(elem, PairElement::StringRef);
231
1
}
232

            
233
#[test]
234
1
fn anyref_cdr_propagates_anyref_pair_element() {
235
1
    let car = Expr::WasmRuntime(WasmType::Ratio);
236
1
    let cdr = Expr::WasmRuntime(WasmType::PairRef(PairElement::AnyRef));
237
1
    let elem = infer_pair_element(&car, &cdr).expect("AnyRef cdr accepts any car");
238
1
    assert_eq!(elem, PairElement::AnyRef);
239
1
}
240

            
241
#[test]
242
1
fn ratio_car_with_pair_of_string_cdr_widens_to_anyref() {
243
1
    let car = Expr::WasmRuntime(WasmType::Ratio);
244
1
    let cdr = Expr::WasmRuntime(WasmType::PairRef(PairElement::StringRef));
245
1
    let elem = infer_pair_element(&car, &cdr).expect("heterogeneous CONS widens to AnyRef");
246
1
    assert_eq!(elem, PairElement::AnyRef);
247
1
}
248

            
249
#[test]
250
1
fn anyref_widen_is_symmetric_and_idempotent() {
251
1
    assert_eq!(
252
1
        PairElement::I32.widen(PairElement::Ratio),
253
        PairElement::AnyRef
254
    );
255
1
    assert_eq!(
256
1
        PairElement::Ratio.widen(PairElement::I32),
257
        PairElement::AnyRef
258
    );
259
1
    assert_eq!(
260
1
        PairElement::AnyRef.widen(PairElement::I32),
261
        PairElement::AnyRef
262
    );
263
1
    assert_eq!(PairElement::I32.widen(PairElement::I32), PairElement::I32);
264
1
}
265

            
266
#[test]
267
1
fn literal_string_car_with_pair_of_ratio_cdr_widens_to_anyref() {
268
    // Regression: before the literal-shape branch ran ahead of the
269
    // cdr-element fallback, a literal String car with a typed Ratio
270
    // cdr was silently classified as Ratio and erroried at codegen
271
    // ("expected ratio, got string"). The widening contract says a
272
    // disagreeing arm widens to AnyRef instead.
273
1
    let car = Expr::String("label".to_string());
274
1
    let cdr = Expr::WasmRuntime(WasmType::PairRef(PairElement::Ratio));
275
1
    let elem = infer_pair_element(&car, &cdr).expect("literal car widens against typed cdr");
276
1
    assert_eq!(elem, PairElement::AnyRef);
277
1
}
278

            
279
#[test]
280
1
fn pair_element_anyref_round_trips_through_wasm_type() {
281
1
    assert_eq!(PairElement::AnyRef.as_wasm_type(), WasmType::AnyRef);
282
1
    assert_eq!(
283
1
        PairElement::from_wasm_type(WasmType::AnyRef),
284
        Some(PairElement::AnyRef)
285
    );
286
1
}