Lines
76.22 %
Functions
27.2 %
Branches
100 %
//! `to_stack` codegen for `+ - * / MOD` over the ADR-0028 dimension
//! lattice {Index, Scalar, Money}. An all-literal call const-folds; a
//! runtime call compiles each non-literal operand to a local (which
//! yields its real wasm type — including closure-call results the eval
//! path cannot reduce), classifies the operation's dimension, then emits
//! the dimension's native ops: raw `i32` for Index, `ratio_*` for Scalar,
//! `commodity_*` for Money. A numeric literal is dimension-flexible and
//! crosses the sanctioned Index↔Scalar boundary; everything else is a
//! compile-time strata mismatch.
use crate::ast::{Expr, Fraction, WasmType};
use crate::compiler::context::CompileContext;
use crate::compiler::emit::FunctionEmitter;
use crate::compiler::expr::{compile_for_effect, compile_for_stack, eval_value, push_ratio};
use crate::error::{Error, Result};
use crate::runtime::SymbolTable;
use super::eval::{fold_add, fold_div, fold_mod, fold_mul, fold_sub, try_fold};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Dim {
Index,
Scalar,
Money,
}
impl Dim {
fn wasm_type(self) -> WasmType {
match self {
Dim::Index => WasmType::I32,
Dim::Scalar => WasmType::Ratio,
Dim::Money => WasmType::Commodity,
fn of(ty: WasmType, op: &str) -> Result<Dim> {
match ty {
WasmType::I32 => Ok(Dim::Index),
WasmType::Ratio => Ok(Dim::Scalar),
WasmType::Commodity => Ok(Dim::Money),
other => Err(Error::Compile(format!(
"{op} expects numeric arguments, got a {other} value"
))),
/// A compiled operand: a dimension-flexible numeric literal (not yet emitted),
/// or a runtime value already evaluated into `local` with its wasm type.
enum Slot {
Literal(Fraction),
Runtime(u32, WasmType),
impl Slot {
/// The fixed dimension of a runtime slot, or `None` for a flexible literal.
fn runtime_dim(&self, op: &str) -> Result<Option<Dim>> {
Slot::Literal(_) => Ok(None),
Slot::Runtime(_, ty) => Dim::of(*ty, op).map(Some),
/// Compile each operand once: a literal (syntactic, or a constant that resolves
/// to a `Number`) stays flexible; everything else is emitted to a fresh local,
/// capturing its real wasm type and its side effects in operand order. The
/// literal probe runs on a clone so it applies no live mutation.
fn collect_slots(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
args: &[Expr],
) -> Result<Vec<Slot>> {
let mut slots = Vec::with_capacity(args.len());
for arg in args {
match eval_value(&mut symbols.clone(), arg) {
Ok(Expr::Number(n)) => {
// A non-literal that *resolves* to a number (e.g. `(setf x 1)`)
// still emits its effects on the live table, in operand order,
// before the flexible literal is recorded.
if !matches!(arg, Expr::Number(_)) {
compile_for_effect(ctx, emit, symbols, arg)?;
slots.push(Slot::Literal(n));
_ => {
let ty = compile_for_stack(ctx, emit, symbols, arg)?;
let local = ctx.alloc_local(ty)?;
emit.local_set(local);
slots.push(Slot::Runtime(local, ty));
Ok(slots)
/// Emit a single slot coerced to `dim`. A literal crosses Index↔Scalar; a
/// runtime slot must already be that dimension (a runtime value never coerces).
fn emit_slot(
ctx: &CompileContext,
slot: &Slot,
dim: Dim,
op: &str,
) -> Result<()> {
match slot {
Slot::Literal(n) => emit_literal(ctx, emit, *n, dim, op),
Slot::Runtime(local, ty) => {
if Dim::of(*ty, op)? == dim {
emit.local_get(*local);
Ok(())
} else {
Err(mix_error(op))
fn emit_literal(
n: Fraction,
match dim {
Dim::Index => {
if *n.denom() != 1 {
return Err(Error::Compile(format!(
"{op}: a fractional literal is a scalar, not an index"
)));
emit.i32_const(i32::try_from(*n.numer()).map_err(|_| {
Error::Compile(format!("integer literal {} exceeds i32 range", n.numer()))
})?);
Dim::Scalar => {
push_ratio(ctx, emit, *n.numer(), *n.denom());
Dim::Money => Err(Error::Compile(format!(
"{op} cannot combine a bare literal with a money value (a literal \
has no currency); supply a money value or scale with `*`"
/// The single dimension every operand of an additive op (`+`/`-`/`MOD`) shares:
/// all runtime operands must agree, and a bare literal next to Money is refused.
fn additive_dim(slots: &[Slot], op: &str) -> Result<Dim> {
let mut runtime = slots.iter().filter_map(|s| s.runtime_dim(op).transpose());
let first = match runtime.next() {
Some(d) => d?,
None => Dim::Scalar,
};
for d in runtime {
if d? != first {
return Err(mix_error(op));
if first == Dim::Money && slots.iter().any(|s| matches!(s, Slot::Literal(_))) {
has no currency); supply a money value"
Ok(first)
fn push_const(ctx: &CompileContext, emit: &mut FunctionEmitter, n: Fraction) -> Result<WasmType> {
if *n.denom() == 1 {
Ok(WasmType::I32)
Ok(WasmType::Ratio)
pub(super) fn compile_add_to_stack(
) -> Result<WasmType> {
if let Some(nums) = try_fold(symbols, args) {
return push_const(ctx, emit, fold_add(&nums)?);
if args.is_empty() {
emit.i32_const(0);
return Ok(WasmType::I32);
compile_additive(ctx, emit, symbols, args, "+", BinOp::Add)
pub(super) fn compile_sub_to_stack(
return Err(Error::Compile("- requires at least 1 argument".to_string()));
return push_const(ctx, emit, fold_sub(&nums)?);
let slots = collect_slots(ctx, emit, symbols, args)?;
let dim = additive_dim(&slots, "-")?;
if slots.len() == 1 {
return emit_unary_neg(ctx, emit, &slots[0], dim);
emit_slot(ctx, emit, &slots[0], dim, "-")?;
for slot in &slots[1..] {
emit_slot(ctx, emit, slot, dim, "-")?;
emit_combine(ctx, emit, dim, BinOp::Sub);
Ok(dim.wasm_type())
pub(super) fn compile_mul_to_stack(
return push_const(ctx, emit, fold_mul(&nums)?);
emit.i32_const(1);
let seed = scaling_seed_dim(&slots, "*")?;
emit_slot(ctx, emit, &slots[0], seed, "*")?;
let mut acc = seed.wasm_type();
acc = combine_mul(ctx, emit, acc, slot)?;
Ok(acc)
pub(super) fn compile_div_to_stack(
return Err(Error::Compile("/ requires at least 1 argument".to_string()));
return push_const(ctx, emit, fold_div(&nums)?);
let seed = scaling_seed_dim(&slots, "/")?;
return emit_reciprocal(ctx, emit, &slots[0], seed);
emit_slot(ctx, emit, &slots[0], seed, "/")?;
acc = combine_div(ctx, emit, acc, slot)?;
pub(super) fn compile_mod_to_stack(
if args.len() != 2 {
return Err(Error::Arity {
name: "MOD".to_string(),
expected: 2,
actual: args.len(),
});
return push_const(ctx, emit, fold_mod(&nums)?);
match additive_dim(&slots, "MOD")? {
emit_slot(ctx, emit, &slots[0], Dim::Index, "MOD")?;
emit_slot(ctx, emit, &slots[1], Dim::Index, "MOD")?;
emit.i32_rem_s();
Dim::Scalar | Dim::Money => Err(Error::Compile(
"MOD with runtime non-integer arguments is not yet supported".to_string(),
)),
#[derive(Clone, Copy)]
enum BinOp {
Add,
Sub,
fn emit_combine(ctx: &CompileContext, emit: &mut FunctionEmitter, dim: Dim, op: BinOp) {
match (dim, op) {
(Dim::Index, BinOp::Add) => emit.i32_add(),
(Dim::Index, BinOp::Sub) => emit.i32_sub(),
(Dim::Scalar, BinOp::Add) => emit.call(ctx.ids.ratio_add),
(Dim::Scalar, BinOp::Sub) => emit.call(ctx.ids.ratio_sub),
(Dim::Money, BinOp::Add) => emit.call(ctx.ids.commodity_add),
(Dim::Money, BinOp::Sub) => emit.call(ctx.ids.commodity_sub),
fn compile_additive(
binop: BinOp,
let dim = additive_dim(&slots, op)?;
emit_slot(ctx, emit, &slots[0], dim, op)?;
emit_slot(ctx, emit, slot, dim, op)?;
emit_combine(ctx, emit, dim, binop);
fn emit_unary_neg(
emit_slot(ctx, emit, slot, Dim::Index, "-")?;
emit.i32_sub();
let r_local = ctx.alloc_local(WasmType::Ratio)?;
emit_slot(ctx, emit, slot, Dim::Scalar, "-")?;
emit.local_set(r_local);
push_ratio(ctx, emit, 0, 1);
emit.local_get(r_local);
emit.call(ctx.ids.ratio_sub);
Dim::Money => {
emit_slot(ctx, emit, slot, Dim::Money, "-")?;
emit.call(ctx.ids.commodity_neg);
Ok(WasmType::Commodity)
/// Seed dimension for the first operand of `*`/`/`: Money if any operand is
/// runtime money, Scalar if any runtime scalar, else Index. A literal first
/// operand is emitted at this dimension (Scalar when money/scalar is involved).
fn scaling_seed_dim(slots: &[Slot], op: &str) -> Result<Dim> {
let mut money = false;
let mut scalar = false;
let mut index = false;
for s in slots {
match s.runtime_dim(op)? {
Some(Dim::Money) => money = true,
Some(Dim::Scalar) => scalar = true,
Some(Dim::Index) => index = true,
None => {}
let dominant = if money {
Dim::Money
} else if scalar {
Dim::Scalar
} else if index {
Dim::Index
Ok(match &slots[0] {
Slot::Runtime(_, ty) => Dim::of(*ty, op)?,
Slot::Literal(_) if dominant == Dim::Index => Dim::Index,
Slot::Literal(_) => Dim::Scalar,
})
fn combine_mul(
acc: WasmType,
match acc {
WasmType::I32 => {
emit_slot(ctx, emit, slot, Dim::Index, "*")?;
emit.i32_mul();
WasmType::Ratio => match slot.runtime_dim("*")? {
Some(Dim::Money) => {
// scalar × money → money: stash the scalar, push the money, restore.
let scalar_local = ctx.alloc_local(WasmType::Ratio)?;
emit.local_set(scalar_local);
emit_slot(ctx, emit, slot, Dim::Money, "*")?;
emit.local_get(scalar_local);
emit.call(ctx.ids.commodity_mul_by_ratio);
emit_slot(ctx, emit, slot, Dim::Scalar, "*")?;
emit.call(ctx.ids.ratio_mul);
},
WasmType::Commodity => match slot.runtime_dim("*")? {
// money × money → compound money (ADR-0028 E2): the unit terms
// multiply (exponents add).
emit.call(ctx.ids.commodity_mul);
_ => Err(mix_error("*")),
fn combine_div(
emit_slot(ctx, emit, slot, Dim::Index, "/")?;
emit.i32_div_s();
WasmType::Ratio => match slot.runtime_dim("/")? {
Some(Dim::Money) => Err(Error::Compile(
"dividing a pure rational by a commodity-bearing value has no \
dimensional meaning; reverse operands or convert-commodity first"
.to_string(),
emit_slot(ctx, emit, slot, Dim::Scalar, "/")?;
emit.call(ctx.ids.ratio_div);
WasmType::Commodity => match slot.runtime_dim("/")? {
// money ÷ money → money carrying the divided unit term (ADR-0028
// E2): same currency cancels to a dimensionless term (serializes
// as a Number), cross-currency gives a compound term.
emit_slot(ctx, emit, slot, Dim::Money, "/")?;
emit.call(ctx.ids.commodity_div);
emit.call(ctx.ids.commodity_div_by_ratio);
_ => Err(mix_error("/")),
fn emit_reciprocal(
Dim::Money => Err(Error::Compile(
"reciprocal of a commodity-bearing value has no dimensional meaning; \
divide a commodity by a Ratio scalar instead"
push_ratio(ctx, emit, 1, 1);
fn mix_error(op: &str) -> Error {
Error::Compile(format!(
"{op} cannot mix dimensions (index / scalar / money); \
bridge explicitly with `(index->scalar ...)`, `(scalar->index ...)`, \
or `(convert-commodity ...)`"
))