Lines
88.89 %
Functions
35 %
Branches
100 %
//! Control-flow special forms.
//!
//! Split by form:
//! - [`quote`] — `QUOTE`.
//! - [`if_form`] — `IF` (compile + for-stack + eval).
//! - [`begin`] — `BEGIN` (sequencing).
//! - [`and_or`] — `AND` / `OR` short-circuit boolean forms.
//! - [`cond`] — `COND` clause chain (rewrites to nested IF for stack).
//! - [`block`] — `BLOCK` / `RETURN-FROM` lexical labelled exits.
//! The shared [`is_truthy`] predicate lives here so every submodule
//! plus the iteration forms can reach it via `super::is_truthy`.
mod and_or;
mod begin;
mod block;
mod block_exits;
mod cond;
mod error_form;
mod handler_case;
mod if_form;
mod quote;
mod tagbody;
mod unwind_protect;
use crate::ast::{Expr, WasmType};
use crate::error::{Error, Result};
use crate::runtime::SymbolTable;
use super::SpecialFormSpec;
pub(in crate::compiler) fn is_truthy(expr: &Expr) -> bool {
!matches!(expr, Expr::Nil | Expr::Bool(false))
}
/// Whether an evaluated IF / COND test is a *runtime* i32 condition — i.e.
/// it must be lowered to a wasm `if` rather than const-folded. A wasm `if`
/// consumes an i32, so the only valid runtime test is an i32-typed runtime
/// value: a host-fn/comparison result (`WasmRuntime(I32)`) or a let-bound
/// runtime boolean (`WasmLocal(_, I32)`). All IF/COND paths (eval, stack,
/// effect) MUST share this predicate, or a let-bound runtime boolean is
/// const-folded by one path and branched by another — a wrong-branch
/// miscompile.
/// Re-export of the divergence classifier for IF/COND callers outside the
/// `control` module (the effect-position IF path in `expr::effect`). Must be
/// run on a CLONED symbol table — see [`block_exits::form_diverges`].
pub(in crate::compiler) fn form_diverges_for_test(
symbols: &mut SymbolTable,
form: &Expr,
) -> Result<bool> {
block_exits::form_diverges(symbols, form)
pub(in crate::compiler) fn is_runtime_test(test: &Expr) -> bool {
matches!(
test,
Expr::WasmRuntime(WasmType::I32 | WasmType::Bool)
| Expr::WasmLocal(_, WasmType::I32 | WasmType::Bool)
)
/// A wasm `if` consumes an i32 condition. A runtime test of any other type
/// (e.g. a `Ratio` returned by a host fn) has no boolean lowering — silently
/// const-folding it via [`is_truthy`] would mis-branch, so reject it loudly.
/// Compile-time constants fold and an i32 runtime value is the live path; any
/// other runtime type is the error. Shared by IF and COND.
///
/// A *diverging* test (`(return-from …)`, `(error …)`) is exempt: it never
/// produces a value used as a condition — control transfers away first — so
/// its nominal non-i32 type is irrelevant. `diverges` must be classified on a
/// cloned symbol table by the caller (the test runs before the branches, so
/// emit-time discovery still collects its exit).
pub(in crate::compiler) fn reject_non_boolean_runtime_test(
test: &Expr,
diverges: bool,
) -> Result<()> {
if diverges {
return Ok(());
match test {
| Expr::WasmLocal(_, WasmType::I32 | WasmType::Bool) => Ok(()),
Expr::WasmRuntime(ty) | Expr::WasmLocal(_, ty) => Err(Error::Compile(format!(
"IF/COND test must be a boolean (i32) runtime value, got {ty}"
))),
_ => Ok(()),
pub(super) const FORMS: &[SpecialFormSpec] = &[
SpecialFormSpec {
name: "QUOTE",
eval: quote::eval_quote,
compile: quote::spec_compile_quote,
stack: None,
effect: None,
},
name: "IF",
eval: if_form::if_form,
compile: if_form::compile_if,
stack: Some(if_form::compile_if_for_stack),
name: "BEGIN",
eval: begin::begin_form,
compile: begin::compile_begin,
stack: Some(begin::compile_begin_for_stack),
name: "AND",
eval: and_or::and_form,
compile: and_or::compile_and,
stack: Some(and_or::compile_and_for_stack),
effect: Some(and_or::compile_and_for_effect),
name: "OR",
eval: and_or::or_form,
compile: and_or::compile_or,
stack: Some(and_or::compile_or_for_stack),
effect: Some(and_or::compile_or_for_effect),
name: "COND",
eval: cond::cond_form,
compile: cond::compile_cond,
stack: Some(cond::compile_cond_for_stack),
effect: Some(cond::compile_cond_for_effect),
name: "ERROR",
eval: error_form::eval_error,
compile: error_form::compile_error,
stack: Some(error_form::compile_error_for_stack),
name: "BLOCK",
eval: block::eval_block,
compile: block::compile_block,
stack: Some(block::compile_block_for_stack),
effect: Some(block::compile_block_for_effect),
name: "RETURN-FROM",
eval: block::eval_return_from,
compile: block::compile_return_from,
stack: Some(block::compile_return_from_for_stack),
name: "TAGBODY",
eval: tagbody::eval_tagbody,
compile: tagbody::compile_tagbody,
stack: Some(tagbody::compile_tagbody_for_stack),
effect: Some(tagbody::compile_tagbody_for_effect),
name: "GO",
eval: tagbody::eval_go,
compile: tagbody::compile_go,
stack: Some(tagbody::compile_go_for_stack),
name: "HANDLER-CASE",
eval: handler_case::eval_handler_case,
compile: handler_case::compile_handler_case,
stack: Some(handler_case::compile_handler_case_for_stack),
effect: Some(handler_case::compile_handler_case_for_effect),
name: "UNWIND-PROTECT",
eval: unwind_protect::eval_unwind_protect,
compile: unwind_protect::compile_unwind_protect,
stack: Some(unwind_protect::compile_unwind_protect_for_stack),
effect: Some(unwind_protect::compile_unwind_protect_for_effect),
];