1
//! Cross-module tests for the iteration family. Span `do_loop` and
2
//! `common` because the form-result-type inference is exercised end-
3
//! to-end via `do_form` / `do_star_form`. Keeping the tests in a
4
//! sibling module instead of either sub-module avoids re-exporting
5
//! the private inference helpers just to test them.
6

            
7
use crate::ast::{Expr, Fraction, PairElement, WasmType};
8
use crate::runtime::{Symbol, SymbolKind, SymbolTable};
9

            
10
use super::common::infer_wasm_type;
11
use super::do_form::do_form;
12
use super::do_star_form::do_star_form;
13

            
14
/// Constructs `(do ((i 0 (+ i 1))) ((= i N) result) (setf result
15
/// (cons cons_car result)))` — the canonical tag-sync pattern.
16
/// `N` is a runtime symbol forcing the runtime path; `result` is a
17
/// pre-bound accumulator that the body builds via setf. The
18
/// caller varies `cons_car` to exercise the static PairElement
19
/// inference that the codegen path actually emits for the
20
/// accumulator.
21
3
fn do_with_setf_cons_car(cons_car: Expr) -> Vec<Expr> {
22
3
    let i_var = Expr::List(vec![
23
3
        Expr::Symbol("i".into()),
24
3
        Expr::Number(Fraction::from_integer(0)),
25
3
        Expr::List(vec![
26
3
            Expr::Symbol("+".into()),
27
3
            Expr::Symbol("i".into()),
28
3
            Expr::Number(Fraction::from_integer(1)),
29
3
        ]),
30
3
    ]);
31
3
    let end_clause = Expr::List(vec![
32
3
        Expr::List(vec![
33
3
            Expr::Symbol("=".into()),
34
3
            Expr::Symbol("i".into()),
35
3
            Expr::Symbol("N".into()),
36
3
        ]),
37
3
        Expr::Symbol("result".into()),
38
3
    ]);
39
3
    let body_setf = Expr::List(vec![
40
3
        Expr::Symbol("SETF".into()),
41
3
        Expr::Symbol("result".into()),
42
3
        Expr::List(vec![
43
3
            Expr::Symbol("CONS".into()),
44
3
            cons_car,
45
3
            Expr::Symbol("result".into()),
46
3
        ]),
47
3
    ]);
48
3
    vec![Expr::List(vec![i_var]), end_clause, body_setf]
49
3
}
50

            
51
3
fn symbols_with_runtime_n_and_acc() -> SymbolTable {
52
3
    let mut s = SymbolTable::with_builtins();
53
3
    s.define(Symbol::new("N", SymbolKind::Variable).with_value(Expr::WasmRuntime(WasmType::Ratio)));
54
3
    s.define(Symbol::new("result", SymbolKind::Variable).with_value(Expr::Nil));
55
3
    s
56
3
}
57

            
58
/// Bug parity with `do_form`: the accumulator's CONS car is the loop var
59
/// `i`, an integer DO* var. After ADR-0028 an integer DO var is a runtime
60
/// Index (I32), so codegen emits a `pair<i32>` (i31-boxed car); `do_star_form`'s
61
/// static return type must match. Resolving `i` to its const `0` init would
62
/// type it as Ratio (the numeric-literal cell slot) and make a downstream
63
/// `dolist` / `car` consumer downcast the wrong way and trap at runtime.
64
#[test]
65
1
fn do_star_runtime_result_matches_index_cons_car() {
66
1
    let mut symbols = symbols_with_runtime_n_and_acc();
67
1
    let args = do_with_setf_cons_car(Expr::Symbol("i".into()));
68
1
    let result = do_star_form(&mut symbols, &args).unwrap();
69
1
    assert_eq!(
70
        result,
71
        Expr::WasmRuntime(WasmType::PairRef(PairElement::I32)),
72
        "do* with body `(setf result (cons i result))` where i is an \
73
         integer (Index) DO var must report PairRef(I32)"
74
    );
75
1
}
76

            
77
/// Same harness, Bool car. A bool rides its own `PairElement::Bool` slot
78
/// (shares I32's i31-boxed car but serializes as Nil/Bool, not Number), so
79
/// the accumulator's static type is `PairRef(Bool)`.
80
#[test]
81
1
fn do_star_runtime_result_matches_bool_cons_car() {
82
1
    let mut symbols = symbols_with_runtime_n_and_acc();
83
1
    let args = do_with_setf_cons_car(Expr::Bool(true));
84
1
    let result = do_star_form(&mut symbols, &args).unwrap();
85
1
    assert_eq!(
86
        result,
87
        Expr::WasmRuntime(WasmType::PairRef(PairElement::Bool))
88
    );
89
1
}
90

            
91
/// Same shape via plain `do` — locks in the `do_form` infer_env fix. The CONS
92
/// car is the integer DO var `i` (runtime Index → I32 cell), so the result is
93
/// `PairRef(I32)`, matching what the runtime codegen emits for `(cons i …)`.
94
#[test]
95
1
fn do_runtime_result_matches_index_cons_car() {
96
1
    let mut symbols = symbols_with_runtime_n_and_acc();
97
1
    let args = do_with_setf_cons_car(Expr::Symbol("i".into()));
98
1
    let result = do_form(&mut symbols, &args).unwrap();
99
1
    assert_eq!(
100
        result,
101
        Expr::WasmRuntime(WasmType::PairRef(PairElement::I32))
102
    );
103
1
}
104

            
105
/// A COUNTING `do` whose result form is a plain integer accumulator (not a
106
/// cons-built list) must report `I32`, not `PairRef`. Hard-coding `PairRef`
107
/// mistyped `(= (count …) 0)` as "= expects numeric, got pair". Mirrors codegen
108
/// `compile_do_runtime_for_stack` → `compile_for_stack(n)` (the let-promoted
109
/// integer local → I32).
110
#[test]
111
1
fn do_counting_result_is_index_not_pair() {
112
1
    let mut symbols = SymbolTable::with_builtins();
113
1
    symbols.define(
114
1
        Symbol::new("N", SymbolKind::Variable).with_value(Expr::WasmRuntime(WasmType::Ratio)),
115
    );
116
1
    symbols.define(
117
1
        Symbol::new("n", SymbolKind::Variable).with_value(Expr::Number(Fraction::from_integer(0))),
118
    );
119
1
    let i_var = Expr::List(vec![
120
1
        Expr::Symbol("i".into()),
121
1
        Expr::Number(Fraction::from_integer(0)),
122
1
        Expr::List(vec![
123
1
            Expr::Symbol("+".into()),
124
1
            Expr::Symbol("i".into()),
125
1
            Expr::Number(Fraction::from_integer(1)),
126
1
        ]),
127
1
    ]);
128
1
    let end_clause = Expr::List(vec![
129
1
        Expr::List(vec![
130
1
            Expr::Symbol("=".into()),
131
1
            Expr::Symbol("i".into()),
132
1
            Expr::Symbol("N".into()),
133
1
        ]),
134
1
        Expr::Symbol("n".into()),
135
1
    ]);
136
1
    let body = Expr::List(vec![
137
1
        Expr::Symbol("SETF".into()),
138
1
        Expr::Symbol("n".into()),
139
1
        Expr::List(vec![
140
1
            Expr::Symbol("+".into()),
141
1
            Expr::Symbol("n".into()),
142
1
            Expr::Number(Fraction::from_integer(1)),
143
1
        ]),
144
1
    ]);
145
1
    let args = vec![Expr::List(vec![i_var]), end_clause, body];
146
1
    let result = do_form(&mut symbols, &args).unwrap();
147
1
    assert_eq!(result, Expr::WasmRuntime(WasmType::I32));
148
1
}
149

            
150
/// `infer_wasm_type` drives the do-var local allocation type.
151
/// Each variant must report the wasm type the codegen path emits
152
/// for an init expression of that shape; a disagreement lands a
153
/// local of the wrong type and the wasm fails validation when
154
/// `local.set` checks the operand.
155
#[test]
156
1
fn infer_wasm_type_integer_is_index_fraction_is_scalar() {
157
    // ADR-0028: an integer literal defaults to Index (I32); a fractional
158
    // literal is a dimensionless Scalar (Ratio). Mirrors `classify_stack_type`.
159
1
    let s = SymbolTable::with_builtins();
160
1
    assert_eq!(
161
1
        infer_wasm_type(&Expr::Number(Fraction::from_integer(0)), None, &s),
162
        WasmType::I32
163
    );
164
1
    assert_eq!(
165
1
        infer_wasm_type(&Expr::Number(Fraction::new(1, 2)), None, &s),
166
        WasmType::Ratio
167
    );
168
1
}
169

            
170
#[test]
171
1
fn infer_wasm_type_bool_is_bool() {
172
    // Bool/nil literals lower to `WasmType::Bool` on the stack; a DO loop var
173
    // sized here must match so a later read keeps Nil/Bool fidelity (not
174
    // Number). A nil without a pair step-hint is the falsy `Bool`.
175
1
    let s = SymbolTable::with_builtins();
176
1
    assert_eq!(infer_wasm_type(&Expr::Bool(true), None, &s), WasmType::Bool);
177
1
    assert_eq!(infer_wasm_type(&Expr::Nil, None, &s), WasmType::Bool);
178
1
}
179

            
180
#[test]
181
1
fn infer_wasm_type_string_is_stringref() {
182
1
    let s = SymbolTable::with_builtins();
183
1
    assert_eq!(
184
1
        infer_wasm_type(&Expr::String("hi".into()), None, &s),
185
        WasmType::StringRef,
186
    );
187
1
}
188

            
189
#[test]
190
1
fn infer_wasm_type_wasm_runtime_passes_through() {
191
1
    let s = SymbolTable::with_builtins();
192
1
    assert_eq!(
193
1
        infer_wasm_type(&Expr::WasmRuntime(WasmType::Commodity), None, &s),
194
        WasmType::Commodity,
195
    );
196
1
}
197

            
198
#[test]
199
1
fn infer_wasm_type_nil_with_pair_step_hint_uses_pair() {
200
1
    let mut s = SymbolTable::with_builtins();
201
1
    s.define(
202
1
        Symbol::new("acc", SymbolKind::Variable)
203
1
            .with_value(Expr::WasmRuntime(WasmType::PairRef(PairElement::Ratio))),
204
    );
205
    // Step expr `(cons 1/2 acc)` — car is Number-fraction so the
206
    // pair-element resolves to Ratio.
207
1
    let step = Expr::List(vec![
208
1
        Expr::Symbol("CONS".into()),
209
1
        Expr::Number(Fraction::new(1, 2)),
210
1
        Expr::Symbol("acc".into()),
211
1
    ]);
212
1
    assert_eq!(
213
1
        infer_wasm_type(&Expr::Nil, Some(&step), &s),
214
        WasmType::PairRef(PairElement::Ratio),
215
    );
216
1
}