Lines
96.86 %
Functions
28.89 %
Branches
100 %
//! `CONS` constructor. Eval path constant-folds chains; compile path
//! pushes typed car/cdr onto the stack and calls `pair_new`.
//! `push_pair_car` boxes i32 values via `ref.i31`; `push_pair_cdr`
//! emits `ref.null pair` for terminal `nil` or validates a typed
//! `PairRef(elem)` matches the declared element.
use crate::ast::{Expr, PairElement, WasmType};
use crate::compiler::context::CompileContext;
use crate::compiler::emit::FunctionEmitter;
use crate::compiler::expr::{
compile_expr, compile_for_effect, compile_for_stack, compile_for_stack_as, eval_value,
serialize_stack_to_output,
};
use crate::error::{Error, Result};
use crate::runtime::SymbolTable;
use super::datum::{compile_folded_to_stack, is_datum_result};
use super::infer::infer_pair_element;
pub(super) fn cons(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
if args.len() != 2 {
return Err(Error::Arity {
name: "CONS".to_string(),
expected: 2,
actual: args.len(),
});
}
let car = eval_value(symbols, &args[0])?;
let cdr = eval_value(symbols, &args[1])?;
if car.is_wasm_runtime()
|| cdr.is_wasm_runtime()
|| matches!(cdr.wasm_type(), Some(WasmType::PairRef(_)))
{
let elem = infer_pair_element(&car, &cdr)?;
return Ok(Expr::WasmRuntime(WasmType::PairRef(elem)));
match cdr {
Expr::Quote(inner) => match *inner {
Expr::List(mut elems) => {
elems.insert(0, car);
Ok(Expr::Quote(Box::new(Expr::List(elems))))
Expr::Nil => Ok(Expr::Quote(Box::new(Expr::List(vec![car])))),
other => Ok(Expr::Quote(Box::new(Expr::cons(car, other)))),
},
pub(super) fn compile_cons(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
args: &[Expr],
) -> Result<()> {
let result = cons(symbols, args)?;
if matches!(result.wasm_type(), Some(WasmType::PairRef(_))) {
let ty = compile_cons_to_stack(ctx, emit, symbols, args)?;
serialize_stack_to_output(ctx, emit, ty)?;
return Ok(());
compile_expr(ctx, emit, symbols, &result)
pub(super) fn compile_cons_to_stack(
) -> Result<WasmType> {
// A fully-constant cons folds to a quoted list datum (e.g. (cons 0 '(1 2 3))
// → '(0 1 2 3)); render it as a datum so value position agrees with the
// effect path instead of trapping when push_pair_cdr meets the quoted cdr.
// Only a runtime car/cdr (PairRef result) takes the pair_new path below.
let folded = cons(symbols, args)?;
if !matches!(folded.wasm_type(), Some(WasmType::PairRef(_))) && is_datum_result(&folded) {
return compile_folded_to_stack(ctx, emit, symbols, folded);
// If an argument transfers control before the pair is built (a
// `(return-from …)` / `(error …)` — args evaluate left-to-right before the
// call), the rest is dead: the pair is never constructed, so a non-list cdr
// must NOT be rejected. Emit the live prefix (which performs the exit) and
// return a placeholder element. Classify divergence on a CLONE (the exit is
// still recorded by the single emit below).
if crate::compiler::special::form_diverges_for_test(&mut symbols.clone(), &args[0])? {
let ty = compile_for_stack(ctx, emit, symbols, &args[0])?;
return Ok(WasmType::PairRef(
PairElement::from_wasm_type(ty).unwrap_or(PairElement::AnyRef),
));
if crate::compiler::special::form_diverges_for_test(&mut symbols.clone(), &args[1])? {
// Car is evaluated (for its effects) then the cdr exits before
// `pair_new`; the car value is dead, so compile it for effect.
compile_for_effect(ctx, emit, symbols, &args[0])?;
let ty = compile_for_stack(ctx, emit, symbols, &args[1])?;
// Decide the element type up front via the eval pipeline so we can
// reject heterogeneous mixing before emitting any wasm.
let car_resolved = eval_value(symbols, &args[0])?;
let cdr_resolved = eval_value(symbols, &args[1])?;
let elem = infer_pair_element(&car_resolved, &cdr_resolved)?;
push_pair_car(ctx, emit, symbols, &args[0], elem)?;
push_pair_cdr(ctx, emit, symbols, &args[1], elem, &cdr_resolved)?;
emit.call(ctx.ids.pair_new);
Ok(WasmType::PairRef(elem))
/// Emit `elem_args` as a nul-terminated `$pair` chain. LIST uses this so it
/// never synthesizes a `(CONS …)` form — that would route through symbol
/// dispatch and could hit a user `(defun cons …)` shadow. `elem_args` are the
/// ORIGINAL element expressions (emitted once via `push_pair_car`); the
/// chain's element type is decided up front by folding their resolved values
/// through the eval `cons` builder (same pairwise widening CONS does), so
/// every cell is emitted homogeneously.
pub(super) fn compile_pair_chain(
elem_args: &[Expr],
let resolved: Vec<Expr> = elem_args
.iter()
.map(|a| eval_value(symbols, a))
.collect::<Result<_>>()?;
let elem = match fold_chain_value(symbols, &resolved)?.wasm_type() {
Some(WasmType::PairRef(elem)) => elem,
_ => PairElement::AnyRef,
emit_chain_cells(ctx, emit, symbols, elem_args, elem)?;
/// Fold resolved `elems` right-to-left through the eval `cons` builder to get
/// the chain's unified `PairRef(elem)` placeholder (or `Nil` when empty).
fn fold_chain_value(symbols: &mut SymbolTable, elems: &[Expr]) -> Result<Expr> {
let mut chain = Expr::Nil;
for elem in elems.iter().rev() {
chain = cons(symbols, &[elem.clone(), chain])?;
Ok(chain)
/// Emit the nested `pair_new` cells for `elem_args` (original expressions),
/// every car at the unified `elem` slot. Innermost (`nil`) cdr first via
/// recursion, so the wasm stack order per cell is `[car, cdr_ref]` → `pair_new`.
fn emit_chain_cells(
elem: PairElement,
let Some((car, rest)) = elem_args.split_first() else {
emit.ref_null(ctx.ids.ty_pair);
push_pair_car(ctx, emit, symbols, car, elem)?;
emit_chain_cells(ctx, emit, symbols, rest, elem)?;
Ok(())
fn push_pair_car(
arg: &Expr,
if elem == PairElement::AnyRef {
// Heterogeneous cell: widen any wasm type to the anyref car. The
// i31-boxed value types (I32 / Bool) need `ref.i31`; reference-typed
// values are anyref subtypes already.
let actual = compile_for_stack(ctx, emit, symbols, arg)?;
if matches!(actual, WasmType::I32 | WasmType::Bool) {
emit.ref_i31();
match elem {
// Value cells: a pair cell is NOT the Index stratum (CLAUDE.md), so a
// dimension-flexible integer literal in a Ratio cell must coerce to
// Ratio rather than lower as I32 and mismatch the slot. `nil` in these
// cells lands on a real zero (`0` / `#f` / `0/1`), never a trapping
// null — `as_wasm_type` is i31-boxed (I32/Bool) or a non-null ratio.
PairElement::I32 | PairElement::Bool | PairElement::Ratio => {
compile_for_stack_as(ctx, emit, symbols, arg, elem.as_wasm_type())?;
if matches!(elem, PairElement::I32 | PairElement::Bool) {
// Reference cells (string / entity / commodity): keep the strict match
// so a `nil` or wrong-typed car stays a COMPILE error rather than a
// typed null that `CAR`'s non-null cast would trap on at runtime.
_ => {
if PairElement::from_wasm_type(actual) != Some(elem) {
return Err(Error::Compile(format!(
"CONS car: expected {elem} to match the inferred pair element, got {actual}"
)));
fn push_pair_cdr(
resolved: &Expr,
if matches!(resolved, Expr::Nil) {
match actual {
WasmType::PairRef(actual_elem) if actual_elem == elem => Ok(()),
// The widened-AnyRef case accepts any typed pair as the cdr —
// `$pair` already carries `anyref` cars, so no per-element
// adjustment is needed once we've decided to ride the
// heterogeneous variant.
WasmType::PairRef(_) if elem == PairElement::AnyRef => Ok(()),
WasmType::PairRef(actual_elem) => Err(Error::Compile(format!(
"CONS cdr element type mismatch — expected pair<{elem}>, got pair<{actual_elem}>"
))),
other => Err(Error::Compile(format!(
"CONS cdr must be a pair or nil, got {other}"