Lines
99.17 %
Functions
20 %
Branches
100 %
//! `(unwind-protect body cleanup...)` — guaranteed cleanup (Tier 3.4,
//! ADR-0026).
//!
//! ```lisp
//! (unwind-protect
//! (acquire-and-do-risky-stuff)
//! (release-resource))
//! ```
//! `cleanup` runs on BOTH normal completion of `body` AND on a `$nomi_error`
//! raised inside it; the raise is then re-raised (cleanup never swallows it).
//! The form's value is `body`'s value on normal completion.
//! Lowering (single-tag `Catch::One`, same shape family as the Tier 3.2
//! boundary wrapper and `(handler-case)`). Cleanup is emitted EXACTLY ONCE:
//! both exit paths converge on a `(ref null $nomi_condition)` — the caught
//! condition on the exceptional path, a null sentinel on the normal path —
//! so a single cleanup splice covers both, after which `ref.is_null` decides
//! whether to return the stashed body value or re-raise.
//! ```text
//! block $done (result T)
//! block $unwind (result (ref null $nomi_condition))
//! try_table (result T) (catch $nomi_error → $unwind)
//! <body → T>
//! end
//! local.set $result ; normal: stash T, push null sentinel
//! ref.null $nomi_condition
//! end ; catch edge delivers the condition
//! local.set $cond ; cond = caught condition | null
//! <cleanup for effect> ; runs once, on BOTH paths
//! local.get $cond
//! ref.is_null
//! (if (result T)
//! (then local.get $result) ; normal: return the stashed value
//! (else local.get $cond; ref.cast; throw $nomi_error)) ; re-raise
//! `Catch::One` on `$nomi_error` (the only exception tag) catches every
//! catchable error — engine deadlines (`OutOfFuel` / `EpochInterrupt`) are
//! NOT wasm exceptions and bypass `try_table` entirely, so cleanup does not
//! run for them (documented in `doc/scripting/error-handling.org`). Re-raise
//! re-`throw`s a fresh `$nomi_error` carrying the SAME condition struct;
//! nomiscript conditions have no observable identity, so this needs no
//! `exnref` / `throw_ref` and stays uniform with handler-case. The null
//! sentinel is unambiguous: `(error)` and every boundary throw build the
//! condition with `struct.new` (always non-null).
//! Cleanup compiles on the LIVE symbol table (not a clone): it shares the
//! surrounding lexical scope, so a `(setf x …)` in cleanup must promote `x`
//! to the same runtime local a later read of `x` resolves to. Emitting it
//! once — at a single wasm depth — also keeps any relative `br` inside
//! cleanup (a `return-from` / `go` out of it) correct without duplication.
use wasm_encoder::{BlockType, Catch, HeapType, RefType, ValType};
use crate::ast::{Expr, WasmType};
use crate::compiler::context::CompileContext;
use crate::compiler::emit::FunctionEmitter;
use crate::compiler::expr::{
compile_for_effect, compile_for_stack, eval_value, serialize_stack_to_output,
};
use crate::error::{Error, Result};
use crate::runtime::SymbolTable;
use super::block_exits::form_diverges;
const UNWIND_PROTECT: &str = "unwind-protect";
pub(super) fn eval_unwind_protect(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
let (body, cleanup) = parse(args)?;
let body_diverges = form_diverges(&mut symbols.clone(), body)?;
let body_val = eval_value(symbols, body)?;
// Cleanup shares the surrounding lexical scope, so eval it on the LIVE
// table (like BLOCK's body) — a `(setf x …)` in cleanup must promote `x`
// for a later read to resolve to the same runtime local.
for form in cleanup {
eval_value(symbols, form)?;
}
if !body_val.is_wasm_runtime() && !body_diverges {
// Body folds to a constant and can't raise — that's the value.
return Ok(body_val);
// Runtime result: the value is the body's; cleanup is effect-only.
Ok(Expr::WasmRuntime(runtime_type(&body_val)))
/// The `WasmType` an eval result carries on the stack, mirroring the literal
/// lowerings the stack-compile path uses. Bool/nil literals lower to `Bool`
/// (matching `compile_for_stack`), so a bound unwind-protect result is sized
/// to the value codegen actually pushes — else a runtime-boolean body would
/// mis-serialize as Number.
fn runtime_type(val: &Expr) -> WasmType {
crate::compiler::expr::classify_stack_type(val).unwrap_or(WasmType::I32)
pub(super) fn compile_unwind_protect(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
args: &[Expr],
) -> Result<()> {
let ty = compile_unwind_protect_for_stack(ctx, emit, symbols, args)?;
serialize_stack_to_output(ctx, emit, ty)
pub(super) fn compile_unwind_protect_for_effect(
compile_unwind_protect_for_stack(ctx, emit, symbols, args)?;
emit.drop_value();
Ok(())
pub(super) fn compile_unwind_protect_for_stack(
) -> Result<WasmType> {
// `result_local` stashes the body's value on the normal path while
// cleanup runs; `cond_local` (anyref) holds the caught condition on the
// exceptional path, or null on the normal path. Both allocated before the
// body so the body's own locals stack above them.
let cond_local = ctx.alloc_local(WasmType::AnyRef)?;
// --- Phase 1: compile body + cleanup into scratches on LIVE symbols. ---
// Body runs inside $done/$unwind/try_table (parent + 3). Cleanup runs once
// at the $done level after $unwind closes (parent + 1). Both compile
// against the live table (shared lexical scope): the body may promote a
// binding to a runtime local that cleanup then reads/writes. Body first,
// so its promotions are visible to cleanup.
let parent_depth = emit.block_depth();
// While the body compiles, register an unwind frame so a non-local exit
// (`(return-from)` / `(go)` to a target outside this unwind-protect) emits
// this cleanup inline before its `br` — CL unwind semantics. The frame's
// `threshold` is `parent_depth`: an exit landing at depth `<= parent_depth`
// leaves the unwind-protect. Cleanup is recompiled (for effect) at each
// crossing exit; the normal/exceptional paths below emit their own copy.
let mut body_scratch = FunctionEmitter::new_seeded(parent_depth + 3);
ctx.push_unwind_frame(parent_depth, cleanup.to_vec());
let body_result = compile_for_stack(ctx, &mut body_scratch, symbols, body);
ctx.pop_unwind_frame()?;
let body_ty = body_result?;
let result_ty = if body_diverges {
WasmType::I32
} else {
body_ty
let result_local = ctx.alloc_local(result_ty)?;
let mut cleanup_scratch = FunctionEmitter::new_seeded(parent_depth + 1);
compile_for_effect(ctx, &mut cleanup_scratch, symbols, form)?;
// --- Phase 2: emit the real structure, splicing the scratches. ---
let condition_idx = ctx.condition_type_idx();
let cond_ref = ValType::Ref(RefType {
nullable: true,
heap_type: HeapType::Concrete(condition_idx),
});
let result_vt = ctx.wasm_val_type(result_ty);
let tag = ctx.nomi_error_tag();
emit.block_start_typed(BlockType::Result(result_vt)); // $done
emit.block_start_typed(BlockType::Result(cond_ref)); // $unwind
emit.try_table(
BlockType::Result(result_vt),
&[Catch::One { tag, label: 0 }],
);
emit.splice(&body_scratch.take_bytes());
if body_diverges {
// Dead tail after a body that always raises; reset to polymorphic so
// the try_table's declared result type is satisfied vacuously.
emit.unreachable();
emit.block_end(); // close try_table
// Normal completion: body result T on the stack. Stash it and push the
// null sentinel so the $unwind block yields `(ref null $nomi_condition)`
// on both edges (catch delivers the real condition).
emit.local_set(result_local);
emit.ref_null(condition_idx);
emit.block_end(); // close $unwind — both edges converge with cond-or-null
// cond = caught condition (exceptional) | null (normal). Run cleanup once,
// on both paths, then branch.
emit.local_set(cond_local);
emit.splice(&cleanup_scratch.take_bytes());
emit.local_get(cond_local);
emit.ref_is_null();
emit.if_block(BlockType::Result(result_vt));
emit.local_get(result_local); // normal: the stashed body value
emit.else_block();
emit.local_get(cond_local); // exceptional: re-raise the same condition
emit.ref_cast(condition_idx);
emit.throw(tag);
emit.block_end(); // close if
emit.block_end(); // close $done
Ok(result_ty)
/// Emit, for effect, the cleanup of every active `(unwind-protect)` an exit
/// to `target_depth` crosses (innermost-first) — the CL guarantee that a
/// non-local `(return-from)` / `(go)` runs intervening cleanups before
/// transferring control. Called from the exit-site codegen (`block.rs`,
/// `tagbody.rs`) just before the `br`, with the exit value (if any) already
/// on the stack: cleanup is stack-neutral (effect position), so it doesn't
/// disturb that value. A no-op when no unwind-protect is crossed (the common
/// case keeps its bare `br`).
pub(super) fn emit_crossing_cleanups(
target_depth: u32,
// REMOVE the crossed frames before emitting, so a `(return-from)` / `(go)`
// inside one of these cleanups sees only the OUTER frames — a cleanup
// can't re-schedule itself (which would recurse forever). Restore after,
// so sibling branches of the protected body still see the frames.
let crossed = ctx.take_unwind_frames_crossing(target_depth);
let result = emit_cleanups(ctx, emit, symbols, &crossed);
ctx.restore_unwind_frames(crossed);
result
fn emit_cleanups(
crossed: &[crate::compiler::context::UnwindFrame],
// Compile each cleanup against a CLONE of the symbol table. This exit
// path diverges (a `br` follows), so code after it in this branch is
// dead — but a SIBLING branch (the other arm of an enclosing runtime
// `if`/`cond`, compiled later against the live table) must NOT observe a
// cleanup's compile-time side effects (e.g. a `(setf x v)` on a not-yet-
// promoted `x` mutating its const value). The canonical convergence-path
// cleanup in `compile_unwind_protect_for_stack` is the one that applies
// effects to the live table; these crossing copies are emit-only.
let mut clone = symbols.clone();
for cleanup in CompileContext::unwind_frame_cleanups(crossed) {
for form in &cleanup {
compile_for_effect(ctx, emit, &mut clone, form)?;
/// Parse `(unwind-protect body cleanup...)`: a required body form followed by
/// zero or more cleanup forms.
fn parse(args: &[Expr]) -> Result<(&Expr, &[Expr])> {
args.split_first().ok_or_else(|| {
Error::Compile(format!(
"{UNWIND_PROTECT} requires a protected body form and zero or more cleanup forms"
))
})