Lines
86.54 %
Functions
27.54 %
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::{
as_literal_arg, call, compile_expr, compile_for_stack, compile_for_stack_as, eval_value,
format_expr,
};
use crate::error::{Error, Result};
use crate::runtime::SymbolTable;
use super::infer::{chain_pair_element, literal_pair_element};
use super::reverse::emit_reverse_loop;
use crate::compiler::expr::emit_box_cell;
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;
Some(chain_pair_element(elems).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::quoted_list(results))
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>> {
match 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 calls: Vec<Expr> = (0..rows).map(|row| row_call(fn_arg, lists, row)).collect();
// The rows' resolved values decide the chain by the LIST rule — the same
// values the eval mirror `mapped_runtime_element` unifies.
let closure_result = match eval_value(&mut symbols.clone(), fn_arg)?.wasm_type() {
Some(WasmType::Closure(sig)) => Some(ctx.closure_sig(sig).result),
_ => None,
let values = calls
.map(|call| row_value(symbols, call, closure_result))
.collect::<Result<Vec<_>>>()?;
let chain_elem = chain_pair_element(&values).map_err(|_| unpairable_row_error())?;
let mut element_locals: Vec<(u32, PairElement)> = Vec::with_capacity(rows);
for (call, value) in calls.iter().zip(&values) {
let elem = compile_map_row(ctx, emit, symbols, call, value, chain_elem)?;
let local = ctx.alloc_local(elem.as_wasm_type())?;
emit.local_set(local);
element_locals.push((local, elem));
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.
emit_box_cell(ctx, emit, *elem_ty, chain_elem);
emit.local_get(acc_local);
emit.call(ctx.ids.pair_new);
Ok(WasmType::PairRef(chain_elem))
/// A row's value as the chain rule sees it: a closure call yields its
/// signature's result (the eval `call` cannot apply a closure local), any
/// other callable resolves on a clone.
fn row_value(symbols: &SymbolTable, call: &Expr, closure_result: Option<WasmType>) -> Result<Expr> {
match closure_result {
Some(result) => Ok(Expr::WasmRuntime(result)),
None => eval_value(&mut symbols.clone(), call),
fn row_call(fn_arg: &Expr, lists: &[Vec<Expr>], row: usize) -> Expr {
let mut call = vec![Expr::Symbol("FUNCALL".to_string()), fn_arg.clone()];
call.extend(lists.iter().map(|list| as_literal_arg(&list[row])));
Expr::List(call)
/// Compiles one row's call onto the chain: a literal result takes the chain's
/// cell type (its own slot in an `AnyRef` chain); a runtime result must match
/// the element its resolved value predicted.
fn compile_map_row(
call: &Expr,
value: &Expr,
chain_elem: PairElement,
) -> Result<PairElement> {
let cell = match (literal_pair_element(value), chain_elem) {
(Some(slot), PairElement::AnyRef) => Some(slot),
(Some(_), chain) => Some(chain),
(None, _) => None,
let ty = match cell {
Some(elem) => {
compile_for_stack_as(ctx, emit, symbols, call, elem.as_wasm_type())?;
elem.as_wasm_type()
None => compile_for_stack(ctx, emit, symbols, call)?,
let elem = PairElement::from_wasm_type(ty).ok_or_else(unpairable_row_error)?;
if chain_elem != PairElement::AnyRef && elem != chain_elem {
return Err(Error::Compile(format!(
"MAP: row result {elem} disagrees with the mapped element {chain_elem}"
)));
Ok(elem)
fn unpairable_row_error() -> Error {
Error::Compile(
"MAP: closure result type can't ride a typed pair; flatten via let-bind first".to_string(),
)
/// 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(|| {
Error::Compile(format!(
"MAP: closure result type {mapped_ty} can't ride a typed pair; \
flatten via let-bind first"
))
})?;
if actual_elem != result_elem {
"MAP: closure result element {actual_elem} doesn't match input element {result_elem}; \
heterogeneous mapping isn't supported yet"
emit_box_cell(ctx, emit, actual_elem, result_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))
/// Emit a per-iteration call of `fn_arg` against the value held in
/// `car_local`. A quoted function name (`'g2`) designates that function, so it
/// is called by name; any other callable rides FUNCALL.
fn compile_map_call_with_local(
car_local: u32,
let car_expr = Expr::WasmLocal(car_local, elem.as_wasm_type());
let call = match fn_arg {
Expr::Quote(designator) if matches!(designator.as_ref(), Expr::Symbol(_)) => {
vec![designator.as_ref().clone(), car_expr]
_ => vec![
Expr::Symbol("FUNCALL".to_string()),
fn_arg.clone(),
car_expr,
],
compile_for_stack(ctx, emit, symbols, &Expr::List(call))