Lines
75.61 %
Functions
14.55 %
Branches
100 %
//! `COND` clause chain. The stack-result variant rewrites the chain
//! into a nested `IF` and delegates to `compile_if_for_stack` so the
//! resulting wasm block carries a value back to the caller. Effect
//! and runtime paths walk the chain directly so a known-true clause
//! can stop emission early.
use wasm_encoder::BlockType;
use crate::ast::{Expr, WasmType};
use crate::compiler::context::CompileContext;
use crate::compiler::emit::FunctionEmitter;
use crate::compiler::expr::{
compile_body, compile_expr, compile_for_effect, compile_for_stack, compile_nil, eval_value,
serialize_stack_to_output,
};
use crate::error::{Error, Result};
use crate::runtime::SymbolTable;
use super::is_truthy;
pub(super) fn compile_cond(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
args: &[Expr],
) -> Result<()> {
for (i, clause) in args.iter().enumerate() {
let elems = clause.as_list().ok_or_else(|| {
Error::Compile(format!("COND: clause must be a list, got {clause:?}"))
})?;
if elems.is_empty() {
return Err(Error::Compile("COND: empty clause".to_string()));
}
// Classify on a CLONE so the test's compile-time effects reach the
// live table only via the single emit below.
let test = eval_value(&mut symbols.clone(), &elems[0])?;
let test_diverges = super::block_exits::form_diverges(&mut symbols.clone(), &elems[0])?;
super::reject_non_boolean_runtime_test(&test, test_diverges)?;
if super::is_runtime_test(&test) || test_diverges {
// Runtime (or diverging) test: emit ONE merged `if (result T)`
// chain via the stack path, then serialize the single result.
// Serializing each clause body separately double-advances the
// compile-time output cursor (every clause bakes an entity header,
// but only one runs) — the decoder then reads a garbage entity
// slot. The IF-chain the stack path builds also fires a diverging
// test correctly (vs. const-folding past it). Mirror IF's fix.
let ty = compile_cond_for_stack(ctx, emit, symbols, &args[i..])?;
return serialize_stack_to_output(ctx, emit, ty);
// Const test: apply its effects to the live table once.
let test = eval_value(symbols, &elems[0])?;
if is_truthy(&test) {
if elems.len() == 1 {
return compile_expr(ctx, emit, symbols, &test);
return compile_body(ctx, emit, symbols, &elems[1..]);
compile_nil(ctx, emit);
Ok(())
pub(super) fn compile_cond_for_stack(
) -> Result<WasmType> {
let chain = rewrite_cond_to_if_chain(args)?;
compile_for_stack(ctx, emit, symbols, &chain)
fn rewrite_cond_to_if_chain(clauses: &[Expr]) -> Result<Expr> {
if clauses.is_empty() {
return Ok(Expr::Nil);
let head = clauses[0].as_list().ok_or_else(|| {
Error::Compile(format!("COND: clause must be a list, got {:?}", clauses[0]))
if head.is_empty() {
let test = head[0].clone();
let body = match head.len() {
1 => test.clone(),
2 => head[1].clone(),
_ => {
let mut forms = Vec::with_capacity(head.len());
forms.push(Expr::Symbol("BEGIN".to_string()));
forms.extend_from_slice(&head[1..]);
Expr::List(forms)
let else_branch = rewrite_cond_to_if_chain(&clauses[1..])?;
Ok(Expr::List(vec![
Expr::Symbol("IF".to_string()),
test,
body,
else_branch,
]))
pub(super) fn compile_cond_for_effect(
// Classify on a CLONE so the test's compile-time side effects are not
// applied to the live table during classification — they ride the
// single emit below exactly once.
return compile_cond_runtime_for_effect(ctx, emit, symbols, &args[i..]);
// Const-false/true test: apply its effects to the live table once.
for expr in &elems[1..] {
compile_for_effect(ctx, emit, symbols, expr)?;
return Ok(());
/// Effect-position codegen for a COND suffix whose first clause has a runtime
/// (or diverging) test. Each runtime clause opens a guard `if` and (when not
/// last) an `else`; clause bodies compile for effect — so effect-only forms
/// (`DOLIST`, etc.) that have no stack lowering still work, unlike a
/// stack+drop rewrite. Tracks the ACTUAL number of open guard blocks (not the
/// clause index, which over-counts when const-false clauses sit between
/// runtime ones) so the closing `block_end`s match exactly.
fn compile_cond_runtime_for_effect(
let last = args.len() - 1;
let mut open_guards = 0u32;
if test_diverges {
// Compile the test for effect so its exit fires; the remaining
// chain is dead. Close exactly the guards opened so far.
compile_for_effect(ctx, emit, symbols, &elems[0])?;
for _ in 0..open_guards {
emit.block_end();
if super::is_runtime_test(&test) {
compile_for_stack(ctx, emit, symbols, &elems[0])?;
emit.if_block(BlockType::Empty);
// Each runtime clause opens exactly one `if` block (one `end`),
// whether or not an `else` arm follows — count it.
open_guards += 1;
if i != last {
emit.else_block();
} else {
// Const test: apply effects to the live table once.
pub(super) fn cond_form(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
// Classify the test on a CLONE so its compile-time side effects
// (setf / macro expansion) are NOT applied to the live table here:
// a const-false test re-evals once on `symbols` below, and a
// runtime/diverging test's effects ride the single `eval_value` of
// the synthesized chain — never twice.
// First runtime (or diverging) test: the result type is the
// unified type of the remaining IF-chain (the same chain
// `compile_cond_for_stack` emits), NOT a blanket I32 — a binder
// sizing a local from this type must match the codegen. This is
// the SINGLE place the suffix's side effects reach `symbols`.
let chain = rewrite_cond_to_if_chain(&args[i..])?;
return eval_value(symbols, &chain);
// Const test: apply its effects to the live table exactly once.
return Ok(test);
return super::super::binding::eval_body(symbols, &elems[1..]);
Ok(Expr::Nil)