1
//! `(handler-case)` — pattern-match recovery from a raised condition
2
//! (Tier 3.3, ADR-0026).
3
//!
4
//! ```lisp
5
//! (handler-case <body>
6
//!   (no-such-account (e) (log (error-message e)))
7
//!   (commodity-mismatch (e) (recover))
8
//!   (t (e) (error-code e)))            ; t = catch-all (optional, last)
9
//! ```
10
//!
11
//! Lowering (single-value `Catch::One`, the same shape as the Tier 3.2
12
//! boundary wrapper):
13
//! ```text
14
//! block $outer (result T)
15
//!   block $handler (result (ref null $nomi_condition))
16
//!     try_table (result T) (catch $nomi_error → $handler)
17
//!       <body → T>
18
//!     end
19
//!     br $outer                       ; normal completion carries T out
20
//!   end                               ; catch edge: condition ref on stack
21
//!   local.set $cond
22
//!   <flat dispatch>                   ; each clause an independent `if` that
23
//!                                     ; `br $outer`s on match; t = uncond.;
24
//!                                     ; no match → re-throw same condition
25
//! end
26
//! ```
27
//!
28
//! Result type `T` is discovered at emit time and unified across the body
29
//! and every reachable clause body (mirrors BLOCK's emit-time discovery);
30
//! a diverging arm contributes no type. Re-raise on no-match re-`throw`s a
31
//! fresh `$nomi_error` carrying the SAME condition struct — nomiscript
32
//! conditions have no observable identity, so this is equivalent to
33
//! re-raising the original and avoids needing an exnref.
34

            
35
use wasm_encoder::{BlockType, Catch, HeapType, RefType, ValType};
36

            
37
use crate::ast::{EntityKind, Expr, WasmType};
38
use crate::compiler::context::CompileContext;
39
use crate::compiler::emit::FunctionEmitter;
40
use crate::compiler::expr::{compile_for_stack, eval_value, serialize_stack_to_output};
41
use crate::error::{Error, Result};
42
use crate::runtime::{Symbol, SymbolKind, SymbolTable};
43

            
44
use super::block_exits::form_diverges;
45

            
46
const HANDLER_CASE: &str = "handler-case";
47
const CATCH_ALL: &str = "T";
48

            
49
/// One parsed clause: `code` is `None` for the `t` catch-all, else the
50
/// (reader-upcased) condition-code symbol; `var` is the condition binding;
51
/// `body` is the clause's body forms.
52
struct Clause<'a> {
53
    code: Option<&'a str>,
54
    var: &'a str,
55
    body: &'a [Expr],
56
}
57

            
58
136
pub(super) fn eval_handler_case(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
59
136
    let (body, clauses) = parse(args)?;
60
    // If the body can't throw and folds to a constant, that's the value.
61
136
    let body_val = eval_value(symbols, body)?;
62
136
    let body_diverges = form_diverges(&mut symbols.clone(), body)?;
63
136
    if !body_val.is_wasm_runtime() && !body_diverges {
64
        return Ok(body_val);
65
136
    }
66
    // Otherwise the form has a runtime result. Its type must match what
67
    // `compile_handler_case_for_stack` emits: the unified type of the body
68
    // (when it can fall through) plus every reachable clause body — NOT just
69
    // the body, which is wrong when the body always throws (e.g. an `(error)`
70
    // body whose value comes from a clause). Callers that materialise a
71
    // runtime value (let/defvar binding, lambda arg) trust this type to size
72
    // the local, so a body-only type yields an invalid module.
73
136
    let mut exit_types: Vec<WasmType> = Vec::new();
74
136
    if !body_diverges && let Some(ty) = runtime_type(&body_val) {
75
        exit_types.push(ty);
76
136
    }
77
136
    for clause in &clauses {
78
136
        let mut clause_syms = symbols.clone();
79
136
        clause_syms.define(Symbol::new(clause.var, SymbolKind::Variable).with_value(
80
136
            Expr::WasmLocal(0, WasmType::EntityRef(EntityKind::Condition)),
81
        ));
82
136
        let (cval, cdiverges) = eval_clause_value(&mut clause_syms, clause)?;
83
136
        if !cdiverges && let Some(ty) = runtime_type(&cval) {
84
136
            exit_types.push(ty);
85
136
        }
86
    }
87
136
    Ok(Expr::WasmRuntime(unify_handler_types(&exit_types)?))
88
136
}
89

            
90
/// Eval-time value + divergence of a clause body, mirroring the
91
/// compile-side `compile_clause_body` walk (last form is the value; an
92
/// earlier diverging form makes the rest dead).
93
136
fn eval_clause_value(symbols: &mut SymbolTable, clause: &Clause<'_>) -> Result<(Expr, bool)> {
94
136
    if clause.body.is_empty() {
95
        return Ok((Expr::Nil, false));
96
136
    }
97
136
    let last = clause.body.len() - 1;
98
136
    for (idx, form) in clause.body.iter().enumerate() {
99
136
        if idx == last {
100
136
            let diverges = form_diverges(&mut symbols.clone(), form)?;
101
136
            return Ok((eval_value(symbols, form)?, diverges));
102
        }
103
        if form_diverges(&mut symbols.clone(), form)? {
104
            eval_value(symbols, form)?;
105
            return Ok((Expr::Nil, true));
106
        }
107
        eval_value(symbols, form)?;
108
    }
109
    Ok((Expr::Nil, false))
110
136
}
111

            
112
/// The `WasmType` an eval result would carry on the stack (`None` for a form
113
/// with no directly-classifiable stack value), via the shared
114
/// [`crate::compiler::expr::classify_stack_type`] — so a handler/body arm
115
/// typed here can't drift from what `compile_for_stack` emits.
116
136
fn runtime_type(val: &Expr) -> Option<WasmType> {
117
136
    crate::compiler::expr::classify_stack_type(val)
118
136
}
119

            
120
1700
pub(super) fn compile_handler_case(
121
1700
    ctx: &mut CompileContext,
122
1700
    emit: &mut FunctionEmitter,
123
1700
    symbols: &mut SymbolTable,
124
1700
    args: &[Expr],
125
1700
) -> Result<()> {
126
1700
    let ty = compile_handler_case_for_stack(ctx, emit, symbols, args)?;
127
1496
    serialize_stack_to_output(ctx, emit, ty)
128
1700
}
129

            
130
204
pub(super) fn compile_handler_case_for_effect(
131
204
    ctx: &mut CompileContext,
132
204
    emit: &mut FunctionEmitter,
133
204
    symbols: &mut SymbolTable,
134
204
    args: &[Expr],
135
204
) -> Result<()> {
136
204
    compile_handler_case_for_stack(ctx, emit, symbols, args)?;
137
204
    emit.drop_value();
138
204
    Ok(())
139
204
}
140

            
141
2312
pub(super) fn compile_handler_case_for_stack(
142
2312
    ctx: &mut CompileContext,
143
2312
    emit: &mut FunctionEmitter,
144
2312
    symbols: &mut SymbolTable,
145
2312
    args: &[Expr],
146
2312
) -> Result<WasmType> {
147
2312
    let (body, clauses) = parse(args)?;
148

            
149
    // Stash local for the caught condition, allocated before the body so the
150
    // body's own locals stack above it (matches the boundary wrapper).
151
2176
    let cond_local = ctx.alloc_local(WasmType::AnyRef)?;
152

            
153
    // --- Phase 1: compile every arm into its own scratch, collect types. ---
154
    // Each scratch is seeded at the depth it will actually be spliced into,
155
    // so relative `br` targets of a `(return-from)` / `(go)` inside the arm
156
    // stay correct after splicing:
157
    //   - body: inside $outer/$handler/try_table → parent + 3.
158
    //   - a non-`t` clause: inside $outer + its own guard `if` → parent + 2.
159
    //   - the `t` clause: inside $outer only (no guard `if`) → parent + 1.
160
2176
    let parent_depth = emit.block_depth();
161
2176
    let body_depth = parent_depth + 3;
162

            
163
2176
    let mut body_scratch = FunctionEmitter::new_seeded(body_depth);
164
2176
    let body_diverges = form_diverges(&mut symbols.clone(), body)?;
165
2176
    let body_ty = compile_for_stack(ctx, &mut body_scratch, symbols, body)?;
166

            
167
2176
    let mut exit_types: Vec<WasmType> = Vec::new();
168
2176
    if !body_diverges {
169
408
        exit_types.push(body_ty);
170
1768
    }
171

            
172
2176
    let mut clause_scratches: Vec<(FunctionEmitter, bool)> = Vec::with_capacity(clauses.len());
173
2244
    for clause in &clauses {
174
2244
        let clause_depth = if clause.code.is_some() {
175
1836
            parent_depth + 2
176
        } else {
177
408
            parent_depth + 1
178
        };
179
2244
        let mut scratch = FunctionEmitter::new_seeded(clause_depth);
180
2244
        let mut clause_syms = symbols.clone();
181
2244
        clause_syms.define(Symbol::new(clause.var, SymbolKind::Variable).with_value(
182
2244
            Expr::WasmLocal(cond_local, WasmType::EntityRef(EntityKind::Condition)),
183
        ));
184
2244
        let (cty, cdiverges) = compile_clause_body(ctx, &mut scratch, &mut clause_syms, clause)?;
185
2244
        if !cdiverges {
186
2108
            exit_types.push(cty);
187
2108
        }
188
2244
        clause_scratches.push((scratch, cdiverges));
189
    }
190

            
191
2176
    let result_ty = unify_handler_types(&exit_types)?;
192

            
193
    // --- Phase 2: emit the real structure, splicing the scratch bodies. ---
194
2108
    let cond_ref = ValType::Ref(RefType {
195
2108
        nullable: true,
196
2108
        heap_type: HeapType::Concrete(ctx.condition_type_idx()),
197
2108
    });
198
2108
    let result_vt = ctx.wasm_val_type(result_ty);
199
2108
    let tag = ctx.nomi_error_tag();
200

            
201
2108
    emit.block_start_typed(BlockType::Result(result_vt)); // $outer
202
2108
    emit.block_start_typed(BlockType::Result(cond_ref)); // $handler
203
2108
    emit.try_table(
204
2108
        BlockType::Result(result_vt),
205
2108
        &[Catch::One { tag, label: 0 }],
206
    );
207
2108
    emit.splice(&body_scratch.take_bytes());
208
2108
    if body_diverges {
209
1768
        // Dead tail value after a diverging body; reset to polymorphic so
210
1768
        // the try_table's declared result type is satisfied vacuously.
211
1768
        emit.unreachable();
212
1768
    }
213
2108
    emit.block_end(); // close try_table
214
2108
    emit.br(1); // normal completion → $outer, skip handler
215
2108
    emit.block_end(); // close $handler — catch lands here, condition on stack
216

            
217
2108
    emit.local_set(cond_local);
218
2108
    emit_dispatch(
219
2108
        ctx,
220
2108
        emit,
221
2108
        &clauses,
222
2108
        &clause_scratches,
223
2108
        cond_local,
224
2108
        result_ty,
225
    )?;
226
2108
    emit.block_end(); // close $outer
227
2108
    Ok(result_ty)
228
2312
}
229

            
230
/// Compile a clause body (already has `e` bound in `symbols`) into `emit`,
231
/// returning its result type and whether it diverges. A clause with an
232
/// empty body yields `nil` (i32 0).
233
2244
fn compile_clause_body(
234
2244
    ctx: &mut CompileContext,
235
2244
    emit: &mut FunctionEmitter,
236
2244
    symbols: &mut SymbolTable,
237
2244
    clause: &Clause<'_>,
238
2244
) -> Result<(WasmType, bool)> {
239
2244
    if clause.body.is_empty() {
240
        // Empty clause body ≡ nil — falsy i31, typed `Bool` so it serializes
241
        // as Nil and unifies homogeneously with other Bool-typed arms.
242
        emit.i32_const(0);
243
        return Ok((WasmType::Bool, false));
244
2244
    }
245
2244
    let last = clause.body.len() - 1;
246
2244
    let mut diverges = false;
247
2244
    for (idx, form) in clause.body.iter().enumerate() {
248
2244
        if idx == last {
249
2244
            diverges = form_diverges(&mut symbols.clone(), form)?;
250
2244
            let ty = compile_for_stack(ctx, emit, symbols, form)?;
251
2244
            return Ok((ty, diverges));
252
        }
253
        if form_diverges(&mut symbols.clone(), form)? {
254
            crate::compiler::expr::compile_for_effect(ctx, emit, symbols, form)?;
255
            return Ok((WasmType::I32, true));
256
        }
257
        crate::compiler::expr::compile_for_effect(ctx, emit, symbols, form)?;
258
    }
259
    Ok((WasmType::I32, diverges))
260
2244
}
261

            
262
/// Emit the flat dispatch at depth `parent+1` (only `$outer` open). Each
263
/// non-`t` clause is an independent `if` (Empty type) whose body, on match,
264
/// runs the spliced clause and `br $outer`s — so every clause body sits at
265
/// the same depth. A `t` clause runs unconditionally. With no `t`, a
266
/// fall-through re-throws the same condition.
267
2108
fn emit_dispatch(
268
2108
    ctx: &mut CompileContext,
269
2108
    emit: &mut FunctionEmitter,
270
2108
    clauses: &[Clause<'_>],
271
2108
    scratches: &[(FunctionEmitter, bool)],
272
2108
    cond_local: u32,
273
2108
    result_ty: WasmType,
274
2108
) -> Result<()> {
275
2108
    let condition_idx = ctx.condition_type_idx();
276
2108
    let string_eq = ctx.ids.string_eq;
277
2108
    let tag = ctx.nomi_error_tag();
278

            
279
2176
    for (clause, (scratch, diverges)) in clauses.iter().zip(scratches) {
280
2176
        match clause.code {
281
1768
            Some(code) => {
282
                // condition.code == "CODE" ?  → guard an Empty `if`.
283
1768
                emit.local_get(cond_local);
284
1768
                emit.ref_cast(condition_idx);
285
1768
                emit.struct_get(condition_idx, 0);
286
1768
                push_string_literal(ctx, emit, code)?;
287
1768
                emit.call(string_eq);
288
1768
                emit.if_block(BlockType::Empty);
289
1768
                emit.splice(scratch.bytes_ref());
290
                // The clause value (if it didn't diverge) is on the stack;
291
                // carry it out to `$outer` (relative depth 1 from inside the
292
                // `if`). A diverging clause already left a polymorphic stack.
293
1768
                if !*diverges {
294
1700
                    emit.br(1);
295
1700
                }
296
1768
                emit.block_end();
297
            }
298
            None => {
299
                // `t` catch-all (last clause): runs unconditionally at
300
                // `$outer`'s depth. Its value falls through to `$outer`'s
301
                // end — no `br` needed. A diverging `t` body left a
302
                // polymorphic stack, also fine.
303
408
                emit.splice(scratch.bytes_ref());
304
408
                let _ = (diverges, result_ty);
305
408
                return Ok(());
306
            }
307
        }
308
    }
309

            
310
    // No `t` and nothing matched: re-throw a fresh $nomi_error carrying the
311
    // same condition (no observable identity loss). The condition is stashed
312
    // as `anyref`, so cast back to the concrete `$nomi_condition` (the tag
313
    // param type) before throwing. `throw` is stack-polymorphic, satisfying
314
    // `$outer`'s declared result type.
315
1700
    emit.local_get(cond_local);
316
1700
    emit.ref_cast(condition_idx);
317
1700
    emit.throw(tag);
318
1700
    Ok(())
319
2108
}
320

            
321
/// Unify reachable arm types into the handler-case result type. The body
322
/// (if it falls through) and each non-diverging clause must agree. Empty
323
/// (all arms diverge) → I32 (wasm polymorphism).
324
2312
fn unify_handler_types(exit_types: &[WasmType]) -> Result<WasmType> {
325
2312
    let mut chosen: Option<WasmType> = None;
326
2652
    for &ty in exit_types {
327
544
        match chosen {
328
544
            Some(existing) if existing != ty => {
329
68
                return Err(Error::Compile(format!(
330
68
                    "HANDLER-CASE: conflicting result types {existing} and {ty}"
331
68
                )));
332
            }
333
476
            Some(_) => {}
334
2108
            None => chosen = Some(ty),
335
        }
336
    }
337
2244
    Ok(chosen.unwrap_or(WasmType::I32))
338
2312
}
339

            
340
1768
fn push_string_literal(
341
1768
    ctx: &mut CompileContext,
342
1768
    emit: &mut FunctionEmitter,
343
1768
    value: &str,
344
1768
) -> Result<()> {
345
1768
    let data_idx = ctx.add_data(value.as_bytes())?;
346
1768
    emit.i32_const(0);
347
1768
    emit.i32_const(value.len() as i32);
348
1768
    emit.array_new_data(ctx.ids.ty_i8_array, data_idx);
349
1768
    Ok(())
350
1768
}
351

            
352
/// Parse `(handler-case body clause...)`. Requires ≥1 body form; clauses
353
/// follow. Each clause is `(code-sym (var) body...)`; `t` is the catch-all
354
/// and, if present, must be the last clause.
355
2448
fn parse(args: &[Expr]) -> Result<(&Expr, Vec<Clause<'_>>)> {
356
2448
    let (body, clause_exprs) = args.split_first().ok_or_else(|| {
357
        Error::Compile(format!(
358
            "{HANDLER_CASE} requires a body and zero or more clauses"
359
        ))
360
    })?;
361
2448
    let mut clauses = Vec::with_capacity(clause_exprs.len());
362
2516
    for (i, clause) in clause_exprs.iter().enumerate() {
363
2516
        let elems = clause.as_list().ok_or_else(|| {
364
            Error::Compile(format!(
365
                "{HANDLER_CASE}: clause must be a list, got {clause:?}"
366
            ))
367
        })?;
368
2516
        if elems.len() < 2 {
369
            return Err(Error::Compile(format!(
370
                "{HANDLER_CASE}: clause needs a code symbol, a (var), and a body"
371
            )));
372
2516
        }
373
2516
        let code = match &elems[0] {
374
            // `t` reads as the boolean literal, not a symbol — it's the
375
            // catch-all marker. A symbol head is a condition code.
376
476
            Expr::Bool(true) => None,
377
2040
            Expr::Symbol(s) if s == CATCH_ALL => None,
378
2040
            Expr::Symbol(s) => Some(s.as_str()),
379
            other => {
380
                return Err(Error::Compile(format!(
381
                    "{HANDLER_CASE}: clause head must be a code symbol or t, got {other:?}"
382
                )));
383
            }
384
        };
385
2516
        if code.is_none() && i + 1 != clause_exprs.len() {
386
68
            return Err(Error::Compile(format!(
387
68
                "{HANDLER_CASE}: the t catch-all clause must be last"
388
68
            )));
389
2448
        }
390
2448
        let var = match elems[1].as_list() {
391
2380
            Some([Expr::Symbol(v)]) => v.as_str(),
392
            _ => {
393
68
                return Err(Error::Compile(format!(
394
68
                    "{HANDLER_CASE}: clause binding must be a single (var), got {:?}",
395
68
                    elems[1]
396
68
                )));
397
            }
398
        };
399
2380
        clauses.push(Clause {
400
2380
            code,
401
2380
            var,
402
2380
            body: &elems[2..],
403
2380
        });
404
    }
405
2312
    Ok((body, clauses))
406
2448
}