Lines
97.3 %
Functions
30.77 %
Branches
100 %
//! `DOLIST` codegen + eval handler.
//!
//! Two paths: a *runtime* path when the list expression resolves to a
//! `WasmType::PairRef(elem)` (typed pair chain on the wasm side), and
//! a *constant-fold* path when the list is a compile-time list literal.
//! The runtime path delegates downcast emit to
//! `super::super::native::emit_pair_car_downcast` so each element type
//! gets the right `ref.cast`.
//! Three result positions share the loop emit (`emit_dolist_runtime_loop`
//! / `emit_dolist_const_loop`) and differ only in how the loop's result
//! is produced: effect drops it, the value path serializes it to the
//! output buffer, the stack path leaves a typed value on the operand
//! stack so a consumer (e.g. a `defun` body tail) can read it.
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_nil, eval_value,
};
use crate::compiler::native::emit_pair_car_downcast;
use crate::error::{Error, Result};
use crate::runtime::{Symbol, SymbolKind, SymbolTable};
use super::common::{collect_setf_targets, mark_runtime_setf_targets, promote_to_wasm_local};
/// Parsed `(dolist (var list [result]) body...)` form.
struct DolistSpec<'a> {
var_name: &'a str,
list_expr: &'a Expr,
result_expr: Option<&'a Expr>,
body: &'a [Expr],
}
fn parse_dolist(args: &[Expr]) -> Result<DolistSpec<'_>> {
if args.len() < 2 {
return Err(Error::Compile(
"DOLIST requires a variable specification and at least one body form".to_string(),
));
let var_spec = args[0].as_list().ok_or_else(|| {
Error::Compile(format!(
"DOLIST: expected variable specification list, got {:?}",
args[0]
))
})?;
if var_spec.len() < 2 || var_spec.len() > 3 {
"DOLIST: variable specification must be (var list) or (var list result)".to_string(),
let var_name = var_spec[0].as_symbol().ok_or_else(|| {
"DOLIST: variable must be a symbol, got {:?}",
var_spec[0]
Ok(DolistSpec {
var_name,
list_expr: &var_spec[1],
result_expr: var_spec.get(2),
body: &args[1..],
})
/// Elements of a compile-time-constant list value, or an error if the
/// list expression didn't fold to a list.
fn const_list_elements(list_value: &Expr) -> Result<Vec<Expr>> {
let to_list = |inner: &Expr| match inner {
Expr::List(elems) => Ok(elems.clone()),
Expr::Nil => Ok(vec![]),
_ => Err(Error::Compile(
"DOLIST: list expression must evaluate to a list".to_string(),
)),
match list_value {
Expr::Quote(inner) => to_list(inner),
other => to_list(other),
fn restore_loop_var(symbols: &mut SymbolTable, var_name: &str, saved: Option<Symbol>) {
match saved {
Some(s) => symbols.define(s),
None => {
symbols.remove(var_name);
/// Emits the runtime DOLIST loop: walks the typed `$pair` chain, binding
/// each car to the loop variable and running the body for effect. Leaves
/// the loop variable's symbol-table entry restored and emits no result —
/// the caller decides effect/value/stack result handling.
fn emit_dolist_runtime_loop(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
var_name: &str,
list_expr: &Expr,
body: &[Expr],
elem: PairElement,
) -> Result<()> {
// Declare the loop variable's symbol-table entry first so any body
// setf-target type inference (peeking into CONS step expressions) can
// see the loop var's element-derived type instead of falling back to a
// default. The actual local-set for the loop var rides inside the loop.
let pair_local = ctx.alloc_local(WasmType::PairRef(elem))?;
let saved_loop_var = symbols.lookup(var_name).cloned();
let var_ty = elem.as_wasm_type();
let var_local = ctx.alloc_local(var_ty)?;
symbols.define(
Symbol::new(var_name, SymbolKind::Variable).with_value(Expr::WasmLocal(var_local, var_ty)),
);
for (target, rhs) in collect_setf_targets(body) {
if target != var_name {
promote_to_wasm_local(ctx, emit, symbols, &target, rhs.as_ref())?;
compile_for_stack(ctx, emit, symbols, list_expr)?;
emit.local_set(pair_local);
let pair_idx = ctx.ids.ty_pair;
emit.block_start();
emit.loop_start();
emit.local_get(pair_local);
emit.ref_is_null();
emit.br_if(1);
// Extract the car as anyref, downcast per the element type.
emit.struct_get(pair_idx, 0);
emit_pair_car_downcast(ctx, emit, elem);
emit.local_set(var_local);
for expr in body {
compile_for_effect(ctx, emit, symbols, expr)?;
emit.struct_get(pair_idx, 1);
emit.br(0);
emit.block_end();
restore_loop_var(symbols, var_name, saved_loop_var);
Ok(())
/// Emits the constant-fold DOLIST loop: unrolls over the known elements,
/// binding the loop variable to each and running the body for effect.
/// Leaves the loop variable restored and emits no result.
fn emit_dolist_const_loop(
elements: Vec<Expr>,
// Promote any outer variable the body `setf`s to a runtime value into a
// wasm local before the unroll — without this a `(setf out <runtime>)`
// leaves a bare `WasmRuntime` placeholder with no stack producer. Same
// pre-pass the runtime path runs; the loop var itself is excluded (it's
// rebound to each constant element below).
for element in elements {
symbols.define(Symbol::new(var_name, SymbolKind::Variable).with_value(element));
/// Runs the loop for effect (runtime or constant-fold), leaving no result.
fn emit_dolist_loop(
spec: &DolistSpec<'_>,
let list_value = eval_value(symbols, spec.list_expr)?;
if let Some(WasmType::PairRef(elem)) = list_value.wasm_type() {
emit_dolist_runtime_loop(
ctx,
emit,
symbols,
spec.var_name,
spec.list_expr,
spec.body,
elem,
)
} else {
let elements = const_list_elements(&list_value)?;
emit_dolist_const_loop(ctx, emit, symbols, spec.var_name, elements, spec.body)
pub(super) fn compile_dolist_for_effect(
args: &[Expr],
let spec = parse_dolist(args)?;
emit_dolist_loop(ctx, emit, symbols, &spec)
pub(super) fn compile_dolist(
emit_dolist_loop(ctx, emit, symbols, &spec)?;
match spec.result_expr {
Some(result) => compile_expr(ctx, emit, symbols, result),
compile_nil(ctx, emit);
pub(super) fn compile_dolist_for_stack(
) -> Result<WasmType> {
Some(result) => compile_for_stack(ctx, emit, symbols, result),
// No result form ≡ nil. Push the falsy i31 the stack convention
// uses for nil (typed `Bool` so it serializes as Nil), matching
// `compile_for_stack(Expr::Nil)` and the DO runtime stack path.
emit.i32_const(0);
Ok(WasmType::Bool)
pub(super) fn dolist_form(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
if let Some(WasmType::PairRef(_)) = list_value.wasm_type() {
// Runtime list: the body executes at runtime. Any OUTER variable the
// body `setf`s (an accumulator / counter) is mutated by that runtime
// iteration; mark it runtime so a const-fold of the enclosing form (an
// eval-inlined `(list-length …)` whose tail is the counter, an IF-test
// classification) doesn't read the stale const init and fold the whole
// branch away.
mark_runtime_setf_targets(symbols, spec.body, &[spec.var_name]);
// The dolist's *value* is its result form (or nil), NOT the list —
// mirror what `compile_dolist_for_stack` pushes so this stack-type
// predictor can't drift from codegen.
return match spec.result_expr {
Some(result) => eval_value(symbols, result),
None => Ok(Expr::Nil),
let saved_loop_var = symbols.lookup(spec.var_name).cloned();
symbols.define(Symbol::new(spec.var_name, SymbolKind::Variable).with_value(element));
for expr in spec.body {
eval_value(symbols, expr)?;
restore_loop_var(symbols, spec.var_name, saved_loop_var);