Lines
74.07 %
Functions
16.25 %
Branches
100 %
//! `(catch-each items var body)` codegen — Tier 2's named recovery
//! primitive (ADR-0025).
//!
//! Lowering: compile `body` as `(lambda (var) body)` via the Tier 1.5
//! closure machinery to get a `$closure_<sig>` value on the stack;
//! extract its funcref + env, push the items pair-list, then call the
//! `__nomi_catch_each` host import. The host walks the chain in Rust,
//! invokes the funcref per item recovering per-call `wasmtime::Error`,
//! and returns a heterogeneous `pair<anyref>` of `(ok . v)` / `(err
//! . (code . msg))` cells. Engine traps (`OutOfFuel` /
//! `EpochInterrupt`) bypass capture and re-throw to the outer call site
//! — they're never catchable.
use crate::ast::{Expr, LambdaParams, PairElement, 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::compiler::special::try_emit_lambda_for_host_iter;
use crate::error::{Error, Result};
use crate::runtime::SymbolTable;
const RESULT_TY: WasmType = WasmType::PairRef(PairElement::AnyRef);
pub(super) fn compile_catch_each(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
args: &[Expr],
) -> Result<()> {
let ty = compile_catch_each_for_stack(ctx, emit, symbols, args)?;
serialize_stack_to_output(ctx, emit, ty)
}
pub(super) fn compile_catch_each_for_effect(
compile_catch_each_for_stack(ctx, emit, symbols, args)?;
emit.drop_value();
Ok(())
pub(super) fn compile_catch_each_for_stack(
) -> Result<WasmType> {
let (items_expr, var_name, body) = parse_form(args)?;
let elem_ty = infer_items_elem_type(symbols, items_expr)?;
let params = LambdaParams::simple(vec![var_name.to_string()]);
let sig = try_emit_lambda_for_host_iter(ctx, emit, symbols, ¶ms, body, &[elem_ty])?
.ok_or_else(|| {
Error::Compile(
"catch-each: body lambda is out of v1 first-class-closure scope; \
rewrite the body to use only required parameters"
.to_string(),
)
})?;
let closure_ty = WasmType::Closure(sig);
let closure_local = ctx.alloc_local(closure_ty)?;
emit.local_set(closure_local);
let closure_type_idx = ctx.closure_sig(sig).closure_type_idx;
emit.local_get(closure_local);
emit.struct_get(closure_type_idx, 0);
emit.struct_get(closure_type_idx, 1);
let items_ty = compile_items_for_stack(ctx, emit, symbols, items_expr)?;
enforce_items_pair(items_ty)?;
let func_idx = ctx.ids.nomi_catch_each()?;
emit.call(func_idx);
emit.ref_cast_nullable(ctx.ids.ty_pair);
Ok(RESULT_TY)
/// Infers the user-visible element type of the items pair-list. The
/// closure body's iteration variable must bind at this type so the body
/// can use it in arithmetic / typed positions; the host fn passes raw
/// anyref per element and the closure's prologue downcasts to this type
/// before the body runs. Empty-list / heterogeneous-AnyRef shapes fall
/// back to AnyRef so the body can still reference the variable, just
/// untyped.
fn infer_items_elem_type(symbols: &mut SymbolTable, items_expr: &Expr) -> Result<WasmType> {
let resolved = eval_value(symbols, items_expr)?;
Ok(match resolved.wasm_type() {
Some(WasmType::PairRef(elem)) => elem.as_wasm_type(),
_ => fallback_elem_type_from_literal(&resolved),
})
fn fallback_elem_type_from_literal(expr: &Expr) -> WasmType {
match expr {
Expr::Quote(inner) => fallback_elem_type_from_literal(inner),
Expr::List(elems) => first_homogeneous_type(elems).unwrap_or(WasmType::AnyRef),
_ => WasmType::AnyRef,
fn first_homogeneous_type(elems: &[Expr]) -> Option<WasmType> {
let head = elems.first()?;
let head_ty = literal_wasm_type(head)?;
if elems.iter().all(|e| literal_wasm_type(e) == Some(head_ty)) {
Some(head_ty)
} else {
Some(WasmType::AnyRef)
/// Mirrors the literal element classification in
/// `compiler::native::list::infer::literal_pair_element` so the closure
/// body's iteration variable binds at the same type CAR/CDR extract — a
/// numeric literal is a dimensionless `Ratio` (never a count), and bool
/// literals ride the `PairElement::Bool` slot (so the iteration var is `Bool`,
/// recovered as Nil/Bool not Number).
fn literal_wasm_type(expr: &Expr) -> Option<WasmType> {
Expr::Number(_) => Some(WasmType::Ratio),
Expr::String(_) => Some(WasmType::StringRef),
Expr::Bool(_) => Some(WasmType::Bool),
Expr::WasmRuntime(t) | Expr::WasmLocal(_, t) => Some(*t),
Expr::Quote(inner) => literal_wasm_type(inner),
_ => None,
pub(super) fn catch_each_form(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
let (items_expr, _, _) = parse_form(args)?;
let items_value = eval_value(symbols, items_expr)?;
enforce_items_pair_value(&items_value)?;
Ok(Expr::WasmRuntime(RESULT_TY))
fn parse_form(args: &[Expr]) -> Result<(&Expr, &str, &Expr)> {
if args.len() != 3 {
return Err(Error::Arity {
name: "catch-each".to_string(),
expected: 3,
actual: args.len(),
});
let var_name = args[1].as_symbol().ok_or_else(|| {
Error::Compile(format!(
"catch-each: variable must be a symbol, got {:?}",
args[1]
))
Ok((&args[0], var_name, &args[2]))
/// Materializes `items_expr` as a `pair<...>` value on the stack.
/// `(list)` resolves to `Expr::Nil` / `Expr::List([])`, which
/// `compile_for_stack` lowers to `i32_const(0)` (legacy `Nil` slot).
/// `catch-each` needs a *typed* pair-null, so detect empties up front
/// and emit `ref.null $pair` directly.
fn compile_items_for_stack(
items_expr: &Expr,
if is_empty_list(&resolved) {
emit.ref_null(ctx.ids.ty_pair);
return Ok(RESULT_TY);
compile_for_stack(ctx, emit, symbols, items_expr)
fn is_empty_list(expr: &Expr) -> bool {
Expr::Nil => true,
Expr::List(elems) => elems.is_empty(),
Expr::Quote(inner) => is_empty_list(inner),
_ => false,
fn enforce_items_pair(ty: WasmType) -> Result<()> {
match ty {
WasmType::PairRef(_) => Ok(()),
other => Err(Error::Compile(format!(
"catch-each: items expression must be a pair list, got {other}"
))),
fn enforce_items_pair_value(value: &Expr) -> Result<()> {
match value.wasm_type() {
Some(WasmType::PairRef(_)) | None => Ok(()),
Some(other) => Err(Error::Compile(format!(