Lines
100 %
Functions
42.5 %
Branches
//! `$nomi_condition` struct + `$nomi_error` exception tag (Tier 3,
//! ADR-0026).
//!
//! A script raise (`(error 'code "msg")`) lowers to `struct.new
//! $nomi_condition` + `throw $nomi_error`; `(handler-case)` /
//! `(unwind-protect)` catch it via `try_table`, and a compiler-emitted
//! boundary wrapper around each host-invoked body catches any uncaught
//! raise and bridges it to the `__nomi_raise` host fn. The tag carries a
//! single `(ref null $nomi_condition)` payload so a `catch` delivers the
//! condition ref straight to the handler block.
use wasm_encoder::{BlockType, Catch, HeapType, RefType, TagKind, TagType, ValType};
use super::CompileContext;
use crate::ast::WasmType;
use crate::compiler::emit::FunctionEmitter;
use crate::error::Result;
impl CompileContext {
/// Registers the `$nomi_condition` struct (`code`, `message` —
/// both `(ref $i8_array)` strings) and the `$nomi_error` tag whose
/// payload is `(ref null $nomi_condition)`. Records the tag index in
/// `nomi_error_tag`. Called once from `new_skeleton`.
pub(super) fn register_exception_support(&mut self) -> Result<()> {
let string = self.string_ref();
let condition_idx = self.register_struct_type(&[string, string])?;
self.ids.ty_nomi_condition = condition_idx;
let payload = ValType::Ref(RefType {
nullable: true,
heap_type: HeapType::Concrete(condition_idx),
});
let sig = self.get_or_create_func_type(&[payload], &[])?;
self.tags.tag(TagType {
kind: TagKind::Exception,
func_type_idx: sig,
let tag_idx = self.tag_count;
self.tag_count = tag_idx
.checked_add(1)
.ok_or_else(|| crate::error::Error::Compile("wasm tag index space exhausted".into()))?;
self.nomi_error_tag = Some(tag_idx);
Ok(())
}
/// Index of the `$nomi_error` exception tag. Panics only if called
/// before `register_exception_support` (a compiler-internal ordering
/// bug, not a script-reachable condition).
pub(in crate::compiler) fn nomi_error_tag(&self) -> u32 {
self.nomi_error_tag
.expect("nomi_error tag registered in new_skeleton")
/// Type index of the `$nomi_condition` struct.
pub(in crate::compiler) fn condition_type_idx(&self) -> u32 {
self.ids.ty_nomi_condition
/// Wraps a host-invoked body in the Tier 3 boundary `try_table` that
/// catches an uncaught `$nomi_error`, reads its `code`/`message`, and
/// bridges to `__nomi_raise` (ADR-0026). `body` emits the actual
/// function body; it runs inside all three wrapper frames, so any
/// `(return-from)` / `(go)` inside it sees the correct depth (the
/// `try_table` helper bumps `block_depth`).
///
/// Three nested frames (spike-validated, `wasm_exceptions_spike.rs`):
/// ```text
/// block $exit (result T?)
/// block $handler (result (ref null $nomi_condition))
/// try_table (result T?) (catch $nomi_error → $handler)
/// <body>
/// end
/// br $exit ;; normal completion: carry T out, skip handler
/// end ;; catch lands here, condition ref on the stack
/// <code = struct.get 0; msg = struct.get 1; call __nomi_raise; unreachable>
/// ```
/// `result_ty` is the body's result: `None` for `process` (void), `Some`
/// for `should_apply` (i32) and `nomi-eval` (anyref). The void form
/// drops the `(result T)` arity from `$exit` and the `try_table`.
pub(in crate::compiler) fn emit_boundary_wrapper(
&mut self,
emit: &mut FunctionEmitter,
result_ty: Option<WasmType>,
body: impl FnOnce(&mut Self, &mut FunctionEmitter) -> Result<()>,
) -> Result<()> {
let condition_idx = self.condition_type_idx();
let cond_ref = ValType::Ref(RefType {
let tag = self.nomi_error_tag();
let raise = self.ids.nomi_raise;
// Scratch local holding the caught condition while the handler
// reads both fields. Declared `anyref` (no `WasmType` variant for
// a bare struct type); the handler `ref.cast`s back to
// `$nomi_condition` before each `struct.get`. Allocated before the
// body so the body's own locals stack above it.
let cond_local = self.alloc_local(WasmType::AnyRef)?;
match result_ty {
Some(ty) => {
let vt = self.wasm_val_type(ty);
emit.block_start_typed(BlockType::Result(vt));
None => emit.block_start(),
emit.block_start_typed(BlockType::Result(cond_ref));
let try_ty = match result_ty {
Some(ty) => BlockType::Result(self.wasm_val_type(ty)),
None => BlockType::Empty,
};
// catch $nomi_error → `$handler` (the block immediately enclosing
// the try_table, relative depth 0).
emit.try_table(try_ty, &[Catch::One { tag, label: 0 }]);
body(self, emit)?;
emit.block_end(); // close try_table
// Normal completion: branch out to `$exit` (now relative depth 1),
// carrying the body result past the handler tail.
emit.br(1);
emit.block_end(); // close $handler — the catch edge lands here
// Handler: caught `(ref $nomi_condition)` on the stack. Stash it
// (as anyref), then read both fields back via `ref.cast` and
// bridge to `__nomi_raise(code, message)`; the call never returns.
emit.local_set(cond_local);
emit.local_get(cond_local);
emit.ref_cast(condition_idx);
emit.struct_get(condition_idx, 0); // code
emit.struct_get(condition_idx, 1); // message
emit.call(raise);
emit.unreachable();
emit.block_end(); // close $exit