Lines
88.61 %
Functions
15.38 %
Branches
100 %
//! `APPEND` — concatenate lists left to right. Constant-folds when every
//! argument is a compile-time list; otherwise lowers the two-list runtime
//! case by reversing the prefix onto an accumulator seeded with the
//! suffix chain (so the suffix is shared, not copied).
use crate::ast::{Expr, PairElement, WasmType};
use crate::compiler::context::CompileContext;
use crate::compiler::emit::FunctionEmitter;
use crate::compiler::expr::{
compile_expr, compile_for_stack, eval_value, format_expr, 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::map::extract_list_elements;
pub(super) fn append(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
if args.iter().any(|a| {
eval_value(symbols, a)
.map(|r| matches!(r.wasm_type(), Some(WasmType::PairRef(_))))
.unwrap_or(false)
}) {
return Ok(Expr::WasmRuntime(WasmType::PairRef(result_element(
symbols, args,
)?)));
}
let mut out = Vec::new();
for arg in args {
let resolved = eval_value(symbols, arg)?;
out.extend(extract_list_elements(&resolved).map_err(|_| {
Error::Compile(format!(
"APPEND expects lists, got {}",
format_expr(&resolved)
))
})?);
Ok(Expr::Quote(Box::new(Expr::List(out))))
/// The element type of a runtime APPEND result: the widening of every
/// argument's element type across both runtime `PairRef` chains and constant
/// lists. A constant list's element is the widening of its members' literal
/// element types (`AnyRef` if any member isn't a plain literal). An empty
/// constant list / nil doesn't constrain the result.
fn result_element(symbols: &mut SymbolTable, args: &[Expr]) -> Result<PairElement> {
let mut elem: Option<PairElement> = None;
let mut widen = |e: PairElement| {
elem = Some(match elem {
Some(prev) => prev.widen(e),
None => e,
});
match resolved.wasm_type() {
Some(WasmType::PairRef(e)) => widen(e),
_ => {
for member in extract_list_elements(&resolved).map_err(|_| {
})? {
widen(
super::infer::literal_pair_element(&member).unwrap_or(PairElement::AnyRef),
);
// All-empty / all-nil args: the result is an empty chain; element type is
// irrelevant (no cells), so any concrete slot works.
Ok(elem.unwrap_or(PairElement::AnyRef))
pub(super) fn compile_append(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
args: &[Expr],
) -> Result<()> {
let folded = append(symbols, args)?;
if folded.is_wasm_runtime() {
let ty = compile_append_to_stack(ctx, emit, symbols, args)?;
return serialize_stack_to_output(ctx, emit, ty);
compile_expr(ctx, emit, symbols, &folded)
pub(super) fn compile_append_to_stack(
) -> Result<WasmType> {
// All-constant APPEND folds to a single quoted list — render it as a datum
// (matching CAR/CDR/REVERSE/CONS and the effect path) instead of forcing it
// through the runtime materialization below, which can't represent symbols.
if !folded.is_wasm_runtime() && is_datum_result(&folded) {
return compile_folded_to_stack(ctx, emit, symbols, folded);
if args.len() != 2 {
return Err(Error::Compile(
"APPEND with a runtime list argument requires exactly 2 lists".to_string(),
));
let elem = result_element(symbols, args)?;
let pair_idx = ctx.ids.ty_pair;
let prefix_local = ctx.alloc_local(WasmType::PairRef(elem))?;
let acc_local = ctx.alloc_local(WasmType::PairRef(elem))?;
// acc ← suffix (shared tail); then prepend the prefix in reverse so
// the final chain is prefix ++ suffix in original order.
push_list_arg(ctx, emit, symbols, &args[1])?;
emit.local_set(acc_local);
push_list_arg(ctx, emit, symbols, &args[0])?;
emit.local_set(prefix_local);
let reversed_local = ctx.alloc_local(WasmType::PairRef(elem))?;
reverse_into(ctx, emit, pair_idx, prefix_local, reversed_local);
// Walk the reversed prefix, prepending each car onto acc (= suffix).
emit.block_start();
emit.loop_start();
emit.local_get(reversed_local);
emit.ref_is_null();
emit.br_if(1);
emit.struct_get(pair_idx, 0);
emit.local_get(acc_local);
emit.call(ctx.ids.pair_new);
emit.struct_get(pair_idx, 1);
emit.local_set(reversed_local);
emit.br(0);
emit.block_end();
Ok(WasmType::PairRef(elem))
/// Push an APPEND argument as a runtime `$pair` chain on the stack. A runtime
/// `PairRef` arg lowers directly; a constant list is materialized into a fresh
/// `$pair` chain via the cons builder; nil / empty list becomes a null pair.
/// The runtime `$pair` struct is monomorphic (anyref car), so a materialized
/// constant chain and a runtime chain are the same wasm type — only the
/// compile-time element label (computed by `result_element`) differs.
fn push_list_arg(
arg: &Expr,
if matches!(resolved.wasm_type(), Some(WasmType::PairRef(_))) {
compile_for_stack(ctx, emit, symbols, arg)?;
return Ok(());
let elements = extract_list_elements(&resolved).map_err(|_| {
})?;
if elements.is_empty() {
emit.ref_null(ctx.ids.ty_pair);
// The extracted members are already the list's DATA, not expressions to
// evaluate. Self-evaluating literals (number / string / bool / nil) pass
// through as-is; a member that the cons builder would otherwise RESOLVE as
// a reference (a bare `Symbol("a")` → variable lookup → "Undefined symbol",
// or a nested list → a call) is quoted so it stays literal. A quoted datum
// with no runtime representation (e.g. a symbol) then surfaces the standard
// "cannot compile to WASM stack value" error rather than a misleading
// undefined-symbol one. Full symbol-list support is a separate slice.
let members: Vec<Expr> = elements
.into_iter()
.map(|e| match e {
Expr::Number(_) | Expr::String(_) | Expr::Bool(_) | Expr::Nil => e,
other => Expr::Quote(Box::new(other)),
})
.collect();
super::cons::compile_pair_chain(ctx, emit, symbols, &members)?;
Ok(())
/// Reverses the chain in `src_local` into `dst_local` (a fresh null-seeded
/// accumulator), consuming `src_local`. Used so APPEND can prepend the
/// prefix onto the shared suffix in original order.
fn reverse_into(
pair_idx: u32,
src_local: u32,
dst_local: u32,
) {
emit.ref_null(pair_idx);
emit.local_set(dst_local);
emit.local_get(src_local);
emit.local_get(dst_local);
emit.local_set(src_local);