Lines
86.53 %
Functions
32.41 %
Branches
100 %
//! Constant-folding eval handlers for `+ - * / MOD` over the ADR-0028
//! lattice. An all-literal call folds to an `Expr::Number` (integer
//! operands fold with Index semantics — `/` truncates, `MOD` is `rem` —
//! so the eval surface agrees with the `i32.div_s` / `i32.rem_s` codegen);
//! otherwise the handler returns `Expr::WasmRuntime(result_dim)`. Real
//! cross-strata refusal happens at codegen time in `compile::*`.
use num_traits::{CheckedAdd, CheckedDiv, CheckedMul, CheckedSub};
use crate::ast::{Expr, Fraction, WasmType};
use crate::compiler::expr::{eval_value, format_expr};
use crate::error::{Error, Result};
use crate::runtime::SymbolTable;
pub(super) fn try_fold(symbols: &mut SymbolTable, args: &[Expr]) -> Option<Vec<Fraction>> {
// Probe on a CLONE: this only decides whether the call const-folds; it must
// not apply an operand's compile-time effects to the live table (the actual
// emission re-walks the operands on the live path).
args.iter()
.map(|arg| {
eval_value(&mut symbols.clone(), arg)
.ok()
.and_then(|e| match e {
Expr::Number(n) => Some(n),
_ => None,
})
.collect()
}
pub(super) fn try_fold_resolved(resolved: &[Expr]) -> Option<Vec<Fraction>> {
resolved
.iter()
.map(|e| match e {
Expr::Number(n) => Some(*n),
fn all_integers(nums: &[Fraction]) -> bool {
nums.iter().all(|n| *n.denom() == 1)
/// The runtime dimension of a resolved operand, or `None` for a numeric literal
/// (a dimension-flexible token).
fn operand_dim(r: &Expr) -> Result<Option<WasmType>> {
match r {
Expr::Number(_) => Ok(None),
_ => match r.wasm_type() {
Some(t @ (WasmType::I32 | WasmType::Ratio | WasmType::Commodity)) => Ok(Some(t)),
_ => Err(Error::Compile(format!(
"expected number arguments, got {}",
format_expr(r)
))),
},
/// Mirror codegen's refusal (`emit_literal` / `additive_dim`) to mix a runtime
/// Index with any Scalar — a runtime Ratio OR a fractional literal. Integer
/// literals stay flexible (they coerce to the Index). Without this the eval
/// surface would optimistically predict `I32` for `(+ IDX 1/2)` while codegen
/// rejects it — eval↔codegen drift. The only legal crossing is an explicit
/// `index->scalar` / `scalar->index` bridge.
fn reject_index_scalar_mix(resolved: &[Expr]) -> Result<()> {
let has_runtime_index = resolved
.any(|r| !matches!(r, Expr::Number(_)) && r.wasm_type() == Some(WasmType::I32));
if !has_runtime_index {
return Ok(());
let has_scalar = resolved.iter().any(|r| match r {
Expr::Number(n) => *n.denom() != 1,
other => other.wasm_type() == Some(WasmType::Ratio),
});
if has_scalar {
return Err(Error::Compile(
"cannot mix an index (count) with a scalar; bridge with \
index->scalar / scalar->index"
.to_string(),
));
Ok(())
/// The dominant runtime dimension across operands: Money > Scalar > Index;
/// all-literal defaults to Scalar. Used for `+ - * MOD`, whose result is the
/// single shared dimension in the legal (non-mixed) case.
fn dominant_result_type(resolved: &[Expr]) -> Result<WasmType> {
reject_index_scalar_mix(resolved)?;
let mut money = false;
let mut scalar = false;
let mut index = false;
for r in resolved {
match operand_dim(r)? {
Some(WasmType::Commodity) => money = true,
Some(WasmType::Ratio) => scalar = true,
Some(WasmType::I32) => index = true,
_ => {}
Ok(if money {
WasmType::Commodity
} else if scalar {
WasmType::Ratio
} else if index {
WasmType::I32
} else {
/// The result dimension of `/`, mirroring codegen's LEFT-associative fold
/// (`combine_div`): Index÷Index→Index, Scalar÷Scalar→Scalar, Money÷Scalar→Money,
/// Money÷Money→Scalar. A dominant-type shortcut would drift from codegen for
/// chains like `(/ money scalar money)` (→ Scalar, not Money).
fn div_result_type(resolved: &[Expr]) -> Result<WasmType> {
// The result dimension is the LEFT operand's dimension, mirroring codegen's
// left-associative `combine_div`: Index÷Index→Index, Scalar÷*→Scalar, and
// (ADR-0028 E2) Money÷anything→Money — money ÷ money no longer collapses to
// Ratio, it stays Money carrying a dimensionless/compound unit term, in
// lockstep with `commodity_div`. A leading bare literal seeds Index only
// when the dominant runtime dim is Index, else Scalar.
let leading = operand_dim(&resolved[0])?;
Ok(match leading {
Some(t) => t,
None if dominant_result_type(resolved)? == WasmType::I32 => WasmType::I32,
None => WasmType::Ratio,
/// The `Fraction` (`Ratio<i64>`) operators panic (debug / `overflow-checks`) or
/// wrap (release) when a cross-multiply exceeds i64 — reachable from any
/// all-literal `(* huge huge)` / `(+ a/b c/d)` whose reduced terms overflow. So
/// const-fold through the `Checked*` traits and surface a structured
/// `Error::Compile` instead, per CLAUDE.md (never a panic / SIGABRT on input).
fn overflow() -> Error {
Error::Compile("arithmetic overflow in constant expression".to_string())
pub(super) fn fold_add(nums: &[Fraction]) -> Result<Fraction> {
nums.iter().try_fold(Fraction::from_integer(0), |a, b| {
a.checked_add(b).ok_or_else(overflow)
pub(super) fn fold_sub(nums: &[Fraction]) -> Result<Fraction> {
if nums.len() == 1 {
return Fraction::from_integer(0)
.checked_sub(&nums[0])
.ok_or_else(overflow);
nums[1..]
.try_fold(nums[0], |a, b| a.checked_sub(b).ok_or_else(overflow))
pub(super) fn fold_mul(nums: &[Fraction]) -> Result<Fraction> {
nums.iter().try_fold(Fraction::from_integer(1), |a, b| {
a.checked_mul(b).ok_or_else(overflow)
/// Const-fold `/`: all-integer operands divide as Index (truncating toward
/// zero, matching `i32.div_s`); any fractional operand divides rationally.
pub(super) fn fold_div(nums: &[Fraction]) -> Result<Fraction> {
if *nums[0].numer() == 0 {
return Err(Error::Compile("division by zero".to_string()));
return Ok(if all_integers(nums) {
// `1 / numer` can't overflow, but keep the lattice's truncating
// Index semantics.
Fraction::from_integer(1 / *nums[0].numer())
// `recip` swaps numer/denom — infallible for a non-zero ratio.
nums[0].recip()
if all_integers(nums) {
let mut acc = *nums[0].numer();
for n in &nums[1..] {
if *n.numer() == 0 {
// `i64::MIN / -1` overflows — `checked_div` catches it.
acc = acc.checked_div(*n.numer()).ok_or_else(overflow)?;
return Ok(Fraction::from_integer(acc));
let mut acc = nums[0];
// Rational division cross-multiplies — can overflow i64.
acc = acc.checked_div(n).ok_or_else(overflow)?;
Ok(acc)
/// Const-fold `MOD`: all-integer operands use `rem` (sign of the dividend,
/// matching `i32.rem_s`); fractional operands use floored modulo.
pub(super) fn fold_mod(nums: &[Fraction]) -> Result<Fraction> {
if *nums[1].numer() == 0 {
return Err(Error::Compile("division by zero in MOD".to_string()));
// Raw `%` panics on `i64::MIN % -1` (overflow). wasm `i32.rem_s` defines
// that case as 0 (it does NOT trap, unlike `i32.div_s`), so `checked_rem`
// returning `None` there folds to 0 — keeping the eval surface in lockstep
// with `i32.rem_s` codegen rather than panicking the compiler.
let r = nums[0].numer().checked_rem(*nums[1].numer()).unwrap_or(0);
return Ok(Fraction::from_integer(r));
// Floored modulo `a - b*floor(a/b)` cross-multiplies twice — guard both.
let quotient = nums[0].checked_div(&nums[1]).ok_or_else(overflow)?.floor();
let scaled = nums[1].checked_mul("ient).ok_or_else(overflow)?;
nums[0].checked_sub(&scaled).ok_or_else(overflow)
fn resolve_all(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Vec<Expr>> {
args.iter().map(|arg| eval_value(symbols, arg)).collect()
pub(super) fn add(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
let resolved = resolve_all(symbols, args)?;
if let Some(nums) = try_fold_resolved(&resolved) {
return Ok(Expr::Number(fold_add(&nums)?));
Ok(Expr::WasmRuntime(dominant_result_type(&resolved)?))
pub(super) fn sub(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
if args.is_empty() {
return Err(Error::Compile("- requires at least 1 argument".to_string()));
return Ok(Expr::Number(fold_sub(&nums)?));
pub(super) fn mul(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
return Ok(Expr::Number(fold_mul(&nums)?));
pub(super) fn div(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
return Err(Error::Compile("/ requires at least 1 argument".to_string()));
return Ok(Expr::Number(fold_div(&nums)?));
Ok(Expr::WasmRuntime(div_result_type(&resolved)?))
pub(super) fn modulo(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
if args.len() != 2 {
return Err(Error::Arity {
name: "MOD".to_string(),
expected: 2,
actual: args.len(),
return Ok(Expr::Number(fold_mod(&nums)?));