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 string_eq: u32,
67
    // boundary imports
68
    pub nomi_raise: u32, // both modes
69
    pub log: u32,        // both modes
70
    // eval-mode-only import (catch-each lowering)
71
    pub nomi_catch_each: Option<u32>,
72
    // script-mode-only env imports (entity-native emit sites)
73
    pub get_output_offset: Option<u32>,
74
    pub get_input_offset: Option<u32>,
75
    pub get_input_entities_count: Option<u32>,
76

            
77
    // types
78
    pub ty_i8_array: u32,
79
    pub ty_ratio: u32,
80
    pub ty_pair: u32,
81
    pub ty_commodity: u32,
82
    pub ty_unit_term: u32,
83
    pub ty_nomi_condition: u32,
84
    // entity struct types (indexed by EntityKind via `entity_type`)
85
    pub ty_account: u32,
86
    pub ty_commodity_entity: u32,
87
    pub ty_transaction: u32,
88
    pub ty_split: u32,
89
    pub ty_tag_entity: u32,
90
    pub ty_price: u32,
91
    pub ty_ssh_key: u32,
92
}
93

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

            
153
    /// The wasm struct-type index for an entity kind. Exhaustive over
154
    /// `EntityKind` — a new variant without an arm is a Rust compile error.
155
    /// `Condition` shares the `$nomi_condition` struct (exception support),
156
    /// matching `EntityKind::type_name`.
157
    #[must_use]
158
2343826
    pub fn entity_type(&self, kind: EntityKind) -> u32 {
159
2343826
        match kind {
160
335563
            EntityKind::Account => self.ty_account,
161
335207
            EntityKind::Commodity => self.ty_commodity_entity,
162
335349
            EntityKind::Transaction => self.ty_transaction,
163
335705
            EntityKind::Split => self.ty_split,
164
334000
            EntityKind::Tag => self.ty_tag_entity,
165
334000
            EntityKind::Price => self.ty_price,
166
334000
            EntityKind::SshKey => self.ty_ssh_key,
167
2
            EntityKind::Condition => self.ty_nomi_condition,
168
        }
169
2343826
    }
170

            
171
    /// Stores the wasm struct-type index for an entity kind as it registers in
172
    /// `new_skeleton`. Exhaustive over `EntityKind` (mirrors `entity_type`), so
173
    /// a new variant without an arm is a Rust compile error.
174
1769257
    pub fn set_entity_type(&mut self, kind: EntityKind, idx: u32) {
175
1769257
        let slot = match kind {
176
252751
            EntityKind::Account => &mut self.ty_account,
177
252751
            EntityKind::Commodity => &mut self.ty_commodity_entity,
178
252751
            EntityKind::Transaction => &mut self.ty_transaction,
179
252751
            EntityKind::Split => &mut self.ty_split,
180
252751
            EntityKind::Tag => &mut self.ty_tag_entity,
181
252751
            EntityKind::Price => &mut self.ty_price,
182
252751
            EntityKind::SshKey => &mut self.ty_ssh_key,
183
            EntityKind::Condition => &mut self.ty_nomi_condition,
184
        };
185
1769257
        *slot = idx;
186
1769257
    }
187

            
188
    /// The ratio comparison helper index for a comparison operator. The
189
    /// comparison dispatch only ever passes `"="` / `"<"` (closed set).
190
4047
    pub fn ratio_cmp(&self, op: &str) -> Result<u32> {
191
4047
        match op {
192
4047
            "=" => Ok(self.ratio_eq),
193
994
            "<" => Ok(self.ratio_lt),
194
            other => Err(Error::Compile(format!(
195
                "no ratio comparison helper for operator '{other}'"
196
            ))),
197
        }
198
4047
    }
199

            
200
    /// The commodity comparison helper index for a comparison operator.
201
    pub fn commodity_cmp(&self, op: &str) -> Result<u32> {
202
        match op {
203
            "=" => Ok(self.commodity_eq),
204
            "<" => Ok(self.commodity_lt),
205
            other => Err(Error::Compile(format!(
206
                "no commodity comparison helper for operator '{other}'"
207
            ))),
208
        }
209
    }
210

            
211
    /// A mode-specific import index, or a structured error if read in a module
212
    /// mode that didn't register it (rather than panicking).
213
129454
    fn mode_import(opt: Option<u32>, name: &str) -> Result<u32> {
214
129454
        opt.ok_or_else(|| {
215
4
            Error::Compile(format!(
216
4
                "wasm import '{name}' is not registered in this module mode"
217
4
            ))
218
4
        })
219
129454
    }
220

            
221
570
    pub fn nomi_catch_each(&self) -> Result<u32> {
222
570
        Self::mode_import(self.nomi_catch_each, "__nomi_catch_each")
223
570
    }
224

            
225
87913
    pub fn get_output_offset(&self) -> Result<u32> {
226
87913
        Self::mode_import(self.get_output_offset, "get_output_offset")
227
87913
    }
228

            
229
34437
    pub fn get_input_offset(&self) -> Result<u32> {
230
34437
        Self::mode_import(self.get_input_offset, "get_input_offset")
231
34437
    }
232

            
233
6534
    pub fn get_input_entities_count(&self) -> Result<u32> {
234
6534
        Self::mode_import(self.get_input_entities_count, "get_input_entities_count")
235
6534
    }
236
}
237

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