Lines
98.13 %
Functions
60 %
Branches
100 %
//! Compound-money guest helpers (ADR-0028 E2): the bridge between the
//! `$commodity` struct's `(numer, denom, hi, lo)` ratio+id fields and its
//! runtime unit term (see [`super::unit_term`]).
//!
//! `commodity_new_with_term` is the single `struct.new $commodity` site;
//! `commodity_new` (the atomic constructor) delegates to it with a null term.
//! `commodity_mul` / `commodity_div` multiply/divide the ratios and combine
//! the unit terms — a same-currency division cancels to an EMPTY term
//! (dimensionless), a cross-currency one produces a compound term. The unit is
//! erased at the wasm↔host border by `commodity_assert_atomic`, which throws a
//! catchable `NON-ATOMIC-COMMODITY` if a non-atomic value reaches a host fn.
use super::CompileContext;
use crate::error::Result;
use wasm_encoder::{BlockType, Function, HeapType, Instruction, ValType};
/// Interned `NON-ATOMIC-COMMODITY` condition handles for the host-border guard
/// (mirrors `commodity::MismatchTrap`).
pub(super) struct NonAtomicTrap {
i8_array_idx: u32,
condition_idx: u32,
tag: u32,
code_data: u32,
code_len: u32,
message_data: u32,
message_len: u32,
}
impl CompileContext {
/// Registers the compound-helper SIGNATURES (and interns the
/// `NON-ATOMIC-COMMODITY` condition), returning the trap. Split from body
/// building so the caller registers these signatures up-front — their
/// indices must exist before `commodity_new` / the binop unit-checks
/// reference them — yet emits their BODIES last: `pending_helpers` maps
/// bodies to function indices in registration order, so body-build order
/// must match registration order.
pub(super) fn declare_commodity_compound_signatures(&mut self) -> Result<()> {
let commodity_ref = self.commodity_ref();
let unit_ref = self.unit_term_ref();
self.register_function(
"commodity_new_with_term",
&[
ValType::I64,
unit_ref,
],
&[commodity_ref],
)?;
self.register_function("materialize_unit", &[commodity_ref], &[unit_ref])?;
"commodity_mul",
&[commodity_ref, commodity_ref],
"commodity_div",
"commodity_assert_atomic",
Ok(())
/// Emits the compound-helper bodies, in the SAME order their signatures were
/// registered by [`Self::register_commodity_compound_signatures`].
pub(super) fn build_commodity_compound_bodies(&mut self, trap: &NonAtomicTrap) -> Result<()> {
self.build_commodity_new_with_term_body();
self.build_materialize_unit_body();
self.build_commodity_mul_body();
self.build_commodity_div_body()?;
self.build_commodity_assert_atomic_body(trap);
pub(super) fn register_non_atomic_trap(&mut self) -> Result<NonAtomicTrap> {
const CODE: &str = "NON-ATOMIC-COMMODITY";
const MESSAGE: &str = "host functions require single-currency (atomic) money";
let code_data = self.add_data(CODE.as_bytes())?;
let message_data = self.add_data(MESSAGE.as_bytes())?;
Ok(NonAtomicTrap {
i8_array_idx: self.ids.ty_i8_array,
condition_idx: self.condition_type_idx(),
tag: self.nomi_error_tag(),
code_data,
code_len: CODE.len() as u32,
message_data,
message_len: MESSAGE.len() as u32,
})
/// The sole `struct.new $commodity` site: `(numer, denom, hi, lo, term)`,
/// with one CANONICALIZATION — a non-null SINGLETON term `[(id, 1)]` is
/// semantically ATOMIC money (e.g. `(* (/ usd usd) usd)` = dimensionless ×
/// usd → a `[(usd,1)]` term), so it is rebuilt as atomic: null term, id from
/// the triple. Otherwise: a null term stays atomic, an empty term stays
/// dimensionless, and a true compound term (len > 3, or len 3 with exp ≠ 1)
/// stays non-null with id fields 0. Without this, valid arithmetic that
/// reduces to a single currency would be mis-flagged compound and rejected
/// by the host-border guard / decoder.
fn build_commodity_new_with_term_body(&mut self) {
let commodity_idx = self.ids.ty_commodity;
let unit_idx = self.ids.ty_unit_term;
let mut f = Function::new([]);
// Keep id fields + term verbatim (atomic-null / dimensionless-empty /
// compound). Used by every non-canonicalizable branch.
let emit_passthrough = |f: &mut Function| {
for p in 0..5 {
f.instruction(&Instruction::LocalGet(p));
f.instruction(&Instruction::StructNew(commodity_idx));
};
f.instruction(&Instruction::LocalGet(4));
f.instruction(&Instruction::RefIsNull);
f.instruction(&Instruction::I32Eqz); // term non-null?
f.instruction(&Instruction::If(BlockType::Result(commodity_ref)));
// Non-null term: a `len == 3` term MIGHT be a singleton — only then is it
// safe to read its exponent (`array.get 2` on a shorter term would trap).
f.instruction(&Instruction::ArrayLen);
f.instruction(&Instruction::I32Const(3));
f.instruction(&Instruction::I32Eq);
f.instruction(&Instruction::I32Const(2));
f.instruction(&Instruction::ArrayGet(unit_idx)); // exponent
f.instruction(&Instruction::I64Const(1));
f.instruction(&Instruction::I64Eq);
// Singleton exponent 1 → canonical atomic: id from the triple, null term.
f.instruction(&Instruction::LocalGet(0));
f.instruction(&Instruction::LocalGet(1));
f.instruction(&Instruction::I32Const(0));
f.instruction(&Instruction::ArrayGet(unit_idx)); // hi
f.instruction(&Instruction::I32Const(1));
f.instruction(&Instruction::ArrayGet(unit_idx)); // lo
self.emit_null_unit_term(&mut f);
f.instruction(&Instruction::Else);
emit_passthrough(&mut f); // len 3, exp ≠ 1 → genuine compound
f.instruction(&Instruction::End);
emit_passthrough(&mut f); // empty (dimensionless) or len > 3 (compound)
emit_passthrough(&mut f); // null term → atomic
self.pending_helpers.push(f);
/// `materialize_unit(c)` → the effective unit term: a null (atomic) term
/// becomes the singleton `[(c.hi, c.lo, 1)]`; a present term is returned
/// as-is. The result is always non-null, so term arithmetic can `array.len`
/// it without a trap.
fn build_materialize_unit_body(&mut self) {
let unit_singleton = self.ids.unit_singleton;
f.instruction(&Instruction::StructGet {
struct_type_index: commodity_idx,
field_index: 4,
});
f.instruction(&Instruction::If(BlockType::Result(unit_ref)));
// atomic: singleton from the id fields
field_index: 2,
field_index: 3,
f.instruction(&Instruction::Call(unit_singleton));
// compound/dimensionless: the present term
/// `commodity_mul(a, b)` — multiply ratios, merge unit terms. Always
/// produces a value whose unit is the term (id fields 0); a money × money
/// is inherently compound.
fn build_commodity_mul_body(&mut self) {
self.build_commodity_combine_body(self.ids.ratio_mul, self.ids.unit_mul);
/// `commodity_div(a, b)` — divide ratios, subtract unit terms. Same
/// currency cancels to an empty (dimensionless) term; different currencies
/// give a compound term. `unit_div` has no `WasmIds` field (it is referenced
/// only here), so its index is looked up from the declared func map once.
fn build_commodity_div_body(&mut self) -> Result<()> {
let unit_div = self.declared_func_index("unit_div")?;
self.build_commodity_combine_body(self.ids.ratio_div, unit_div);
/// Shared body for `commodity_mul`/`commodity_div`: `ratio_op` combines the
/// two ratios, `unit_op` combines the two materialized unit terms, and the
/// result is packed with id fields zeroed (the term is the unit of truth).
fn build_commodity_combine_body(&mut self, ratio_op: u32, unit_op: u32) {
let ratio_idx = self.ids.ty_ratio;
let ratio_ref = self.ratio_ref();
let materialize = self.ids.materialize_unit;
let new_with_term = self.ids.commodity_new_with_term;
// locals: $r=2 (ratio_ref), $term=3 (unit_ref)
let mut f = Function::new([(1, ratio_ref), (1, unit_ref)]);
self.emit_ratio_from_commodity(&mut f, 0);
self.emit_ratio_from_commodity(&mut f, 1);
f.instruction(&Instruction::Call(ratio_op));
f.instruction(&Instruction::LocalSet(2));
f.instruction(&Instruction::Call(materialize));
f.instruction(&Instruction::Call(unit_op));
f.instruction(&Instruction::LocalSet(3));
// commodity_new_with_term(r.numer, r.denom, 0, 0, term)
f.instruction(&Instruction::LocalGet(2));
struct_type_index: ratio_idx,
field_index: 0,
field_index: 1,
f.instruction(&Instruction::I64Const(0));
f.instruction(&Instruction::LocalGet(3));
f.instruction(&Instruction::Call(new_with_term));
/// `commodity_assert_atomic(c)` → `c` if its term is null (atomic), else a
/// catchable `NON-ATOMIC-COMMODITY` throw. Emitted at the host border so a
/// compound money never reaches a host fn (which sees only fields 0-3).
fn build_commodity_assert_atomic_body(&mut self, trap: &NonAtomicTrap) {
f.instruction(&Instruction::I32Eqz);
f.instruction(&Instruction::If(BlockType::Empty));
Self::emit_non_atomic_throw(&mut f, trap);
fn emit_non_atomic_throw(f: &mut Function, trap: &NonAtomicTrap) {
f.instruction(&Instruction::I32Const(trap.code_len as i32));
f.instruction(&Instruction::ArrayNewData {
array_type_index: trap.i8_array_idx,
array_data_index: trap.code_data,
f.instruction(&Instruction::I32Const(trap.message_len as i32));
array_data_index: trap.message_data,
f.instruction(&Instruction::StructNew(trap.condition_idx));
f.instruction(&Instruction::Throw(trap.tag));
/// Pushes `ref.null $unit_term` (the ATOMIC term marker) onto the stack.
pub(super) fn emit_null_unit_term(&self, f: &mut Function) {
f.instruction(&Instruction::RefNull(HeapType::Concrete(
self.ids.ty_unit_term,
)));