Lines
85.94 %
Functions
16.92 %
Branches
100 %
//! `(handler-case)` — pattern-match recovery from a raised condition
//! (Tier 3.3, ADR-0026).
//!
//! ```lisp
//! (handler-case <body>
//! (no-such-account (e) (log (error-message e)))
//! (commodity-mismatch (e) (recover))
//! (t (e) (error-code e))) ; t = catch-all (optional, last)
//! ```
//! Lowering (single-value `Catch::One`, the same shape as the Tier 3.2
//! boundary wrapper):
//! ```text
//! block $outer (result T)
//! block $handler (result (ref null $nomi_condition))
//! try_table (result T) (catch $nomi_error → $handler)
//! <body → T>
//! end
//! br $outer ; normal completion carries T out
//! end ; catch edge: condition ref on stack
//! local.set $cond
//! <flat dispatch> ; each clause an independent `if` that
//! ; `br $outer`s on match; t = uncond.;
//! ; no match → re-throw same condition
//! Result type `T` is discovered at emit time and unified across the body
//! and every reachable clause body (mirrors BLOCK's emit-time discovery);
//! a diverging arm contributes no type. Re-raise on no-match re-`throw`s a
//! fresh `$nomi_error` carrying the SAME condition struct — nomiscript
//! conditions have no observable identity, so this is equivalent to
//! re-raising the original and avoids needing an exnref.
use wasm_encoder::{BlockType, Catch, HeapType, RefType, ValType};
use crate::ast::{EntityKind, Expr, WasmType};
use crate::compiler::context::CompileContext;
use crate::compiler::emit::FunctionEmitter;
use crate::compiler::expr::{compile_for_stack, eval_value, serialize_stack_to_output};
use crate::error::{Error, Result};
use crate::runtime::{Symbol, SymbolKind, SymbolTable};
use super::block_exits::form_diverges;
const HANDLER_CASE: &str = "handler-case";
const CATCH_ALL: &str = "T";
/// One parsed clause: `code` is `None` for the `t` catch-all, else the
/// (reader-upcased) condition-code symbol; `var` is the condition binding;
/// `body` is the clause's body forms.
struct Clause<'a> {
code: Option<&'a str>,
var: &'a str,
body: &'a [Expr],
}
pub(super) fn eval_handler_case(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
let (body, clauses) = parse(args)?;
// If the body can't throw and folds to a constant, that's the value.
let body_val = eval_value(symbols, body)?;
let body_diverges = form_diverges(&mut symbols.clone(), body)?;
if !body_val.is_wasm_runtime() && !body_diverges {
return Ok(body_val);
// Otherwise the form has a runtime result. Its type must match what
// `compile_handler_case_for_stack` emits: the unified type of the body
// (when it can fall through) plus every reachable clause body — NOT just
// the body, which is wrong when the body always throws (e.g. an `(error)`
// body whose value comes from a clause). Callers that materialise a
// runtime value (let/defvar binding, lambda arg) trust this type to size
// the local, so a body-only type yields an invalid module.
let mut exit_types: Vec<WasmType> = Vec::new();
if !body_diverges && let Some(ty) = runtime_type(&body_val) {
exit_types.push(ty);
for clause in &clauses {
let mut clause_syms = symbols.clone();
clause_syms.define(Symbol::new(clause.var, SymbolKind::Variable).with_value(
Expr::WasmLocal(0, WasmType::EntityRef(EntityKind::Condition)),
));
let (cval, cdiverges) = eval_clause_value(&mut clause_syms, clause)?;
if !cdiverges && let Some(ty) = runtime_type(&cval) {
Ok(Expr::WasmRuntime(unify_handler_types(&exit_types)?))
/// Eval-time value + divergence of a clause body, mirroring the
/// compile-side `compile_clause_body` walk (last form is the value; an
/// earlier diverging form makes the rest dead).
fn eval_clause_value(symbols: &mut SymbolTable, clause: &Clause<'_>) -> Result<(Expr, bool)> {
if clause.body.is_empty() {
return Ok((Expr::Nil, false));
let last = clause.body.len() - 1;
for (idx, form) in clause.body.iter().enumerate() {
if idx == last {
let diverges = form_diverges(&mut symbols.clone(), form)?;
return Ok((eval_value(symbols, form)?, diverges));
if form_diverges(&mut symbols.clone(), form)? {
eval_value(symbols, form)?;
return Ok((Expr::Nil, true));
Ok((Expr::Nil, false))
/// The `WasmType` an eval result would carry on the stack (`None` for a form
/// with no directly-classifiable stack value), via the shared
/// [`crate::compiler::expr::classify_stack_type`] — so a handler/body arm
/// typed here can't drift from what `compile_for_stack` emits.
fn runtime_type(val: &Expr) -> Option<WasmType> {
crate::compiler::expr::classify_stack_type(val)
pub(super) fn compile_handler_case(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
args: &[Expr],
) -> Result<()> {
let ty = compile_handler_case_for_stack(ctx, emit, symbols, args)?;
serialize_stack_to_output(ctx, emit, ty)
pub(super) fn compile_handler_case_for_effect(
compile_handler_case_for_stack(ctx, emit, symbols, args)?;
emit.drop_value();
Ok(())
pub(super) fn compile_handler_case_for_stack(
) -> Result<WasmType> {
// Stash local for the caught condition, allocated before the body so the
// body's own locals stack above it (matches the boundary wrapper).
let cond_local = ctx.alloc_local(WasmType::AnyRef)?;
// --- Phase 1: compile every arm into its own scratch, collect types. ---
// Each scratch is seeded at the depth it will actually be spliced into,
// so relative `br` targets of a `(return-from)` / `(go)` inside the arm
// stay correct after splicing:
// - body: inside $outer/$handler/try_table → parent + 3.
// - a non-`t` clause: inside $outer + its own guard `if` → parent + 2.
// - the `t` clause: inside $outer only (no guard `if`) → parent + 1.
let parent_depth = emit.block_depth();
let body_depth = parent_depth + 3;
let mut body_scratch = FunctionEmitter::new_seeded(body_depth);
let body_ty = compile_for_stack(ctx, &mut body_scratch, symbols, body)?;
if !body_diverges {
exit_types.push(body_ty);
let mut clause_scratches: Vec<(FunctionEmitter, bool)> = Vec::with_capacity(clauses.len());
let clause_depth = if clause.code.is_some() {
parent_depth + 2
} else {
parent_depth + 1
};
let mut scratch = FunctionEmitter::new_seeded(clause_depth);
Expr::WasmLocal(cond_local, WasmType::EntityRef(EntityKind::Condition)),
let (cty, cdiverges) = compile_clause_body(ctx, &mut scratch, &mut clause_syms, clause)?;
if !cdiverges {
exit_types.push(cty);
clause_scratches.push((scratch, cdiverges));
let result_ty = unify_handler_types(&exit_types)?;
// --- Phase 2: emit the real structure, splicing the scratch bodies. ---
let cond_ref = ValType::Ref(RefType {
nullable: true,
heap_type: HeapType::Concrete(ctx.condition_type_idx()),
});
let result_vt = ctx.wasm_val_type(result_ty);
let tag = ctx.nomi_error_tag();
emit.block_start_typed(BlockType::Result(result_vt)); // $outer
emit.block_start_typed(BlockType::Result(cond_ref)); // $handler
emit.try_table(
BlockType::Result(result_vt),
&[Catch::One { tag, label: 0 }],
);
emit.splice(&body_scratch.take_bytes());
if body_diverges {
// Dead tail value after a diverging body; reset to polymorphic so
// the try_table's declared result type is satisfied vacuously.
emit.unreachable();
emit.block_end(); // close try_table
emit.br(1); // normal completion → $outer, skip handler
emit.block_end(); // close $handler — catch lands here, condition on stack
emit.local_set(cond_local);
emit_dispatch(
ctx,
emit,
&clauses,
&clause_scratches,
cond_local,
result_ty,
)?;
emit.block_end(); // close $outer
Ok(result_ty)
/// Compile a clause body (already has `e` bound in `symbols`) into `emit`,
/// returning its result type and whether it diverges. A clause with an
/// empty body yields `nil` (i32 0).
fn compile_clause_body(
clause: &Clause<'_>,
) -> Result<(WasmType, bool)> {
// Empty clause body ≡ nil — falsy i31, typed `Bool` so it serializes
// as Nil and unifies homogeneously with other Bool-typed arms.
emit.i32_const(0);
return Ok((WasmType::Bool, false));
let mut diverges = false;
diverges = form_diverges(&mut symbols.clone(), form)?;
let ty = compile_for_stack(ctx, emit, symbols, form)?;
return Ok((ty, diverges));
crate::compiler::expr::compile_for_effect(ctx, emit, symbols, form)?;
return Ok((WasmType::I32, true));
Ok((WasmType::I32, diverges))
/// Emit the flat dispatch at depth `parent+1` (only `$outer` open). Each
/// non-`t` clause is an independent `if` (Empty type) whose body, on match,
/// runs the spliced clause and `br $outer`s — so every clause body sits at
/// the same depth. A `t` clause runs unconditionally. With no `t`, a
/// fall-through re-throws the same condition.
fn emit_dispatch(
clauses: &[Clause<'_>],
scratches: &[(FunctionEmitter, bool)],
cond_local: u32,
result_ty: WasmType,
let condition_idx = ctx.condition_type_idx();
let string_eq = ctx.ids.string_eq;
for (clause, (scratch, diverges)) in clauses.iter().zip(scratches) {
match clause.code {
Some(code) => {
// condition.code == "CODE" ? → guard an Empty `if`.
emit.local_get(cond_local);
emit.ref_cast(condition_idx);
emit.struct_get(condition_idx, 0);
push_string_literal(ctx, emit, code)?;
emit.call(string_eq);
emit.if_block(BlockType::Empty);
emit.splice(scratch.bytes_ref());
// The clause value (if it didn't diverge) is on the stack;
// carry it out to `$outer` (relative depth 1 from inside the
// `if`). A diverging clause already left a polymorphic stack.
if !*diverges {
emit.br(1);
emit.block_end();
None => {
// `t` catch-all (last clause): runs unconditionally at
// `$outer`'s depth. Its value falls through to `$outer`'s
// end — no `br` needed. A diverging `t` body left a
// polymorphic stack, also fine.
let _ = (diverges, result_ty);
return Ok(());
// No `t` and nothing matched: re-throw a fresh $nomi_error carrying the
// same condition (no observable identity loss). The condition is stashed
// as `anyref`, so cast back to the concrete `$nomi_condition` (the tag
// param type) before throwing. `throw` is stack-polymorphic, satisfying
// `$outer`'s declared result type.
emit.throw(tag);
/// Unify reachable arm types into the handler-case result type. The body
/// (if it falls through) and each non-diverging clause must agree. Empty
/// (all arms diverge) → I32 (wasm polymorphism).
fn unify_handler_types(exit_types: &[WasmType]) -> Result<WasmType> {
let mut chosen: Option<WasmType> = None;
for &ty in exit_types {
match chosen {
Some(existing) if existing != ty => {
return Err(Error::Compile(format!(
"HANDLER-CASE: conflicting result types {existing} and {ty}"
)));
Some(_) => {}
None => chosen = Some(ty),
Ok(chosen.unwrap_or(WasmType::I32))
fn push_string_literal(
value: &str,
let data_idx = ctx.add_data(value.as_bytes())?;
emit.i32_const(value.len() as i32);
emit.array_new_data(ctx.ids.ty_i8_array, data_idx);
/// Parse `(handler-case body clause...)`. Requires ≥1 body form; clauses
/// follow. Each clause is `(code-sym (var) body...)`; `t` is the catch-all
/// and, if present, must be the last clause.
fn parse(args: &[Expr]) -> Result<(&Expr, Vec<Clause<'_>>)> {
let (body, clause_exprs) = args.split_first().ok_or_else(|| {
Error::Compile(format!(
"{HANDLER_CASE} requires a body and zero or more clauses"
))
})?;
let mut clauses = Vec::with_capacity(clause_exprs.len());
for (i, clause) in clause_exprs.iter().enumerate() {
let elems = clause.as_list().ok_or_else(|| {
"{HANDLER_CASE}: clause must be a list, got {clause:?}"
if elems.len() < 2 {
"{HANDLER_CASE}: clause needs a code symbol, a (var), and a body"
let code = match &elems[0] {
// `t` reads as the boolean literal, not a symbol — it's the
// catch-all marker. A symbol head is a condition code.
Expr::Bool(true) => None,
Expr::Symbol(s) if s == CATCH_ALL => None,
Expr::Symbol(s) => Some(s.as_str()),
other => {
"{HANDLER_CASE}: clause head must be a code symbol or t, got {other:?}"
if code.is_none() && i + 1 != clause_exprs.len() {
"{HANDLER_CASE}: the t catch-all clause must be last"
let var = match elems[1].as_list() {
Some([Expr::Symbol(v)]) => v.as_str(),
_ => {
"{HANDLER_CASE}: clause binding must be a single (var), got {:?}",
elems[1]
clauses.push(Clause {
code,
var,
body: &elems[2..],
Ok((body, clauses))