1
//! Resolved wasm function / type indices for a `CompileContext`.
2
//!
3
//! Emit code used to resolve indices by string — `ctx.func("ratio_new")` /
4
//! `ctx.type_idx("pair")` — through a `HashMap[key]` that **panicked** on a
5
//! miss (a missing helper, or a name registered in one module mode but read in
6
//! the other, e.g. `log` before it was added to eval mode). The name set is a
7
//! CLOSED, STATIC universe registered by the context constructor, so a miss is
8
//! a compiler bug — but it must surface as a structured `Error::Compile`, never
9
//! a SIGABRT (CLAUDE.md).
10
//!
11
//! `WasmIds` resolves every emit-referenced name ONCE at the end of
12
//! construction into a typed field. Emit then reads `ctx.ids.ratio_new` — a
13
//! plain `u32`, infallible, no hashing, no panic. A forgotten field is a Rust
14
//! "missing field in initializer" compile error; the `EntityKind` resolver is an
15
//! exhaustive `match`, so a new entity variant without a mapping is a Rust
16
//! compile error too. **The missing-key panic class cannot recur.**
17
//!
18
//! Indices are per-compilation, module-internal wasm section indices — never
19
//! serialized, never crossing a process/disk/rpc boundary — so this is a pure
20
//! internal-representation change with no effect on emitted bytes or script
21
//! portability.
22

            
23
use crate::ast::EntityKind;
24
use crate::error::{Error, Result};
25

            
26
use super::CompileContext;
27

            
28
/// Wasm function indices referenced by emit code, resolved at construction.
29
/// Common helpers are present in BOTH module modes; the three `get_*` host
30
/// imports are SCRIPT-mode only (their emit sites — the entity natives — run
31
/// only on the script path), so they are `Option` and read through accessors
32
/// that return a structured error rather than panicking if read in eval mode.
33
#[derive(Debug, Clone)]
34
pub(in crate::compiler) struct WasmIds {
35
    // ratio helpers
36
    pub gcd: u32,
37
    pub ratio_new: u32,
38
    pub ratio_add: u32,
39
    pub ratio_sub: u32,
40
    pub ratio_mul: u32,
41
    pub ratio_div: u32,
42
    pub ratio_eq: u32,
43
    pub ratio_lt: u32,
44
    pub ratio_from_i64: u32,
45
    pub ratio_to_i64: u32,
46
    // unit-term helpers
47
    pub unit_singleton: u32,
48
    pub unit_mul: u32,
49
    pub unit_negate: u32,
50
    pub unit_eq: u32,
51
    pub materialize_unit: u32,
52
    // commodity helpers
53
    pub commodity_add: u32,
54
    pub commodity_sub: u32,
55
    pub commodity_mul: u32,
56
    pub commodity_div: u32,
57
    pub commodity_mul_by_ratio: u32,
58
    pub commodity_div_by_ratio: u32,
59
    pub commodity_neg: u32,
60
    pub commodity_eq: u32,
61
    pub commodity_lt: u32,
62
    pub commodity_assert_atomic: u32,
63
    pub commodity_new_with_term: u32,
64
    // pair + string
65
    pub pair_new: u32,
66
    pub box_index: u32,
67
    pub index_div: u32,
68
    pub index_rem: u32,
69
    pub box_bool: u32,
70
    pub bool_chain_to_anyref: u32,
71
    pub car_of_nil: u32,
72
    pub string_eq: u32,
73
    // boundary imports
74
    pub nomi_raise: u32, // both modes
75
    pub log: u32,        // both modes
76
    // eval-mode-only import (catch-each lowering)
77
    pub nomi_catch_each: Option<u32>,
78
    // script-mode-only env imports (entity-native emit sites)
79
    pub get_output_offset: Option<u32>,
80
    pub get_input_offset: Option<u32>,
81
    pub get_input_entities_count: Option<u32>,
82

            
83
    // types
84
    pub ty_i8_array: u32,
85
    pub ty_ratio: u32,
86
    pub ty_pair: u32,
87
    pub ty_commodity: u32,
88
    pub ty_unit_term: u32,
89
    pub ty_nomi_condition: u32,
90
    // entity struct types (indexed by EntityKind via `entity_type`)
91
    pub ty_account: u32,
92
    pub ty_commodity_entity: u32,
93
    pub ty_transaction: u32,
94
    pub ty_split: u32,
95
    pub ty_tag_entity: u32,
96
    pub ty_price: u32,
97
    pub ty_ssh_key: u32,
98
}
99

            
100
impl WasmIds {
101
    /// Pre-resolution poison. Every index is `u32::MAX` (an out-of-range wasm
102
    /// section index), so a read *before* `resolve_ids` overwrites it fails wasm
103
    /// validation loudly rather than silently emitting index 0. The two public
104
    /// constructors overwrite this unconditionally before emit can run; it only
105
    /// fills the `ids` field for the brief construction window in which the
106
    /// helper indices it would resolve don't yet exist. This is NOT a `Default`
107
    /// — it's a single named poison value, and the real completeness checkpoint
108
    /// is the all-fields literal in `resolve_ids`.
109
    pub(super) const UNRESOLVED: Self = Self {
110
        gcd: u32::MAX,
111
        ratio_new: u32::MAX,
112
        ratio_add: u32::MAX,
113
        ratio_sub: u32::MAX,
114
        ratio_mul: u32::MAX,
115
        ratio_div: u32::MAX,
116
        ratio_eq: u32::MAX,
117
        ratio_lt: u32::MAX,
118
        ratio_from_i64: u32::MAX,
119
        ratio_to_i64: u32::MAX,
120
        unit_singleton: u32::MAX,
121
        unit_mul: u32::MAX,
122
        unit_negate: u32::MAX,
123
        unit_eq: u32::MAX,
124
        materialize_unit: u32::MAX,
125
        commodity_add: u32::MAX,
126
        commodity_sub: u32::MAX,
127
        commodity_mul: u32::MAX,
128
        commodity_div: u32::MAX,
129
        commodity_mul_by_ratio: u32::MAX,
130
        commodity_div_by_ratio: u32::MAX,
131
        commodity_neg: u32::MAX,
132
        commodity_eq: u32::MAX,
133
        commodity_lt: u32::MAX,
134
        commodity_assert_atomic: u32::MAX,
135
        commodity_new_with_term: u32::MAX,
136
        pair_new: u32::MAX,
137
        box_index: u32::MAX,
138
        index_div: u32::MAX,
139
        index_rem: u32::MAX,
140
        box_bool: u32::MAX,
141
        bool_chain_to_anyref: u32::MAX,
142
        car_of_nil: u32::MAX,
143
        string_eq: u32::MAX,
144
        nomi_raise: u32::MAX,
145
        log: u32::MAX,
146
        nomi_catch_each: None,
147
        get_output_offset: None,
148
        get_input_offset: None,
149
        get_input_entities_count: None,
150
        ty_i8_array: u32::MAX,
151
        ty_ratio: u32::MAX,
152
        ty_pair: u32::MAX,
153
        ty_commodity: u32::MAX,
154
        ty_unit_term: u32::MAX,
155
        ty_nomi_condition: u32::MAX,
156
        ty_account: u32::MAX,
157
        ty_commodity_entity: u32::MAX,
158
        ty_transaction: u32::MAX,
159
        ty_split: u32::MAX,
160
        ty_tag_entity: u32::MAX,
161
        ty_price: u32::MAX,
162
        ty_ssh_key: u32::MAX,
163
    };
164

            
165
    /// The wasm struct-type index for an entity kind. Exhaustive over
166
    /// `EntityKind` — a new variant without an arm is a Rust compile error.
167
    /// `Condition` shares the `$nomi_condition` struct (exception support),
168
    /// matching `EntityKind::type_name`.
169
    #[must_use]
170
5284562
    pub fn entity_type(&self, kind: EntityKind) -> u32 {
171
5284562
        match kind {
172
755817
            EntityKind::Account => self.ty_account,
173
755565
            EntityKind::Commodity => self.ty_commodity_entity,
174
755897
            EntityKind::Transaction => self.ty_transaction,
175
755815
            EntityKind::Split => self.ty_split,
176
753822
            EntityKind::Tag => self.ty_tag_entity,
177
753822
            EntityKind::Price => self.ty_price,
178
753822
            EntityKind::SshKey => self.ty_ssh_key,
179
2
            EntityKind::Condition => self.ty_nomi_condition,
180
        }
181
5284562
    }
182

            
183
    /// Stores the wasm struct-type index for an entity kind as it registers in
184
    /// `new_skeleton`. Exhaustive over `EntityKind` (mirrors `entity_type`), so
185
    /// a new variant without an arm is a Rust compile error.
186
3340673
    pub fn set_entity_type(&mut self, kind: EntityKind, idx: u32) {
187
3340673
        let slot = match kind {
188
477239
            EntityKind::Account => &mut self.ty_account,
189
477239
            EntityKind::Commodity => &mut self.ty_commodity_entity,
190
477239
            EntityKind::Transaction => &mut self.ty_transaction,
191
477239
            EntityKind::Split => &mut self.ty_split,
192
477239
            EntityKind::Tag => &mut self.ty_tag_entity,
193
477239
            EntityKind::Price => &mut self.ty_price,
194
477239
            EntityKind::SshKey => &mut self.ty_ssh_key,
195
            EntityKind::Condition => &mut self.ty_nomi_condition,
196
        };
197
3340673
        *slot = idx;
198
3340673
    }
199

            
200
    /// The ratio comparison helper index for a comparison operator. The
201
    /// comparison dispatch only ever passes `"="` / `"<"` (closed set).
202
4731
    pub fn ratio_cmp(&self, op: &str) -> Result<u32> {
203
4731
        match op {
204
4731
            "=" => Ok(self.ratio_eq),
205
1162
            "<" => Ok(self.ratio_lt),
206
            other => Err(Error::Compile(format!(
207
                "no ratio comparison helper for operator '{other}'"
208
            ))),
209
        }
210
4731
    }
211

            
212
    /// The commodity comparison helper index for a comparison operator.
213
    pub fn commodity_cmp(&self, op: &str) -> Result<u32> {
214
        match op {
215
            "=" => Ok(self.commodity_eq),
216
            "<" => Ok(self.commodity_lt),
217
            other => Err(Error::Compile(format!(
218
                "no commodity comparison helper for operator '{other}'"
219
            ))),
220
        }
221
    }
222

            
223
    /// A mode-specific import index, or a structured error if read in a module
224
    /// mode that didn't register it (rather than panicking).
225
151662
    fn mode_import(opt: Option<u32>, name: &str) -> Result<u32> {
226
151662
        opt.ok_or_else(|| {
227
4
            Error::Compile(format!(
228
4
                "wasm import '{name}' is not registered in this module mode"
229
4
            ))
230
4
        })
231
151662
    }
232

            
233
915
    pub fn nomi_catch_each(&self) -> Result<u32> {
234
915
        Self::mode_import(self.nomi_catch_each, "__nomi_catch_each")
235
915
    }
236

            
237
102852
    pub fn get_output_offset(&self) -> Result<u32> {
238
102852
        Self::mode_import(self.get_output_offset, "get_output_offset")
239
102852
    }
240

            
241
40257
    pub fn get_input_offset(&self) -> Result<u32> {
242
40257
        Self::mode_import(self.get_input_offset, "get_input_offset")
243
40257
    }
244

            
245
7638
    pub fn get_input_entities_count(&self) -> Result<u32> {
246
7638
        Self::mode_import(self.get_input_entities_count, "get_input_entities_count")
247
7638
    }
248
}
249

            
250
impl CompileContext {
251
    /// Resolves every emit-referenced HELPER-FUNCTION name from the
252
    /// (fully-declared) `func_names` map into the function fields of `WasmIds`,
253
    /// preserving the TYPE fields already populated in `new_skeleton` (the
254
    /// type-ref accessors need those mid-skeleton, before this runs). Called
255
    /// ONCE after all `declare_*` have run. A missing common name is a
256
    /// structured `Error::Compile` (a compiler bug surfaced cleanly, not a
257
    /// panic); the mode-specific imports resolve to `None` in the other mode by
258
    /// design.
259
577567
    pub(super) fn resolve_ids(&self) -> Result<WasmIds> {
260
20792412
        let f = |name: &str| -> Result<u32> {
261
20792412
            self.func_names.get(name).copied().ok_or_else(|| {
262
                Error::Compile(format!(
263
                    "internal: wasm function '{name}' was not registered during context construction"
264
                ))
265
            })
266
20792412
        };
267
        Ok(WasmIds {
268
577567
            gcd: f("gcd")?,
269
577567
            ratio_new: f("ratio_new")?,
270
577567
            ratio_add: f("ratio_add")?,
271
577567
            ratio_sub: f("ratio_sub")?,
272
577567
            ratio_mul: f("ratio_mul")?,
273
577567
            ratio_div: f("ratio_div")?,
274
577567
            ratio_eq: f("ratio_eq")?,
275
577567
            ratio_lt: f("ratio_lt")?,
276
577567
            ratio_from_i64: f("ratio_from_i64")?,
277
577567
            ratio_to_i64: f("ratio_to_i64")?,
278
577567
            unit_singleton: f("unit_singleton")?,
279
577567
            unit_mul: f("unit_mul")?,
280
577567
            unit_negate: f("unit_negate")?,
281
577567
            unit_eq: f("unit_eq")?,
282
577567
            materialize_unit: f("materialize_unit")?,
283
577567
            commodity_add: f("commodity_add")?,
284
577567
            commodity_sub: f("commodity_sub")?,
285
577567
            commodity_mul: f("commodity_mul")?,
286
577567
            commodity_div: f("commodity_div")?,
287
577567
            commodity_mul_by_ratio: f("commodity_mul_by_ratio")?,
288
577567
            commodity_div_by_ratio: f("commodity_div_by_ratio")?,
289
577567
            commodity_neg: f("commodity_neg")?,
290
577567
            commodity_eq: f("commodity_eq")?,
291
577567
            commodity_lt: f("commodity_lt")?,
292
577567
            commodity_assert_atomic: f("commodity_assert_atomic")?,
293
577567
            commodity_new_with_term: f("commodity_new_with_term")?,
294
577567
            pair_new: f("pair_new")?,
295
577567
            box_index: f("box_index")?,
296
577567
            index_div: f("index_div")?,
297
577567
            index_rem: f("index_rem")?,
298
577567
            box_bool: f("box_bool")?,
299
577567
            bool_chain_to_anyref: f("bool_chain_to_anyref")?,
300
577567
            car_of_nil: f("car_of_nil")?,
301
577567
            string_eq: f("string_eq")?,
302
577567
            nomi_raise: f("__nomi_raise")?,
303
577567
            log: f("log")?,
304
577567
            nomi_catch_each: self.func_names.get("__nomi_catch_each").copied(),
305
577567
            get_output_offset: self.func_names.get("get_output_offset").copied(),
306
577567
            get_input_offset: self.func_names.get("get_input_offset").copied(),
307
577567
            get_input_entities_count: self.func_names.get("get_input_entities_count").copied(),
308
            // Type fields were populated in `new_skeleton` (the type-ref
309
            // accessors needed them while declaring helper signatures); carry
310
            // them through unchanged.
311
577567
            ty_i8_array: self.ids.ty_i8_array,
312
577567
            ty_ratio: self.ids.ty_ratio,
313
577567
            ty_pair: self.ids.ty_pair,
314
577567
            ty_commodity: self.ids.ty_commodity,
315
577567
            ty_unit_term: self.ids.ty_unit_term,
316
577567
            ty_nomi_condition: self.ids.ty_nomi_condition,
317
577567
            ty_account: self.ids.ty_account,
318
577567
            ty_commodity_entity: self.ids.ty_commodity_entity,
319
577567
            ty_transaction: self.ids.ty_transaction,
320
577567
            ty_split: self.ids.ty_split,
321
577567
            ty_tag_entity: self.ids.ty_tag_entity,
322
577567
            ty_price: self.ids.ty_price,
323
577567
            ty_ssh_key: self.ids.ty_ssh_key,
324
        })
325
577567
    }
326
}