Lines
95.31 %
Functions
33.33 %
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 (indices range-checked by
//! `box_index`); `push_pair_cdr` emits `ref.null pair` for terminal `nil`,
//! materializes a constant list, 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, emit_box_anyref,
emit_box_cell, emit_relabel_chain, 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::{chain_pair_element, infer_pair_element, is_scalar_literal};
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::quoted_list(elems))
Expr::Nil => Ok(Expr::quoted_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 from their resolved values by
/// [`chain_pair_element`] — the same rule the eval `list` reports — 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 = chain_pair_element(&resolved)?;
emit_pair_chain(ctx, emit, symbols, elem_args, elem)?;
/// Materialize a constant list's `elements` (DATA, not expressions) as a
/// `$pair` chain whose cells ride `elem`, the element its consumer declared.
/// A member the cell push would otherwise RESOLVE (a bare symbol → variable
/// lookup, a nested list → a call) is quoted so it stays literal; one with no
/// runtime representation then surfaces "cannot compile to WASM stack value".
pub(super) fn emit_constant_chain(
elements: Vec<Expr>,
elem: PairElement,
let members: Vec<Expr> = elements
.into_iter()
.map(|e| match e {
Expr::Number(_) | Expr::String(_) | Expr::T | Expr::Nil => e,
other => Expr::Quote(Box::new(other)),
})
.collect();
emit_pair_chain(ctx, emit, symbols, &members, elem)
/// 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_pair_chain(
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_pair_chain(ctx, emit, symbols, rest, elem)?;
Ok(())
fn push_pair_car(
arg: &Expr,
let resolved = eval_value(&mut symbols.clone(), arg)?;
if crate::compiler::expr::is_quoted_datum(&resolved) {
return Err(Error::Compile(format!(
"cannot compile to WASM stack value: {} has no list cell representation",
crate::compiler::expr::format_expr(&resolved)
)));
match elem {
PairElement::AnyRef => push_anyref_car(ctx, emit, symbols, arg),
// Value cells: a dimension-flexible integer literal coerces to the
// cell's stratum (Index or Scalar) rather than mismatching the slot.
PairElement::I32 | PairElement::Bool | PairElement::Ratio => {
compile_for_stack_as(ctx, emit, symbols, arg, elem.as_wasm_type())?;
emit_box_cell(ctx, emit, elem, elem);
// 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.
_ => {
let actual = compile_for_stack(ctx, emit, symbols, arg)?;
if PairElement::from_wasm_type(actual) != Some(elem) {
"CONS car: expected {elem} to match the inferred pair element, got {actual}"
/// A heterogeneous cell: `nil` rides as a null ref (never an i31 zero), a
/// scalar literal as a ratio (never a truncated index), anything else as its
/// own value boxed to anyref.
fn push_anyref_car(
match eval_value(&mut symbols.clone(), arg)? {
Expr::Nil => {
if !matches!(arg, Expr::Nil) {
compile_for_effect(ctx, emit, symbols, arg)?;
emit.ref_null_any();
resolved if is_scalar_literal(&resolved) => {
compile_for_stack_as(ctx, emit, symbols, arg, WasmType::Ratio)?;
emit_box_anyref(ctx, emit, actual);
fn push_pair_cdr(
resolved: &Expr,
if matches!(resolved, Expr::Nil) {
if let Expr::Quote(inner) = resolved
&& let Expr::List(members) = inner.as_ref()
if !matches!(arg, Expr::Quote(_)) {
return emit_constant_chain(ctx, emit, symbols, members.clone(), elem);
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(actual_elem) if elem == PairElement::AnyRef => {
emit_relabel_chain(ctx, emit, actual_elem, elem);
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}"