1
//! `$nomi_condition` struct + `$nomi_error` exception tag (Tier 3,
2
//! ADR-0026).
3
//!
4
//! A script raise (`(error 'code "msg")`) lowers to `struct.new
5
//! $nomi_condition` + `throw $nomi_error`; `(handler-case)` /
6
//! `(unwind-protect)` catch it via `try_table`, and a compiler-emitted
7
//! boundary wrapper around each host-invoked body catches any uncaught
8
//! raise and bridges it to the `__nomi_raise` host fn. The tag carries a
9
//! single `(ref null $nomi_condition)` payload so a `catch` delivers the
10
//! condition ref straight to the handler block.
11

            
12
use wasm_encoder::{
13
    BlockType, Catch, Function, HeapType, Instruction, RefType, TagKind, TagType, ValType,
14
};
15

            
16
use super::CompileContext;
17
use crate::ast::WasmType;
18
use crate::compiler::emit::FunctionEmitter;
19
use crate::error::{Error, Result};
20

            
21
/// A condition's interned code + message and the indices a helper body needs
22
/// to `throw` it as a catchable `$nomi_error` while only borrowing `&self`.
23
pub(super) struct ConditionTrap {
24
    i8_array_idx: u32,
25
    condition_idx: u32,
26
    tag: u32,
27
    code_data: u32,
28
    code_len: i32,
29
    message_data: u32,
30
    message_len: i32,
31
}
32

            
33
impl ConditionTrap {
34
    /// Builds the `$nomi_condition` and `throw`s it. `throw` never returns, so
35
    /// the body's declared result is satisfied by stack-polymorphism.
36
2863428
    pub(super) fn emit_throw(&self, f: &mut Function) {
37
2863428
        f.instruction(&Instruction::I32Const(0));
38
2863428
        f.instruction(&Instruction::I32Const(self.code_len));
39
2863428
        f.instruction(&Instruction::ArrayNewData {
40
2863428
            array_type_index: self.i8_array_idx,
41
2863428
            array_data_index: self.code_data,
42
2863428
        });
43
2863428
        f.instruction(&Instruction::I32Const(0));
44
2863428
        f.instruction(&Instruction::I32Const(self.message_len));
45
2863428
        f.instruction(&Instruction::ArrayNewData {
46
2863428
            array_type_index: self.i8_array_idx,
47
2863428
            array_data_index: self.message_data,
48
2863428
        });
49
2863428
        f.instruction(&Instruction::StructNew(self.condition_idx));
50
2863428
        f.instruction(&Instruction::Throw(self.tag));
51
2863428
    }
52
}
53

            
54
impl CompileContext {
55
    /// Interns a condition's strings as passive data. `code` is the UPPER-CASE
56
    /// symbol form: handler dispatch compares it byte-for-byte against the
57
    /// reader-folded clause symbol.
58
2386190
    pub(super) fn register_condition_trap(
59
2386190
        &mut self,
60
2386190
        code: &str,
61
2386190
        message: &str,
62
2386190
    ) -> Result<ConditionTrap> {
63
2386190
        let code_data = self.add_data(code.as_bytes())?;
64
2386190
        let message_data = self.add_data(message.as_bytes())?;
65
        Ok(ConditionTrap {
66
2386190
            i8_array_idx: self.ids.ty_i8_array,
67
2386190
            condition_idx: self.condition_type_idx(),
68
2386190
            tag: self.nomi_error_tag(),
69
2386190
            code_data,
70
2386190
            code_len: data_len(code)?,
71
2386190
            message_data,
72
2386190
            message_len: data_len(message)?,
73
        })
74
2386190
    }
75
}
76

            
77
4772380
fn data_len(text: &str) -> Result<i32> {
78
4772380
    i32::try_from(text.len())
79
4772380
        .map_err(|_| Error::Compile("condition string exceeds i32 length".to_string()))
80
4772380
}
81

            
82
impl CompileContext {
83
    /// Registers the `$nomi_condition` struct (`code`, `message` —
84
    /// both `(ref $i8_array)` strings) and the `$nomi_error` tag whose
85
    /// payload is `(ref null $nomi_condition)`. Records the tag index in
86
    /// `nomi_error_tag`. Called once from `new_skeleton`.
87
477239
    pub(super) fn register_exception_support(&mut self) -> Result<()> {
88
477239
        let string = self.string_ref();
89
477239
        let condition_idx = self.register_struct_type(&[string, string])?;
90
477239
        self.ids.ty_nomi_condition = condition_idx;
91
477239
        let payload = ValType::Ref(RefType {
92
477239
            nullable: true,
93
477239
            heap_type: HeapType::Concrete(condition_idx),
94
477239
        });
95
477239
        let sig = self.get_or_create_func_type(&[payload], &[])?;
96
477239
        self.tags.tag(TagType {
97
477239
            kind: TagKind::Exception,
98
477239
            func_type_idx: sig,
99
477239
        });
100
477239
        let tag_idx = self.tag_count;
101
477239
        self.tag_count = tag_idx
102
477239
            .checked_add(1)
103
477239
            .ok_or_else(|| crate::error::Error::Compile("wasm tag index space exhausted".into()))?;
104
477239
        self.nomi_error_tag = Some(tag_idx);
105
477239
        Ok(())
106
477239
    }
107

            
108
    /// Index of the `$nomi_error` exception tag. Panics only if called
109
    /// before `register_exception_support` (a compiler-internal ordering
110
    /// bug, not a script-reachable condition).
111
3834866
    pub(in crate::compiler) fn nomi_error_tag(&self) -> u32 {
112
3834866
        self.nomi_error_tag
113
3834866
            .expect("nomi_error tag registered in new_skeleton")
114
3834866
    }
115

            
116
    /// Type index of the `$nomi_condition` struct.
117
3834866
    pub(in crate::compiler) fn condition_type_idx(&self) -> u32 {
118
3834866
        self.ids.ty_nomi_condition
119
3834866
    }
120

            
121
    /// Wraps a host-invoked body in the Tier 3 boundary `try_table` that
122
    /// catches an uncaught `$nomi_error`, reads its `code`/`message`, and
123
    /// bridges to `__nomi_raise` (ADR-0026). `body` emits the actual
124
    /// function body; it runs inside all three wrapper frames, so any
125
    /// `(return-from)` / `(go)` inside it sees the correct depth (the
126
    /// `try_table` helper bumps `block_depth`).
127
    ///
128
    /// Three nested frames (spike-validated, `wasm_exceptions_spike.rs`):
129
    /// ```text
130
    /// block $exit (result T?)
131
    ///   block $handler (result (ref null $nomi_condition))
132
    ///     try_table (result T?) (catch $nomi_error → $handler)
133
    ///       <body>
134
    ///     end
135
    ///     br $exit          ;; normal completion: carry T out, skip handler
136
    ///   end                 ;; catch lands here, condition ref on the stack
137
    ///   <code = struct.get 0; msg = struct.get 1; call __nomi_raise; unreachable>
138
    /// end
139
    /// ```
140
    /// `result_ty` is the body's result: `None` for `process` (void), `Some`
141
    /// for `should_apply` (i32) and `nomi-eval` (anyref). The void form
142
    /// drops the `(result T)` arity from `$exit` and the `try_table`.
143
480173
    pub(in crate::compiler) fn emit_boundary_wrapper(
144
480173
        &mut self,
145
480173
        emit: &mut FunctionEmitter,
146
480173
        result_ty: Option<WasmType>,
147
480173
        body: impl FnOnce(&mut Self, &mut FunctionEmitter) -> Result<()>,
148
480173
    ) -> Result<()> {
149
480173
        let condition_idx = self.condition_type_idx();
150
480173
        let cond_ref = ValType::Ref(RefType {
151
480173
            nullable: true,
152
480173
            heap_type: HeapType::Concrete(condition_idx),
153
480173
        });
154
480173
        let tag = self.nomi_error_tag();
155
480173
        let raise = self.ids.nomi_raise;
156
        // Scratch local holding the caught condition while the handler
157
        // reads both fields. Declared `anyref` (no `WasmType` variant for
158
        // a bare struct type); the handler `ref.cast`s back to
159
        // `$nomi_condition` before each `struct.get`. Allocated before the
160
        // body so the body's own locals stack above it.
161
480173
        let cond_local = self.alloc_local(WasmType::AnyRef)?;
162

            
163
480173
        match result_ty {
164
379896
            Some(ty) => {
165
379896
                let vt = self.wasm_val_type(ty);
166
379896
                emit.block_start_typed(BlockType::Result(vt));
167
379896
            }
168
100277
            None => emit.block_start(),
169
        }
170
480173
        emit.block_start_typed(BlockType::Result(cond_ref));
171
480173
        let try_ty = match result_ty {
172
379896
            Some(ty) => BlockType::Result(self.wasm_val_type(ty)),
173
100277
            None => BlockType::Empty,
174
        };
175
        // catch $nomi_error → `$handler` (the block immediately enclosing
176
        // the try_table, relative depth 0).
177
480173
        emit.try_table(try_ty, &[Catch::One { tag, label: 0 }]);
178

            
179
480173
        body(self, emit)?;
180

            
181
465064
        emit.block_end(); // close try_table
182
        // Normal completion: branch out to `$exit` (now relative depth 1),
183
        // carrying the body result past the handler tail.
184
465064
        emit.br(1);
185
465064
        emit.block_end(); // close $handler — the catch edge lands here
186

            
187
        // Handler: caught `(ref $nomi_condition)` on the stack. Stash it
188
        // (as anyref), then read both fields back via `ref.cast` and
189
        // bridge to `__nomi_raise(code, message)`; the call never returns.
190
465064
        emit.local_set(cond_local);
191
465064
        emit.local_get(cond_local);
192
465064
        emit.ref_cast(condition_idx);
193
465064
        emit.struct_get(condition_idx, 0); // code
194
465064
        emit.local_get(cond_local);
195
465064
        emit.ref_cast(condition_idx);
196
465064
        emit.struct_get(condition_idx, 1); // message
197
465064
        emit.call(raise);
198
465064
        emit.unreachable();
199

            
200
465064
        emit.block_end(); // close $exit
201
465064
        Ok(())
202
480173
    }
203
}