Lines
100 %
Functions
Branches
//! Cross-module tests for the iteration family. Span `do_loop` and
//! `common` because the form-result-type inference is exercised end-
//! to-end via `do_form` / `do_star_form`. Keeping the tests in a
//! sibling module instead of either sub-module avoids re-exporting
//! the private inference helpers just to test them.
use crate::ast::{Expr, Fraction, PairElement, WasmType};
use crate::runtime::{Symbol, SymbolKind, SymbolTable};
use super::common::infer_wasm_type;
use super::do_form::do_form;
use super::do_star_form::do_star_form;
/// Constructs `(do ((i 0 (+ i 1))) ((= i N) result) (setf result
/// (cons cons_car result)))` — the canonical tag-sync pattern.
/// `N` is a runtime symbol forcing the runtime path; `result` is a
/// pre-bound accumulator that the body builds via setf. The
/// caller varies `cons_car` to exercise the static PairElement
/// inference that the codegen path actually emits for the
/// accumulator.
fn do_with_setf_cons_car(cons_car: Expr) -> Vec<Expr> {
let i_var = Expr::List(vec![
Expr::Symbol("i".into()),
Expr::Number(Fraction::from_integer(0)),
Expr::List(vec![
Expr::Symbol("+".into()),
Expr::Number(Fraction::from_integer(1)),
]),
]);
let end_clause = Expr::List(vec![
Expr::Symbol("=".into()),
Expr::Symbol("N".into()),
Expr::Symbol("result".into()),
let body_setf = Expr::List(vec![
Expr::Symbol("SETF".into()),
Expr::Symbol("CONS".into()),
cons_car,
vec![Expr::List(vec![i_var]), end_clause, body_setf]
}
fn symbols_with_runtime_n_and_acc() -> SymbolTable {
let mut s = SymbolTable::with_builtins();
s.define(Symbol::new("N", SymbolKind::Variable).with_value(Expr::WasmRuntime(WasmType::Ratio)));
s.define(Symbol::new("result", SymbolKind::Variable).with_value(Expr::Nil));
s
/// Bug parity with `do_form`: the accumulator's CONS car is the loop var
/// `i`, an integer DO* var. After ADR-0028 an integer DO var is a runtime
/// Index (I32), so codegen emits a `pair<i32>` (i31-boxed car); `do_star_form`'s
/// static return type must match. Resolving `i` to its const `0` init would
/// type it as Ratio (the numeric-literal cell slot) and make a downstream
/// `dolist` / `car` consumer downcast the wrong way and trap at runtime.
#[test]
fn do_star_runtime_result_matches_index_cons_car() {
let mut symbols = symbols_with_runtime_n_and_acc();
let args = do_with_setf_cons_car(Expr::Symbol("i".into()));
let result = do_star_form(&mut symbols, &args).unwrap();
assert_eq!(
result,
Expr::WasmRuntime(WasmType::PairRef(PairElement::I32)),
"do* with body `(setf result (cons i result))` where i is an \
integer (Index) DO var must report PairRef(I32)"
);
/// Same harness, Bool car. A bool rides its own `PairElement::Bool` slot
/// (shares I32's i31-boxed car but serializes as Nil/Bool, not Number), so
/// the accumulator's static type is `PairRef(Bool)`.
fn do_star_runtime_result_matches_bool_cons_car() {
let args = do_with_setf_cons_car(Expr::Bool(true));
Expr::WasmRuntime(WasmType::PairRef(PairElement::Bool))
/// Same shape via plain `do` — locks in the `do_form` infer_env fix. The CONS
/// car is the integer DO var `i` (runtime Index → I32 cell), so the result is
/// `PairRef(I32)`, matching what the runtime codegen emits for `(cons i …)`.
fn do_runtime_result_matches_index_cons_car() {
let result = do_form(&mut symbols, &args).unwrap();
Expr::WasmRuntime(WasmType::PairRef(PairElement::I32))
/// A COUNTING `do` whose result form is a plain integer accumulator (not a
/// cons-built list) must report `I32`, not `PairRef`. Hard-coding `PairRef`
/// mistyped `(= (count …) 0)` as "= expects numeric, got pair". Mirrors codegen
/// `compile_do_runtime_for_stack` → `compile_for_stack(n)` (the let-promoted
/// integer local → I32).
fn do_counting_result_is_index_not_pair() {
let mut symbols = SymbolTable::with_builtins();
symbols.define(
Symbol::new("N", SymbolKind::Variable).with_value(Expr::WasmRuntime(WasmType::Ratio)),
Symbol::new("n", SymbolKind::Variable).with_value(Expr::Number(Fraction::from_integer(0))),
Expr::Symbol("n".into()),
let body = Expr::List(vec![
let args = vec![Expr::List(vec![i_var]), end_clause, body];
assert_eq!(result, Expr::WasmRuntime(WasmType::I32));
/// `infer_wasm_type` drives the do-var local allocation type.
/// Each variant must report the wasm type the codegen path emits
/// for an init expression of that shape; a disagreement lands a
/// local of the wrong type and the wasm fails validation when
/// `local.set` checks the operand.
fn infer_wasm_type_integer_is_index_fraction_is_scalar() {
// ADR-0028: an integer literal defaults to Index (I32); a fractional
// literal is a dimensionless Scalar (Ratio). Mirrors `classify_stack_type`.
let s = SymbolTable::with_builtins();
infer_wasm_type(&Expr::Number(Fraction::from_integer(0)), None, &s),
WasmType::I32
infer_wasm_type(&Expr::Number(Fraction::new(1, 2)), None, &s),
WasmType::Ratio
fn infer_wasm_type_bool_is_bool() {
// Bool/nil literals lower to `WasmType::Bool` on the stack; a DO loop var
// sized here must match so a later read keeps Nil/Bool fidelity (not
// Number). A nil without a pair step-hint is the falsy `Bool`.
assert_eq!(infer_wasm_type(&Expr::Bool(true), None, &s), WasmType::Bool);
assert_eq!(infer_wasm_type(&Expr::Nil, None, &s), WasmType::Bool);
fn infer_wasm_type_string_is_stringref() {
infer_wasm_type(&Expr::String("hi".into()), None, &s),
WasmType::StringRef,
fn infer_wasm_type_wasm_runtime_passes_through() {
infer_wasm_type(&Expr::WasmRuntime(WasmType::Commodity), None, &s),
WasmType::Commodity,
fn infer_wasm_type_nil_with_pair_step_hint_uses_pair() {
s.define(
Symbol::new("acc", SymbolKind::Variable)
.with_value(Expr::WasmRuntime(WasmType::PairRef(PairElement::Ratio))),
// Step expr `(cons 1/2 acc)` — car is Number-fraction so the
// pair-element resolves to Ratio.
let step = Expr::List(vec![
Expr::Number(Fraction::new(1, 2)),
Expr::Symbol("acc".into()),
infer_wasm_type(&Expr::Nil, Some(&step), &s),
WasmType::PairRef(PairElement::Ratio),