Lines
100 %
Functions
60 %
Branches
//! Guest-side helpers for the Ratio type — the integer-fraction
//! arithmetic kernel every numeric path bottoms out in.
//!
//! `ratio_new` reduces by GCD and normalizes the sign so equality
//! checks are byte-comparable. The binop bodies use the textbook
//! cross-multiply formulas; comparisons compare signed cross-products.
//! All helpers are guest-side wasm functions registered up-front so
//! the codegen can `(call $ratio_*)` against stable function indices.
use super::CompileContext;
use crate::error::Result;
use wasm_encoder::{BlockType, Function, Instruction, ValType};
/// Captured indices + interned `division-by-zero` condition strings the
/// `ratio_div` guard needs to `throw` a catchable `$nomi_error` from a body
/// that only borrows `&self`. Mirrors `commodity::MismatchTrap`.
struct DivByZeroTrap {
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 ratio helper signatures (no bodies). Runs before
/// `resolve_ids` so every index exists when `WasmIds` is assembled; the
/// bodies are emitted later by [`Self::build_ratio_helpers`].
pub(super) fn declare_ratio_helpers(&mut self) -> Result<()> {
let ratio_ref = self.ratio_ref();
self.register_function("gcd", &[ValType::I64, ValType::I64], &[ValType::I64])?;
self.register_function("ratio_new", &[ValType::I64, ValType::I64], &[ratio_ref])?;
self.register_function("ratio_add", &[ratio_ref, ratio_ref], &[ratio_ref])?;
self.register_function("ratio_sub", &[ratio_ref, ratio_ref], &[ratio_ref])?;
self.register_function("ratio_mul", &[ratio_ref, ratio_ref], &[ratio_ref])?;
self.register_function("ratio_div", &[ratio_ref, ratio_ref], &[ratio_ref])?;
self.register_function("ratio_eq", &[ratio_ref, ratio_ref], &[ValType::I32])?;
self.register_function("ratio_lt", &[ratio_ref, ratio_ref], &[ValType::I32])?;
self.register_function("ratio_from_i64", &[ValType::I64], &[ratio_ref])?;
self.register_function("ratio_to_i64", &[ratio_ref], &[ValType::I64])?;
Ok(())
/// Emits the ratio helper bodies, in registration order so each maps to its
/// reserved function index. Reads resolved indices from `self.ids`; the
/// `division-by-zero` trap interns its strings here (in build order, so the
/// data-segment index sequence is unchanged from the pre-split layout).
pub(super) fn build_ratio_helpers(&mut self) -> Result<()> {
let div_trap = self.register_div_by_zero_trap()?;
self.build_gcd_body();
self.build_ratio_new_body(&div_trap);
self.build_ratio_binop_bodies();
self.build_ratio_cmp_bodies();
self.build_ratio_from_i64_body();
self.build_ratio_to_i64_body();
/// Interns the `division-by-zero` condition strings as passive data so the
/// `ratio_div` guard can `struct.new` + `throw` against stable indices.
fn register_div_by_zero_trap(&mut self) -> Result<DivByZeroTrap> {
// UPPER-CASE matches the reader's symbol case-folding so a script can
// catch it with `(handler-case … (division-by-zero (e) …))`.
const CODE: &str = "DIVISION-BY-ZERO";
const MESSAGE: &str = "division by zero";
let code_data = self.add_data(CODE.as_bytes())?;
let message_data = self.add_data(MESSAGE.as_bytes())?;
Ok(DivByZeroTrap {
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_gcd_body(&mut self) {
// Euclidean GCD: gcd(a, b) while b != 0 { t = b; b = a % b; a = t }; abs(a)
let mut f = Function::new([(1, ValType::I64)]); // local $t
// params: $a=0, $b=1, local: $t=2
f.instruction(&Instruction::Block(wasm_encoder::BlockType::Empty));
f.instruction(&Instruction::Loop(wasm_encoder::BlockType::Empty));
// if b == 0, break
f.instruction(&Instruction::LocalGet(1));
f.instruction(&Instruction::I64Eqz);
f.instruction(&Instruction::BrIf(1));
// t = b
f.instruction(&Instruction::LocalSet(2));
// b = a % b
f.instruction(&Instruction::LocalGet(0));
f.instruction(&Instruction::I64RemS);
f.instruction(&Instruction::LocalSet(1));
// a = t
f.instruction(&Instruction::LocalGet(2));
f.instruction(&Instruction::LocalSet(0));
f.instruction(&Instruction::Br(0));
f.instruction(&Instruction::End); // loop
f.instruction(&Instruction::End); // block
// return abs(a)
f.instruction(&Instruction::I64Const(0));
f.instruction(&Instruction::I64Sub);
f.instruction(&Instruction::I64LtS);
f.instruction(&Instruction::Select);
f.instruction(&Instruction::End);
self.pending_helpers.push(f);
fn build_ratio_new_body(&mut self, div_trap: &DivByZeroTrap) {
let ratio_idx = self.ids.ty_ratio;
let gcd_func = self.ids.gcd;
// ratio_new(num, denom) -> reduce by GCD, normalize sign
// params: $num=0, $denom=1, locals: $g=2
let mut f = Function::new([(1, ValType::I64)]);
// denom == 0 → throw a catchable `division-by-zero` rather than build a
// degenerate ratio (or trap in `gcd(0,0)` / a later `i64.div_s`). This
// is the single chokepoint for ALL ratio construction: a runtime zero
// divisor (via `ratio_div`) AND an `i64.mul` denominator overflow that
// wraps to zero in add/sub/mul both land here.
f.instruction(&Instruction::If(BlockType::Empty));
Self::emit_div_by_zero_throw(&mut f, div_trap);
// g = gcd(num, denom)
f.instruction(&Instruction::Call(gcd_func));
// num = num / g
f.instruction(&Instruction::I64DivS);
// denom = denom / g
// if denom < 0 { num = -num; denom = -denom }
f.instruction(&Instruction::If(wasm_encoder::BlockType::Empty));
// struct.new $ratio (num, denom)
f.instruction(&Instruction::StructNew(ratio_idx));
fn build_ratio_binop_bodies(&mut self) {
let ratio_new = self.ids.ratio_new;
// ratio_add(a, b): (a.num * b.denom + b.num * a.denom, a.denom * b.denom)
let mut f = Function::new([]);
// a.num * b.denom
f.instruction(&Instruction::StructGet {
struct_type_index: ratio_idx,
field_index: 0,
});
field_index: 1,
f.instruction(&Instruction::I64Mul);
// b.num * a.denom
f.instruction(&Instruction::I64Add);
// a.denom * b.denom
f.instruction(&Instruction::Call(ratio_new));
// ratio_sub(a, b): (a.num * b.denom - b.num * a.denom, a.denom * b.denom)
// ratio_mul(a, b): (a.num * b.num, a.denom * b.denom)
// ratio_div(a, b): (a.num * b.denom, a.denom * b.num). A zero divisor
// makes `a.denom * b.num` zero, caught by `ratio_new`'s denom-0 guard.
fn build_ratio_cmp_bodies(&mut self) {
// ratio_eq(a, b): a.num * b.denom == b.num * a.denom
f.instruction(&Instruction::I64Eq);
// ratio_lt(a, b): a.num * b.denom < b.num * a.denom
fn build_ratio_from_i64_body(&mut self) {
// ratio_from_i64(n) -> struct.new $ratio (n, 1)
f.instruction(&Instruction::I64Const(1));
/// Builds the `division-by-zero` `$nomi_condition` (interned code + message)
/// and `throw`s `$nomi_error`. `throw` never returns, so the body's declared
/// ratio result is satisfied by wasm stack-polymorphism past the throw.
fn emit_div_by_zero_throw(f: &mut Function, trap: &DivByZeroTrap) {
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));
fn build_ratio_to_i64_body(&mut self) {
// ratio_to_i64(r) -> r.numer / r.denom (i64.div_s truncates toward zero,
// matching ADR-0028's scalar->index narrowing). denom is never 0: the
// only path that could build a denom-0 ratio is division by a zero
// divisor, which `ratio_div` now throws on — so no guard is needed here.