Lines
97.64 %
Functions
60 %
Branches
100 %
//! Guest-side helpers for the Commodity type.
//!
//! Mirrors `register_ratio_helpers` but for commodity-bearing values.
//! `commodity_new` is a thin wrapper around `struct.new commodity`;
//! the arithmetic helpers (`commodity_add`, `commodity_sub`, ...) do
//! an id-equality check at the front and, on mismatch, `throw` a
//! `$nomi_error` carrying a `commodity-mismatch` condition (ADR-0014 +
//! ADR-0026). The throw is catchable by an in-module `(handler-case)` /
//! `(unwind-protect)`; an uncaught one is bridged to `__nomi_raise` by the
//! boundary wrapper, surfacing the same `commodity-mismatch` wire code as
//! before — engine errors now travel the single exception channel rather
//! than the lone `unreachable` trap. Scaling helpers
//! (`commodity_mul_by_ratio`, `commodity_div_by_ratio`) accept a pure Ratio
//! second operand and keep the commodity id from the first.
use super::CompileContext;
use crate::error::Result;
use wasm_encoder::{Function, Instruction, ValType};
/// Pre-registered data-segment handles + type/tag indices the
/// commodity-mismatch `throw` needs. Computed once in
/// `register_commodity_helpers` (where `&mut self` can `add_data`) and
/// threaded into each id-check site (which only has `&self`).
#[derive(Clone, Copy)]
struct MismatchTrap {
i8_array_idx: u32,
condition_idx: u32,
tag: u32,
code_data: u32,
code_len: u32,
message_data: u32,
message_len: u32,
}
impl CompileContext {
/// Declares the commodity helper signatures (atomic + compound), no bodies.
/// The compound signatures are registered here too so every commodity index
/// exists before `resolve_ids`; bodies are emitted by
/// [`Self::build_commodity_helpers`] in the same registration order.
pub(super) fn declare_commodity_helpers(&mut self) -> Result<()> {
let commodity_ref = self.commodity_ref();
let ratio_ref = self.ratio_ref();
self.register_function(
"commodity_new",
&[ValType::I64, ValType::I64, ValType::I64, ValType::I64],
&[commodity_ref],
)?;
"commodity_add",
&[commodity_ref, commodity_ref],
"commodity_sub",
self.register_function("commodity_neg", &[commodity_ref], &[commodity_ref])?;
"commodity_mul_by_ratio",
&[commodity_ref, ratio_ref],
"commodity_div_by_ratio",
"commodity_eq",
&[ValType::I32],
"commodity_lt",
// Exported so the host (`alloc_commodity_ref`) constructs commodity
// values by re-entering this helper rather than building the now
// ref-bearing 5-field struct itself (ADR-0028 E0), mirroring the
// `pair_new` / entity-allocator re-entry pattern.
self.export_func("commodity_new")?;
// Compound-money helpers (ADR-0028 E2): SIGNATURES only, registered
// after the atomic ones so the function-index order is unchanged.
self.declare_commodity_compound_signatures()
/// Emits the commodity helper bodies in registration order. Traps are
/// interned here (build order), so the data-segment index sequence matches
/// the pre-split layout; `&self`-borrowing throw sites read `self.ids`.
pub(super) fn build_commodity_helpers(&mut self) -> Result<()> {
let trap = self.register_mismatch_trap()?;
let non_atomic = self.register_non_atomic_trap()?;
self.build_commodity_new_body();
self.build_commodity_binop_bodies(trap);
self.build_commodity_neg_body();
self.build_commodity_scale_bodies();
self.build_commodity_cmp_bodies(trap);
self.build_commodity_compound_bodies(&non_atomic)?;
Ok(())
/// Interns the `commodity-mismatch` condition's code + message strings as
/// passive data segments and captures the `$nomi_condition` / `$nomi_error`
/// indices, so each id-check site (which only borrows `&self`) can emit the
/// `struct.new` + `throw` without needing `&mut self`.
fn register_mismatch_trap(&mut self) -> Result<MismatchTrap> {
// UPPER-CASE to match the reader's symbol case-folding: a
// `(handler-case … (commodity-mismatch (e) …))` clause upcases its
// code to `COMMODITY-MISMATCH`, and handler dispatch compares the
// condition's code byte-for-byte — so the thrown code must be the
// upcased symbol form (the same convention script raises follow, e.g.
// `(error 'no-such-account …)` → `NO-SUCH-ACCOUNT`). The uncaught wire
// `:code` is therefore `COMMODITY-MISMATCH` too.
const CODE: &str = "COMMODITY-MISMATCH";
const MESSAGE: &str = "cannot combine values of different commodities";
let code_data = self.add_data(CODE.as_bytes())?;
let message_data = self.add_data(MESSAGE.as_bytes())?;
Ok(MismatchTrap {
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,
})
fn build_commodity_new_body(&mut self) {
let new_with_term = self.ids.commodity_new_with_term;
// params: $numer=0, $denom=1, $commodity_hi=2, $commodity_lo=3.
// The atomic constructor: delegate to `commodity_new_with_term` with a
// NULL unit term (ATOMIC `[(atom,1)]`). No reduction — callers pass the
// canonical (numer, denom) pair they want.
let mut f = Function::new([]);
f.instruction(&Instruction::LocalGet(0));
f.instruction(&Instruction::LocalGet(1));
f.instruction(&Instruction::LocalGet(2));
f.instruction(&Instruction::LocalGet(3));
self.emit_null_unit_term(&mut f);
f.instruction(&Instruction::Call(new_with_term));
f.instruction(&Instruction::End);
self.pending_helpers.push(f);
/// Emits the prologue of a same-commodity binop: compares both i64
/// halves of the UUID pair and, on mismatch, throws a `commodity-mismatch`
/// `$nomi_error` (ADR-0026). Caller-supplied param indices (0 and 1 for
/// the standard two-Commodity shape).
fn emit_commodity_id_check(&self, f: &mut Function, trap: MismatchTrap, a: u32, b: u32) {
let commodity_idx = self.ids.ty_commodity;
// hi mismatch
f.instruction(&Instruction::LocalGet(a));
f.instruction(&Instruction::StructGet {
struct_type_index: commodity_idx,
field_index: 2,
});
f.instruction(&Instruction::LocalGet(b));
f.instruction(&Instruction::I64Ne);
// lo mismatch
field_index: 3,
// either half differs → build the condition and throw $nomi_error.
f.instruction(&Instruction::I32Or);
f.instruction(&Instruction::If(wasm_encoder::BlockType::Empty));
self.emit_mismatch_throw(f, trap);
/// Unit-aware prologue for add/sub/eq/lt (ADR-0028 E2): when both operands
/// are ATOMIC (null term) it is the cheap hi/lo id-check above; otherwise it
/// compares the materialized unit terms with `unit_eq` and throws
/// `commodity-mismatch` if they differ. Atomic money keeps the exact
/// fast-path behaviour it had before compound money existed.
fn emit_commodity_unit_check(&self, f: &mut Function, trap: MismatchTrap, a: u32, b: u32) {
let materialize = self.ids.materialize_unit;
let unit_eq = self.ids.unit_eq;
// both terms null?
field_index: 4,
f.instruction(&Instruction::RefIsNull);
f.instruction(&Instruction::I32And);
self.emit_commodity_id_check(f, trap, a, b);
f.instruction(&Instruction::Else);
// compound: compare materialized terms by multiset equality
f.instruction(&Instruction::Call(materialize));
f.instruction(&Instruction::Call(unit_eq));
f.instruction(&Instruction::I32Eqz);
/// Builds the `commodity-mismatch` `$nomi_condition` (interned code +
/// message strings) and `throw`s `$nomi_error`. `throw` never returns, so
/// the surrounding binop body's declared result type is satisfied by
/// wasm stack-polymorphism past the throw.
fn emit_mismatch_throw(&self, f: &mut Function, trap: MismatchTrap) {
f.instruction(&Instruction::I32Const(0));
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 a fresh `ratio_ref` built from the (numer, denom) fields of
/// the commodity at the given param index. Uses `struct.new ratio`
/// directly (no normalization) since the values are already canonical
/// from a prior `ratio_new` / `commodity_new`.
pub(super) fn emit_ratio_from_commodity(&self, f: &mut Function, param_idx: u32) {
let ratio_idx = self.ids.ty_ratio;
f.instruction(&Instruction::LocalGet(param_idx));
field_index: 0,
field_index: 1,
f.instruction(&Instruction::StructNew(ratio_idx));
/// After a same-commodity binop / scaling produces a result `ratio_ref` (in
/// local $r), repackages it as a `commodity_ref` carrying the id AND unit
/// term of param $a. Add/sub verify both operands share a term, and scaling
/// leaves the unit unchanged, so propagating $a's term (field 4) is correct
/// for every caller and keeps compound money compound (ADR-0028 E2).
fn emit_commodity_repack(&self, f: &mut Function, ratio_local: u32, a_param: u32) {
f.instruction(&Instruction::LocalGet(ratio_local));
struct_type_index: ratio_idx,
f.instruction(&Instruction::LocalGet(a_param));
fn build_commodity_binop_bodies(&mut self, trap: MismatchTrap) {
let ratio_add = self.ids.ratio_add;
let ratio_sub = self.ids.ratio_sub;
// commodity_add(a, b): id-check, then ratio_add(a.ratio, b.ratio),
// re-pack as commodity carrying a's id. Locals: $r=2 (ratio_ref).
let mut f = Function::new([(1, ratio_ref)]);
self.emit_commodity_unit_check(&mut f, trap, 0, 1);
self.emit_ratio_from_commodity(&mut f, 0);
self.emit_ratio_from_commodity(&mut f, 1);
f.instruction(&Instruction::Call(ratio_add));
f.instruction(&Instruction::LocalSet(2));
self.emit_commodity_repack(&mut f, 2, 0);
// commodity_sub(a, b): id-check, then ratio_sub.
f.instruction(&Instruction::Call(ratio_sub));
fn build_commodity_neg_body(&mut self) {
// commodity_neg(a) = commodity_new_with_term(-a.numer, a.denom, a.hi,
// a.lo, a.term). Negation flips the sign but preserves the unit term, so
// a compound money stays compound (ADR-0028 E2).
// -a.numer = 0 - a.numer
f.instruction(&Instruction::I64Const(0));
f.instruction(&Instruction::I64Sub);
// a.denom
// a.hi
// a.lo
// a.term
fn build_commodity_scale_bodies(&mut self) {
let ratio_mul = self.ids.ratio_mul;
let ratio_div = self.ids.ratio_div;
// commodity_mul_by_ratio(c, r): ratio_mul(c.ratio, r) -> repack with c's id.
// Param 0=c (commodity_ref), param 1=r (ratio_ref). Local 2=result ratio_ref.
f.instruction(&Instruction::Call(ratio_mul));
// commodity_div_by_ratio(c, r): ratio_div(c.ratio, r) -> repack with c's id.
f.instruction(&Instruction::Call(ratio_div));
fn build_commodity_cmp_bodies(&mut self, trap: MismatchTrap) {
let ratio_eq = self.ids.ratio_eq;
let ratio_lt = self.ids.ratio_lt;
// commodity_eq(a, b): id-check, ratio_eq(a.ratio, b.ratio).
f.instruction(&Instruction::Call(ratio_eq));
// commodity_lt(a, b): id-check, ratio_lt(a.ratio, b.ratio).
f.instruction(&Instruction::Call(ratio_lt));