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
    pub ty_report_node: u32,
93
}
94

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

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

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

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

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

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

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

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

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

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

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