Lines
86.32 %
Functions
43.08 %
Branches
100 %
//! Resolved wasm function / type indices for a `CompileContext`.
//!
//! Emit code used to resolve indices by string — `ctx.func("ratio_new")` /
//! `ctx.type_idx("pair")` — through a `HashMap[key]` that **panicked** on a
//! miss (a missing helper, or a name registered in one module mode but read in
//! the other, e.g. `log` before it was added to eval mode). The name set is a
//! CLOSED, STATIC universe registered by the context constructor, so a miss is
//! a compiler bug — but it must surface as a structured `Error::Compile`, never
//! a SIGABRT (CLAUDE.md).
//! `WasmIds` resolves every emit-referenced name ONCE at the end of
//! construction into a typed field. Emit then reads `ctx.ids.ratio_new` — a
//! plain `u32`, infallible, no hashing, no panic. A forgotten field is a Rust
//! "missing field in initializer" compile error; the `EntityKind` resolver is an
//! exhaustive `match`, so a new entity variant without a mapping is a Rust
//! compile error too. **The missing-key panic class cannot recur.**
//! Indices are per-compilation, module-internal wasm section indices — never
//! serialized, never crossing a process/disk/rpc boundary — so this is a pure
//! internal-representation change with no effect on emitted bytes or script
//! portability.
use crate::ast::EntityKind;
use crate::error::{Error, Result};
use super::CompileContext;
/// Wasm function indices referenced by emit code, resolved at construction.
/// Common helpers are present in BOTH module modes; the three `get_*` host
/// imports are SCRIPT-mode only (their emit sites — the entity natives — run
/// only on the script path), so they are `Option` and read through accessors
/// that return a structured error rather than panicking if read in eval mode.
#[derive(Debug, Clone)]
pub(in crate::compiler) struct WasmIds {
// ratio helpers
pub gcd: u32,
pub ratio_new: u32,
pub ratio_add: u32,
pub ratio_sub: u32,
pub ratio_mul: u32,
pub ratio_div: u32,
pub ratio_eq: u32,
pub ratio_lt: u32,
pub ratio_from_i64: u32,
pub ratio_to_i64: u32,
// unit-term helpers
pub unit_singleton: u32,
pub unit_mul: u32,
pub unit_negate: u32,
pub unit_eq: u32,
pub materialize_unit: u32,
// commodity helpers
pub commodity_add: u32,
pub commodity_sub: u32,
pub commodity_mul: u32,
pub commodity_div: u32,
pub commodity_mul_by_ratio: u32,
pub commodity_div_by_ratio: u32,
pub commodity_neg: u32,
pub commodity_eq: u32,
pub commodity_lt: u32,
pub commodity_assert_atomic: u32,
pub commodity_new_with_term: u32,
// pair + string
pub pair_new: u32,
pub string_eq: u32,
// boundary imports
pub nomi_raise: u32, // both modes
pub log: u32, // both modes
// eval-mode-only import (catch-each lowering)
pub nomi_catch_each: Option<u32>,
// script-mode-only env imports (entity-native emit sites)
pub get_output_offset: Option<u32>,
pub get_input_offset: Option<u32>,
pub get_input_entities_count: Option<u32>,
// types
pub ty_i8_array: u32,
pub ty_ratio: u32,
pub ty_pair: u32,
pub ty_commodity: u32,
pub ty_unit_term: u32,
pub ty_nomi_condition: u32,
// entity struct types (indexed by EntityKind via `entity_type`)
pub ty_account: u32,
pub ty_commodity_entity: u32,
pub ty_transaction: u32,
pub ty_split: u32,
pub ty_tag_entity: u32,
pub ty_price: u32,
pub ty_ssh_key: u32,
pub ty_report_node: u32,
}
impl WasmIds {
/// Pre-resolution poison. Every index is `u32::MAX` (an out-of-range wasm
/// section index), so a read *before* `resolve_ids` overwrites it fails wasm
/// validation loudly rather than silently emitting index 0. The two public
/// constructors overwrite this unconditionally before emit can run; it only
/// fills the `ids` field for the brief construction window in which the
/// helper indices it would resolve don't yet exist. This is NOT a `Default`
/// — it's a single named poison value, and the real completeness checkpoint
/// is the all-fields literal in `resolve_ids`.
pub(super) const UNRESOLVED: Self = Self {
gcd: u32::MAX,
ratio_new: u32::MAX,
ratio_add: u32::MAX,
ratio_sub: u32::MAX,
ratio_mul: u32::MAX,
ratio_div: u32::MAX,
ratio_eq: u32::MAX,
ratio_lt: u32::MAX,
ratio_from_i64: u32::MAX,
ratio_to_i64: u32::MAX,
unit_singleton: u32::MAX,
unit_mul: u32::MAX,
unit_negate: u32::MAX,
unit_eq: u32::MAX,
materialize_unit: u32::MAX,
commodity_add: u32::MAX,
commodity_sub: u32::MAX,
commodity_mul: u32::MAX,
commodity_div: u32::MAX,
commodity_mul_by_ratio: u32::MAX,
commodity_div_by_ratio: u32::MAX,
commodity_neg: u32::MAX,
commodity_eq: u32::MAX,
commodity_lt: u32::MAX,
commodity_assert_atomic: u32::MAX,
commodity_new_with_term: u32::MAX,
pair_new: u32::MAX,
string_eq: u32::MAX,
nomi_raise: u32::MAX,
log: u32::MAX,
nomi_catch_each: None,
get_output_offset: None,
get_input_offset: None,
get_input_entities_count: None,
ty_i8_array: u32::MAX,
ty_ratio: u32::MAX,
ty_pair: u32::MAX,
ty_commodity: u32::MAX,
ty_unit_term: u32::MAX,
ty_nomi_condition: u32::MAX,
ty_account: u32::MAX,
ty_commodity_entity: u32::MAX,
ty_transaction: u32::MAX,
ty_split: u32::MAX,
ty_tag_entity: u32::MAX,
ty_price: u32::MAX,
ty_ssh_key: u32::MAX,
ty_report_node: u32::MAX,
};
/// The wasm struct-type index for an entity kind. Exhaustive over
/// `EntityKind` — a new variant without an arm is a Rust compile error.
/// `Condition` shares the `$nomi_condition` struct (exception support),
/// matching `EntityKind::type_name`.
#[must_use]
pub fn entity_type(&self, kind: EntityKind) -> u32 {
match kind {
EntityKind::Account => self.ty_account,
EntityKind::Commodity => self.ty_commodity_entity,
EntityKind::Transaction => self.ty_transaction,
EntityKind::Split => self.ty_split,
EntityKind::Tag => self.ty_tag_entity,
EntityKind::Price => self.ty_price,
EntityKind::SshKey => self.ty_ssh_key,
EntityKind::ReportNode => self.ty_report_node,
EntityKind::Condition => self.ty_nomi_condition,
/// Stores the wasm struct-type index for an entity kind as it registers in
/// `new_skeleton`. Exhaustive over `EntityKind` (mirrors `entity_type`), so
/// a new variant without an arm is a Rust compile error.
pub fn set_entity_type(&mut self, kind: EntityKind, idx: u32) {
let slot = match kind {
EntityKind::Account => &mut self.ty_account,
EntityKind::Commodity => &mut self.ty_commodity_entity,
EntityKind::Transaction => &mut self.ty_transaction,
EntityKind::Split => &mut self.ty_split,
EntityKind::Tag => &mut self.ty_tag_entity,
EntityKind::Price => &mut self.ty_price,
EntityKind::SshKey => &mut self.ty_ssh_key,
EntityKind::ReportNode => &mut self.ty_report_node,
EntityKind::Condition => &mut self.ty_nomi_condition,
*slot = idx;
/// The ratio comparison helper index for a comparison operator. The
/// comparison dispatch only ever passes `"="` / `"<"` (closed set).
pub fn ratio_cmp(&self, op: &str) -> Result<u32> {
match op {
"=" => Ok(self.ratio_eq),
"<" => Ok(self.ratio_lt),
other => Err(Error::Compile(format!(
"no ratio comparison helper for operator '{other}'"
))),
/// The commodity comparison helper index for a comparison operator.
pub fn commodity_cmp(&self, op: &str) -> Result<u32> {
"=" => Ok(self.commodity_eq),
"<" => Ok(self.commodity_lt),
"no commodity comparison helper for operator '{other}'"
/// A mode-specific import index, or a structured error if read in a module
/// mode that didn't register it (rather than panicking).
fn mode_import(opt: Option<u32>, name: &str) -> Result<u32> {
opt.ok_or_else(|| {
Error::Compile(format!(
"wasm import '{name}' is not registered in this module mode"
))
})
pub fn nomi_catch_each(&self) -> Result<u32> {
Self::mode_import(self.nomi_catch_each, "__nomi_catch_each")
pub fn get_output_offset(&self) -> Result<u32> {
Self::mode_import(self.get_output_offset, "get_output_offset")
pub fn get_input_offset(&self) -> Result<u32> {
Self::mode_import(self.get_input_offset, "get_input_offset")
pub fn get_input_entities_count(&self) -> Result<u32> {
Self::mode_import(self.get_input_entities_count, "get_input_entities_count")
impl CompileContext {
/// Resolves every emit-referenced HELPER-FUNCTION name from the
/// (fully-declared) `func_names` map into the function fields of `WasmIds`,
/// preserving the TYPE fields already populated in `new_skeleton` (the
/// type-ref accessors need those mid-skeleton, before this runs). Called
/// ONCE after all `declare_*` have run. A missing common name is a
/// structured `Error::Compile` (a compiler bug surfaced cleanly, not a
/// panic); the mode-specific imports resolve to `None` in the other mode by
/// design.
pub(super) fn resolve_ids(&self) -> Result<WasmIds> {
let f = |name: &str| -> Result<u32> {
self.func_names.get(name).copied().ok_or_else(|| {
"internal: wasm function '{name}' was not registered during context construction"
Ok(WasmIds {
gcd: f("gcd")?,
ratio_new: f("ratio_new")?,
ratio_add: f("ratio_add")?,
ratio_sub: f("ratio_sub")?,
ratio_mul: f("ratio_mul")?,
ratio_div: f("ratio_div")?,
ratio_eq: f("ratio_eq")?,
ratio_lt: f("ratio_lt")?,
ratio_from_i64: f("ratio_from_i64")?,
ratio_to_i64: f("ratio_to_i64")?,
unit_singleton: f("unit_singleton")?,
unit_mul: f("unit_mul")?,
unit_negate: f("unit_negate")?,
unit_eq: f("unit_eq")?,
materialize_unit: f("materialize_unit")?,
commodity_add: f("commodity_add")?,
commodity_sub: f("commodity_sub")?,
commodity_mul: f("commodity_mul")?,
commodity_div: f("commodity_div")?,
commodity_mul_by_ratio: f("commodity_mul_by_ratio")?,
commodity_div_by_ratio: f("commodity_div_by_ratio")?,
commodity_neg: f("commodity_neg")?,
commodity_eq: f("commodity_eq")?,
commodity_lt: f("commodity_lt")?,
commodity_assert_atomic: f("commodity_assert_atomic")?,
commodity_new_with_term: f("commodity_new_with_term")?,
pair_new: f("pair_new")?,
string_eq: f("string_eq")?,
nomi_raise: f("__nomi_raise")?,
log: f("log")?,
nomi_catch_each: self.func_names.get("__nomi_catch_each").copied(),
get_output_offset: self.func_names.get("get_output_offset").copied(),
get_input_offset: self.func_names.get("get_input_offset").copied(),
get_input_entities_count: self.func_names.get("get_input_entities_count").copied(),
// Type fields were populated in `new_skeleton` (the type-ref
// accessors needed them while declaring helper signatures); carry
// them through unchanged.
ty_i8_array: self.ids.ty_i8_array,
ty_ratio: self.ids.ty_ratio,
ty_pair: self.ids.ty_pair,
ty_commodity: self.ids.ty_commodity,
ty_unit_term: self.ids.ty_unit_term,
ty_nomi_condition: self.ids.ty_nomi_condition,
ty_account: self.ids.ty_account,
ty_commodity_entity: self.ids.ty_commodity_entity,
ty_transaction: self.ids.ty_transaction,
ty_split: self.ids.ty_split,
ty_tag_entity: self.ids.ty_tag_entity,
ty_price: self.ids.ty_price,
ty_ssh_key: self.ids.ty_ssh_key,
ty_report_node: self.ids.ty_report_node,