Lines
88.07 %
Functions
27.27 %
Branches
100 %
//! Control-flow helpers for `BLOCK` emit-time result typing.
//!
//! The block's result type is discovered while the body is compiled —
//! each reachable `(return-from)` records its value type into the block
//! frame (see `block.rs`). This module no longer collects exit types by
//! syntactic pre-scan; it only answers two narrower, decidable questions
//! the emitter still needs:
//! - [`form_diverges`] — does a single form unconditionally diverge?
//! Used by `IF`'s stack-compile to peek the live branch past a
//! diverging one, and by BLOCK to stop compiling dead sequential forms.
//! Divergence is conservative: any form we cannot prove diverges is
//! treated as falling through. Over-estimating fall-through only costs a
//! redundant (harmless) seal `unreachable`; it never drops a live value,
//! because actual exit values ride the recorded `return-from` `br` edges.
use crate::ast::{Expr, WasmType};
use crate::compiler::expr::{eval_value, expand_macro};
use crate::error::Result;
use crate::runtime::{SymbolKind, SymbolTable};
const RETURN_FROM: &str = "return-from";
const ERROR: &str = "error";
const IF: &str = "if";
const BEGIN: &str = "begin";
const PROGN: &str = "progn";
const UNWIND_PROTECT: &str = "unwind-protect";
/// Whether control can fall through `body` to its tail value position.
/// False once a form unconditionally diverges — subsequent forms (incl.
/// the tail) are dead. Internal helper for `BEGIN`/`PROGN` divergence.
fn body_falls_through(symbols: &mut SymbolTable, body: &[Expr]) -> Result<bool> {
for (idx, form) in body.iter().enumerate() {
let is_tail = idx + 1 == body.len();
if form_diverges(symbols, form)? {
return Ok(false);
}
if is_tail {
return Ok(true);
Ok(true)
/// Whether `form` unconditionally diverges (cannot fall through to its
/// successor): `(error …)`, any `(return-from …)`, a `BEGIN`/`PROGN`
/// whose last reachable form diverges, or an `IF` whose test or live
/// branch(es) all diverge. Anything else is conservatively treated as
/// falling through — code stored as a value (quote / lambda / labels
/// bodies) and ordinary calls do not diverge here.
///
/// CONTRACT: this is a *query* that internally runs `eval_value` (for
/// const-fold / macro expansion / IF-test classification), which can apply
/// compile-time side effects (`setf`, macro expansion) to `symbols`. Callers
/// MUST pass a THROWAWAY CLONE (`&mut symbols.clone()`), never the live table
/// — every call site does. Because `symbols` here is already disposable, the
/// internal re-evaluations (e.g. `if_diverges` classifying then valuing the
/// test) are clone-local and cannot double-apply effects to a live table.
pub(super) fn form_diverges(symbols: &mut SymbolTable, form: &Expr) -> Result<bool> {
let Expr::List(items) = form else {
};
let head = match items.first() {
Some(Expr::Symbol(s)) => s.as_str(),
// A genuinely-eager non-symbol callee — an inline lambda `((lambda …)
// args)` or a computed function `((f) args)` (head is a `List`/`Lambda`
// node) — evaluates the callee then each arg before the call, so it
// diverges if any of them diverges. Other non-symbol heads (notably a
// quoted symbol `('quote …)`, which the call path may route to the LAZY
// QUOTE special form) are NOT classified eager: over-reporting
// divergence is unsafe (it would drop a live tail), so we conservatively
// fall through to non-diverging for them.
Some(callee @ (Expr::List(_) | Expr::Lambda(_, _))) => {
if form_diverges(symbols, callee)? {
for arg in &items[1..] {
if form_diverges(symbols, arg)? {
_ => return Ok(false),
if head.eq_ignore_ascii_case(RETURN_FROM) || head.eq_ignore_ascii_case(ERROR) {
if head.eq_ignore_ascii_case(BEGIN) || head.eq_ignore_ascii_case(PROGN) {
return body_falls_through(symbols, &items[1..]).map(|ft| !ft);
if head.eq_ignore_ascii_case(IF) {
return if_diverges(symbols, items);
// `(unwind-protect body cleanup...)` diverges if EITHER the protected
// body diverges (the raise/return-from propagates past the form after
// cleanup) OR a cleanup form diverges. Cleanup always runs on the way out,
// so a `(return-from)` / `(error)` / `(go)` in a reachable cleanup
// unconditionally transfers control — the body's value never falls
// through. Misjudging this leaves a dead tail "reachable" and lets BLOCK
// unify a bogus type against the real (cleanup) exit.
if head.eq_ignore_ascii_case(UNWIND_PROTECT) {
let Some((body, cleanup)) = items[1..].split_first() else {
if form_diverges(symbols, body)? {
// Cleanup runs as a sequence; it diverges iff control can't fall
// through it (some reachable form unconditionally exits).
return body_falls_through(symbols, cleanup).map(|ft| !ft);
// A macro call whose expansion diverges (e.g. expands to a bare
// `(return-from …)`) is itself diverging. Expand one step and
// re-classify, mirroring the compile path.
if is_macro(symbols, head) {
return macro_diverges(symbols, head, &items[1..]);
// An eager call (native / operator / user function) evaluates ALL its
// arguments before the call, so it diverges if any argument diverges — the
// arg's `(return-from …)` / `(error …)` transfers control before the call
// runs. e.g. `(cons (return-from b "x") 2)` exits via the car, leaving the
// block's tail dead. Gated on an eager-call head: special forms (handled
// above) and lazy heads (QUOTE / AND / OR with short-circuit) must NOT
// propagate — over-reporting divergence would wrongly drop a live tail.
if head_is_eager_call(symbols, head) {
Ok(false)
/// Whether `head` names an eager call — a native, operator, or user function
/// that evaluates all its arguments before being applied. Special forms and
/// macros (lazy / custom evaluation order) are excluded.
fn head_is_eager_call(symbols: &SymbolTable, head: &str) -> bool {
matches!(
symbols.lookup(head).map(|sym| sym.kind()),
Some(SymbolKind::Native | SymbolKind::Operator | SymbolKind::Function)
)
fn is_macro(symbols: &SymbolTable, head: &str) -> bool {
matches!(symbols.lookup(head), Some(sym) if sym.kind() == SymbolKind::Macro)
/// Classify divergence of a macro call by one-step expansion + recursion,
/// with a depth guard balanced like `expand_macro_then`: enter before
/// expanding, exit after the recursive re-classification (including the
/// error path) so a self-referential macro is bounded while sibling
/// divergence checks on the same throwaway table start from fresh depth.
fn macro_diverges(symbols: &mut SymbolTable, head: &str, args: &[Expr]) -> Result<bool> {
let func = match symbols.lookup(head) {
Some(sym) => sym.function().cloned(),
None => return Ok(false),
let Some(Expr::Lambda(params, body)) = func else {
symbols.enter_macro_expansion()?;
let result = expand_macro(symbols, ¶ms, &body, args).and_then(|expansion| {
let code = match expansion {
Expr::Quote(inner) => *inner,
other => other,
form_diverges(symbols, &code)
});
symbols.exit_macro_expansion();
result
/// `IF` divergence, mirroring the emitter's const-fold: a diverging *test*
/// makes the whole IF diverge (control transfers away before the branches);
/// otherwise a constant test keeps only the live branch and a runtime test
/// diverges only when *both* branches diverge (a missing else-branch falls
/// through).
fn if_diverges(symbols: &mut SymbolTable, items: &[Expr]) -> Result<bool> {
if items.len() < 3 || items.len() > 4 {
if form_diverges(symbols, &items[1])? {
let test = eval_value(symbols, &items[1])?;
if test.is_wasm_runtime() {
let then_div = form_diverges(symbols, &items[2])?;
let else_div = match items.get(3) {
Some(else_branch) => form_diverges(symbols, else_branch)?,
None => false,
return Ok(then_div && else_div);
let live = if super::is_truthy(&test) {
&items[2]
} else {
match items.get(3) {
Some(else_branch) => else_branch,
form_diverges(symbols, live)
/// Static `WasmType` a value-position form would deposit on the stack,
/// looking through a `(return-from name V)` to `V`. Mirrors the literal
/// lowerings the stack-compile path uses. Used by `eval_return_from` to
/// surface a runtime placeholder of the right type.
pub(super) fn peek_value_type(symbols: &mut SymbolTable, expr: &Expr) -> Result<WasmType> {
if let Expr::List(items) = expr
&& let Some(Expr::Symbol(head)) = items.first()
&& head.eq_ignore_ascii_case(RETURN_FROM)
&& items.len() == 3
{
return peek_value_type(symbols, &items[2]);
let resolved = eval_value(symbols, expr)?;
Ok(crate::compiler::expr::classify_stack_type(&resolved).unwrap_or(WasmType::I32))