Lines
84.82 %
Functions
16.19 %
Branches
100 %
//! `MAP` eval + compile.
//!
//! Three paths:
//! - **Constant-fold**: when the function and every list arg resolves
//! at compile time, `map_fn` walks element-by-element via the eval
//! `call` pipeline and returns the resulting list literal.
//! - **Runtime closure, literal list**: emit a per-element FUNCALL,
//! stash each result in a fresh local, then walk the locals in
//! reverse and prepend each car onto the accumulator via `pair_new`.
//! - **Runtime closure or runtime `PairRef` list**: walk the input
//! chain at runtime, calling the function on each car, prepend onto
//! a reversed accumulator, and run `emit_reverse_loop` once at the
//! end so the output preserves input order.
use crate::ast::{Expr, PairElement, WasmType};
use crate::compiler::context::CompileContext;
use crate::compiler::emit::FunctionEmitter;
use crate::compiler::expr::{call, compile_expr, compile_for_stack, eval_value, format_expr};
use crate::error::{Error, Result};
use crate::runtime::SymbolTable;
use super::reverse::emit_reverse_loop;
pub(super) fn map_fn(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
if args.len() < 2 {
return Err(Error::Arity {
name: "MAP".to_string(),
expected: 2,
actual: args.len(),
});
}
let fn_resolved = eval_value(symbols, &args[0])?;
if let Some(WasmType::Closure(sig)) = fn_resolved.wasm_type() {
// MAP's OUTPUT element is the closure's RESULT type (it transforms each
// element) — NOT the input element. The closure-emit site records the
// result so this eval prediction agrees with codegen (`compile_map_*`
// uses the per-row closure result). Fall back to the input element only
// when the result isn't recorded or isn't a pair-cell type (e.g. a
// nested-pair result, which codegen rejects anyway).
let elem = symbols
.closure_result(sig)
.and_then(PairElement::from_wasm_type)
.or(runtime_pair_input_element(symbols, &args[1..])?)
.unwrap_or(PairElement::Ratio);
return Ok(Expr::WasmRuntime(WasmType::PairRef(elem)));
if list_args_have_runtime_pair(symbols, &args[1..])? {
let elem = runtime_pair_input_element(symbols, &args[1..])?
.ok_or_else(|| Error::Compile("MAP: runtime list element type unknown".to_string()))?;
// Static fn + constant list(s): try to fully reduce. If the body produces
// a runtime value per element (a side-effecting native, or a runtime
// result), MAP ISN'T constant-foldable — surface a runtime placeholder
// typed by the body's per-element result so the compile path lowers each
// element call at runtime instead of baking placeholders into a list.
let folded = constant_fold_map(symbols, &args[0], &args[1..])?;
if let Some(elem) = mapped_runtime_element(&folded) {
Ok(folded)
/// If a folded MAP result is a list whose elements are runtime placeholders
/// (the body produced runtime values), returns the unified `PairElement` of
/// those results — the signal that MAP must lower at runtime. `None` when the
/// fold produced a genuine constant list.
fn mapped_runtime_element(folded: &Expr) -> Option<PairElement> {
let elems = match folded {
Expr::Quote(inner) => match inner.as_ref() {
Expr::List(elems) => elems,
_ => return None,
},
};
if !elems.iter().any(Expr::is_wasm_runtime) {
return None;
let mut elem: Option<PairElement> = None;
for e in elems {
let pe = e
.wasm_type()
.or_else(|| super::infer::literal_pair_element(e))
.unwrap_or(PairElement::AnyRef);
elem = Some(match elem {
Some(prev) => prev.widen(pe),
None => pe,
Some(elem.unwrap_or(PairElement::AnyRef))
fn constant_fold_map(
symbols: &mut SymbolTable,
function_arg: &Expr,
list_args: &[Expr],
) -> Result<Expr> {
let lists: Vec<Vec<Expr>> = list_args
.iter()
.map(|arg| {
let resolved = eval_value(symbols, arg)?;
extract_list_elements(&resolved)
})
.collect::<Result<_>>()?;
let min_len = lists.iter().map(std::vec::Vec::len).min().unwrap_or(0);
let mut results = Vec::with_capacity(min_len);
for i in 0..min_len {
let mut call_args = vec![function_arg.clone()];
for list in &lists {
call_args.push(as_literal_arg(&list[i]));
let result = call(symbols, &call_args)?;
let resolved_result = eval_value(symbols, &result)?;
results.push(resolved_result);
Ok(Expr::Quote(Box::new(Expr::List(results))))
fn as_literal_arg(expr: &Expr) -> Expr {
match expr {
Expr::Symbol(_) | Expr::List(_) | Expr::Cons(_, _) => Expr::Quote(Box::new(expr.clone())),
_ => expr.clone(),
fn list_args_have_runtime_pair(symbols: &mut SymbolTable, list_args: &[Expr]) -> Result<bool> {
for arg in list_args {
if matches!(resolved.wasm_type(), Some(WasmType::PairRef(_))) {
return Ok(true);
Ok(false)
fn runtime_pair_input_element(
) -> Result<Option<PairElement>> {
if let Some(WasmType::PairRef(elem)) = resolved.wasm_type() {
return Ok(Some(elem));
Ok(None)
pub(super) fn extract_list_elements(expr: &Expr) -> Result<Vec<Expr>> {
Expr::List(elems) => Ok(elems.clone()),
Expr::Nil => Ok(vec![]),
other => Err(Error::Compile(format!(
"MAP expects list arguments, got quoted {}",
format_expr(other)
))),
"MAP expects list arguments, got {}",
pub(super) fn compile_map(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
args: &[Expr],
) -> Result<()> {
let runtime_closure = matches!(fn_resolved.wasm_type(), Some(WasmType::Closure(_)));
let runtime_list = list_args_have_runtime_pair(symbols, &args[1..])?;
if runtime_closure || runtime_list {
let ty = compile_map_to_stack(ctx, emit, symbols, args)?;
return crate::compiler::expr::serialize_stack_to_output(ctx, emit, ty);
let result = constant_fold_map(symbols, &args[0], &args[1..])?;
if mapped_runtime_element(&result).is_some() {
// Body isn't constant-foldable — lower the static fn + constant list
// element-by-element via the runtime path.
compile_expr(ctx, emit, symbols, &result)
pub(super) fn compile_map_to_stack(
) -> Result<WasmType> {
return Err(Error::Compile(
"MAP: stack-position lowering requires a function and at least one list".to_string(),
));
// Single runtime `PairRef` list: walk the chain at runtime.
if args.len() == 2
&& let Some(WasmType::PairRef(elem)) = eval_value(symbols, &args[1])?.wasm_type()
{
return compile_map_runtime_list(ctx, emit, symbols, &args[0], &args[1], elem);
// One or more constant lists with a callable whose body isn't constant-
// foldable (runtime/side-effecting). Zip the constant lists and apply the
// function per row via FUNCALL — serves a closure OR a bare lambda, and any
// arity of lists. (A multi-list MAP over RUNTIME pair chains is not
// supported here — only the single-list runtime chain is.)
let lists: Vec<Vec<Expr>> = args[1..]
"MAP: a multi-list mapping requires constant lists; a runtime list \
is only supported as the sole list argument"
.to_string(),
compile_map_literal(ctx, emit, symbols, &args[0], &lists)
/// Lower `(map <fn> '(a0 a1 ...) '(b0 b1 ...) ...)` row-by-row over one or more
/// constant lists (zipped to the shortest). Each per-row call rides FUNCALL (→
/// `call_ref` for a closure, inline for a bare lambda); results land in fresh
/// per-row locals. We then walk the locals in reverse and prepend each car onto
/// the accumulator via `pair_new` so the output keeps input order without an
/// extra reverse pass.
fn compile_map_literal(
fn_arg: &Expr,
lists: &[Vec<Expr>],
let rows = lists.iter().map(Vec::len).min().unwrap_or(0);
if rows == 0 {
emit.ref_null(ctx.ids.ty_pair);
return Ok(WasmType::PairRef(PairElement::Ratio));
let mut element_locals: Vec<(u32, PairElement)> = Vec::with_capacity(rows);
let mut shared_elem: Option<PairElement> = None;
for row in 0..rows {
let mut call = vec![Expr::Symbol("FUNCALL".to_string()), fn_arg.clone()];
call.extend(lists.iter().map(|list| as_literal_arg(&list[row])));
let funcall = Expr::List(call);
let ty = compile_for_stack(ctx, emit, symbols, &funcall)?;
let elem = PairElement::from_wasm_type(ty).ok_or_else(|| {
Error::Compile(format!(
"MAP: closure result type {ty} can't ride a typed pair; \
flatten via let-bind first"
))
})?;
// Heterogeneous per-element results widen the chain to `AnyRef` (the
// ADR-0025 escape hatch) instead of erroring — each car is boxed to
// anyref at prepend. Matches the eval-side `mapped_runtime_element`.
shared_elem = Some(match shared_elem {
Some(prev) => prev.widen(elem),
None => elem,
let local = ctx.alloc_local(elem.as_wasm_type())?;
emit.local_set(local);
element_locals.push((local, elem));
let Some(chain_elem) = shared_elem else {
"MAP: empty literal-list closure mapping reached prepend phase".to_string(),
let pair_idx = ctx.ids.ty_pair;
let acc_local = ctx.alloc_local(WasmType::PairRef(chain_elem))?;
emit.ref_null(pair_idx);
emit.local_set(acc_local);
for (local, elem_ty) in element_locals.iter().rev() {
emit.local_get(*local);
// Box each car for the CHAIN's element slot: an AnyRef chain needs
// every car widened to anyref (i31 for i32/bool); a homogeneous chain
// boxes per the element's own type.
box_for_pair_car(emit, chain_car_box(chain_elem, *elem_ty));
emit.local_get(acc_local);
emit.call(ctx.ids.pair_new);
Ok(WasmType::PairRef(chain_elem))
/// The `PairElement` to box a car AS when prepending into a chain whose slot is
/// `chain_elem`. For an `AnyRef` chain, an i32/bool car must still be i31-boxed
/// (so `box_for_pair_car` sees its own value type); ref-typed cars are anyref
/// subtypes already. For a homogeneous chain the car boxes per the chain type.
fn chain_car_box(chain_elem: PairElement, car_elem: PairElement) -> PairElement {
match chain_elem {
PairElement::AnyRef => car_elem,
other => other,
/// Walks a runtime `PairRef(elem)` input. Builds the result reversed
/// (one prepend per car) by calling the function on each car, then
/// reverses once at the end so the output preserves input order.
fn compile_map_runtime_list(
list_expr: &Expr,
elem: PairElement,
let result_elem = elem;
let pair_local = ctx.alloc_local(WasmType::PairRef(result_elem))?;
let acc_local = ctx.alloc_local(WasmType::PairRef(result_elem))?;
let car_local = ctx.alloc_local(elem.as_wasm_type())?;
compile_for_stack(ctx, emit, symbols, list_expr)?;
emit.local_set(pair_local);
emit.block_start();
emit.loop_start();
emit.local_get(pair_local);
emit.ref_is_null();
emit.br_if(1);
emit.struct_get(pair_idx, 0);
crate::compiler::native::list::emit_pair_car_downcast(ctx, emit, elem);
emit.local_set(car_local);
let mapped_ty = compile_map_call_with_local(ctx, emit, symbols, fn_arg, car_local, elem)?;
let actual_elem = PairElement::from_wasm_type(mapped_ty).ok_or_else(|| {
"MAP: closure result type {mapped_ty} can't ride a typed pair; \
if actual_elem != result_elem {
return Err(Error::Compile(format!(
"MAP: closure result element {actual_elem} doesn't match input element {result_elem}; \
heterogeneous mapping isn't supported yet"
)));
box_for_pair_car(emit, actual_elem);
emit.struct_get(pair_idx, 1);
emit.br(0);
emit.block_end();
// The accumulator is in reverse order — walk it once more through
// the standard reverse loop so the output preserves input order.
let acc_expr = Expr::WasmLocal(acc_local, WasmType::PairRef(result_elem));
emit_reverse_loop(ctx, emit, symbols, &acc_expr, result_elem)?;
Ok(WasmType::PairRef(result_elem))
fn box_for_pair_car(emit: &mut FunctionEmitter, elem: PairElement) {
// I32 and Bool share the i31-boxed car representation.
if matches!(elem, PairElement::I32 | PairElement::Bool) {
emit.ref_i31();
/// Emit a per-iteration call of `fn_arg` against the value held in
/// `car_local`. Mirrors the FUNCALL stack-position emit but inlined so
/// we don't have to allocate a temporary `Expr` for the local.
fn compile_map_call_with_local(
car_local: u32,
let car_expr = Expr::WasmLocal(car_local, elem.as_wasm_type());
let funcall = Expr::List(vec![
Expr::Symbol("FUNCALL".to_string()),
fn_arg.clone(),
car_expr,
]);
compile_for_stack(ctx, emit, symbols, &funcall)