Lines
89.66 %
Functions
33.33 %
Branches
100 %
//! `IF` special form — eval, effect-compile, and stack-compile paths.
//! Constant-folds when the test resolves at compile time; emits a
//! WASM `if/else` block when the test is a runtime value. The
//! stack variant carries the result type so call sites composing
//! `(if ...)` with arithmetic see the right WasmType.
use wasm_encoder::BlockType;
use crate::ast::{Expr, WasmType};
use crate::compiler::context::CompileContext;
use crate::compiler::emit::FunctionEmitter;
use crate::compiler::expr::{
compile_expr, compile_for_stack, compile_nil, eval_value, serialize_stack_to_output,
};
use crate::error::{Error, Result};
use crate::runtime::SymbolTable;
use super::arms::{
Arm, OnConflict, compile_arm, emit_typed_nil, eval_arm, finish_arm, reconcile_arms,
pub(super) fn compile_if(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
args: &[Expr],
) -> Result<()> {
if args.len() < 2 || args.len() > 3 {
return Err(Error::Compile(
"IF requires a test, a then-form, and an optional else-form".to_string(),
));
}
// Classify the test on a CLONE: its compile-time side effects (setf /
// macro expansion) must reach the live table exactly once, via the single
// emit below — never twice (classify + emit).
let test = eval_value(&mut symbols.clone(), &args[0])?;
let test_diverges = super::block_exits::form_diverges(&mut symbols.clone(), &args[0])?;
super::reject_non_boolean_runtime_test(&test, test_diverges)?;
if test_diverges {
// The test transfers control before yielding a condition (`(error …)`,
// `(return-from …)`). Compile it for effect so the exit actually
// fires; both branches are then dead. Const-folding past it (the bug
// this guards) would silently drop the exit and pick a branch.
return crate::compiler::expr::compile_for_effect(ctx, emit, symbols, &args[0]);
if super::is_runtime_test(&test) {
// Runtime test: emit ONE merged `if (result T)` block via the stack
// path, then serialize the single result. Serializing each branch
// separately (via `compile_expr`) double-advances the compile-time
// output cursor — both branches bake an entity header, but only one
// runs, so the decoder reads a garbage entity slot ("unknown value
// type"). The stack path collapses both arms to one value.
let ty = compile_if_for_stack(ctx, emit, symbols, args)?;
return serialize_stack_to_output(ctx, emit, ty);
let test = super::apply_const_test(ctx, emit, symbols, &args[0], &test)?;
if !test.is_nil() {
compile_expr(ctx, emit, symbols, &args[1])
} else if args.len() == 3 {
compile_expr(ctx, emit, symbols, &args[2])
} else {
compile_nil(ctx, emit);
Ok(())
pub(super) fn compile_if_for_stack(
) -> Result<WasmType> {
// Classify on a CLONE so the test's compile-time effects reach the live
// table only through the single `compile_for_stack` emit below.
// The test exits before producing a condition; compiling it for the
// stack emits its terminating control flow (`br`/`throw`), leaving a
// polymorphic stack that satisfies any declared result type. Both
// branches are dead. Report I32 as a nominal type for callers.
compile_for_stack(ctx, emit, symbols, &args[0])?;
return Ok(WasmType::I32);
return compile_runtime_if(ctx, emit, symbols, args);
compile_for_stack(ctx, emit, symbols, &args[1])
compile_for_stack(ctx, emit, symbols, &args[2])
// Const-false IF with no else ≡ nil — falsy i31, typed `Bool` so it
// serializes as Nil and matches the eval mirror's `Expr::Nil`.
emit.i32_const(0);
Ok(WasmType::Bool)
/// Emit the runtime `if (result T)` block, with the i32 condition already on
/// the stack. Each branch compiles into its own scratch seeded at the
/// branch's wasm depth (`parent + 1`) — `compile_for_stack` lowers a
/// recursive self-call to `call $idx` and terminates, where an `eval_value`
/// peek would inline the body and recurse forever. The real branch types are
/// then unified (divergence-aware, widen-to-AnyRef on disagreement), each
/// scratch is coerced to `T`, and both are spliced into the live block — so
/// each branch is compiled exactly once.
fn compile_runtime_if(
let branch_depth = emit.block_depth() + 1;
let mut then_scratch = FunctionEmitter::new_seeded(branch_depth);
let then_arm = compile_arm(ctx, &mut then_scratch, symbols, &args[1])?;
let mut else_scratch = FunctionEmitter::new_seeded(branch_depth);
let else_arm = match args.get(2) {
Some(else_form) => compile_arm(ctx, &mut else_scratch, symbols, else_form)?,
None => Some(Arm::Nil),
let arms: Vec<Arm> = [then_arm, else_arm].into_iter().flatten().collect();
let result_ty = reconcile_arms(&arms, OnConflict::Widen)?;
if let Some(arm) = then_arm {
finish_arm(ctx, &mut then_scratch, symbols, &args[1], arm, result_ty)?;
match (args.get(2), else_arm) {
(Some(else_form), Some(arm)) => {
finish_arm(ctx, &mut else_scratch, symbols, else_form, arm, result_ty)?;
(None, _) => emit_typed_nil(ctx, &mut else_scratch, result_ty)?,
(Some(_), None) => {}
emit.if_block(BlockType::Result(ctx.wasm_val_type(result_ty)));
emit.splice(&then_scratch.take_bytes());
emit.else_block();
emit.splice(&else_scratch.take_bytes());
emit.block_end();
Ok(result_ty)
pub(super) fn if_form(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
// Classify on a CLONE — branch peeks below must not apply runtime
// branches' side effects to the live table (they execute at runtime, in
// one branch only). A const test re-evals on the live table once below.
// The test exits before yielding a condition; its value is whatever
// the diverging form evaluates to (the branches are dead). This is the
// single live application of the test's effects.
return eval_value(symbols, &args[0]);
// A runtime-test IF emits a wasm `if (result T)` block whose T the
// codegen path (`compile_if_for_stack`) reconciles from both arms.
// Eval-time typing MUST agree, or a binder that sizes a local from this
// eval type (let / defvar / lambda arg) mismatches the value codegen
// pushes — so both sides reconcile the same arms.
let then_arm = eval_arm(symbols, &args[1])?;
Some(else_form) => eval_arm(symbols, else_form)?,
return Ok(Expr::WasmRuntime(reconcile_arms(&arms, OnConflict::Widen)?));
// Const test: apply its effects to the live table once, then the live arm.
let test = eval_value(symbols, &args[0])?;
eval_value(symbols, &args[1])
eval_value(symbols, &args[2])
Ok(Expr::Nil)