Lines
79.38 %
Functions
43.33 %
Branches
100 %
//! Shared helpers for the comparison family — the `NATIVES` registry
//! const, eval/compile dispatchers, the bool-emit helpers, and the
//! type-classification and stack-coercion helpers consumed by both
//! `ordering` and `predicates`.
use crate::ast::{Expr, Fraction, WasmType};
use crate::compiler::context::CompileContext;
use crate::compiler::emit::FunctionEmitter;
use crate::compiler::expr::{
compile_bool, compile_for_stack, compile_for_stack_as, compile_nil, eval_value, format_expr,
};
use crate::error::{Error, Result};
use crate::runtime::SymbolTable;
use super::super::NativeSpec;
use super::ordering::{
compile_eq, compile_eq_to_stack, compile_eql, compile_equal, compile_equal_to_stack,
compile_ge, compile_ge_to_stack, compile_gt, compile_gt_to_stack, compile_le,
compile_le_to_stack, compile_lt, compile_lt_to_stack, compile_neq_to_stack, compile_num_neq,
eql, equal, eval_eq, eval_ge, eval_gt, eval_le, eval_lt, num_neq,
use super::predicates::{
compile_not, compile_not_to_stack, compile_null_p, compile_null_p_to_stack, not_fn, null_p,
pub(in crate::compiler::native) const NATIVES: &[NativeSpec] = &[
NativeSpec {
name: "=",
eval: eval_eq,
stack: Some(compile_eq_to_stack),
effect: Some(compile_eq),
},
name: "/=",
eval: num_neq,
stack: Some(compile_neq_to_stack),
effect: Some(compile_num_neq),
name: "<",
eval: eval_lt,
stack: Some(compile_lt_to_stack),
effect: Some(compile_lt),
name: ">",
eval: eval_gt,
stack: Some(compile_gt_to_stack),
effect: Some(compile_gt),
name: "<=",
eval: eval_le,
stack: Some(compile_le_to_stack),
effect: Some(compile_le),
name: ">=",
eval: eval_ge,
stack: Some(compile_ge_to_stack),
effect: Some(compile_ge),
name: "EQL",
eval: eql,
stack: Some(compile_equal_to_stack),
effect: Some(compile_eql),
name: "EQUAL",
eval: equal,
effect: Some(compile_equal),
// Scheme-spelled aliases of the CL equality predicates — same
// structural comparison, registered so the documented `EQ?` / `EQUAL?`
// natives are not dead names the compiler rejects at codegen.
name: "EQ?",
name: "EQUAL?",
name: "NOT",
eval: not_fn,
stack: Some(compile_not_to_stack),
effect: Some(compile_not),
name: "NULL?",
eval: null_p,
stack: Some(compile_null_p_to_stack),
effect: Some(compile_null_p),
];
pub(in crate::compiler::native) fn bool_result(value: bool) -> Expr {
if value { Expr::Bool(true) } else { Expr::Nil }
}
pub(super) fn emit_bool(ctx: &mut CompileContext, emit: &mut FunctionEmitter, value: bool) {
if value {
compile_bool(ctx, emit, true);
} else {
compile_nil(ctx, emit);
pub(super) fn has_runtime(resolved: &[Expr]) -> bool {
resolved.iter().any(crate::ast::Expr::is_wasm_runtime)
pub(super) fn resolve_all(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Vec<Expr>> {
args.iter().map(|arg| eval_value(symbols, arg)).collect()
pub(super) fn extract_numbers(resolved: &[Expr]) -> Option<Vec<Fraction>> {
resolved
.iter()
.map(|e| match e {
Expr::Number(n) => Some(*n),
_ => None,
})
.collect()
pub(super) fn validate_cmp_args(resolved: &[Expr], name: &str) -> Result<WasmType> {
let mut seen_ratio = false;
let mut seen_i32 = false;
let mut seen_int_const = false;
let mut seen_commodity = false;
for r in resolved {
match r {
Expr::Number(n) if *n.denom() == 1 => seen_int_const = true,
Expr::Number(_) => seen_ratio = true,
_ if r.wasm_type() == Some(WasmType::Ratio) => seen_ratio = true,
_ if r.wasm_type() == Some(WasmType::Commodity) => seen_commodity = true,
_ if r.wasm_type() == Some(WasmType::I32) => seen_i32 = true,
other => {
return Err(Error::Compile(format!(
"{name} expects numeric arguments, got {}",
format_expr(other)
)));
if seen_commodity && (seen_ratio || seen_i32 || seen_int_const) {
"{name} cannot compare a commodity-bearing value with a pure-rational \
or index value (a bare literal has no currency); compare two \
same-currency money values, or `(convert-commodity ...)` to bridge"
if seen_commodity {
return Ok(WasmType::Commodity);
// ADR-0028: a RUNTIME index (count) never implicitly promotes to Scalar.
// Mixing it with a Scalar is a compile error (the implicit I32→Ratio bridge
// is removed); cross explicitly with `(index->scalar ...)`.
if seen_ratio && seen_i32 {
"{name} cannot compare a runtime index (count) with a scalar; \
bridge explicitly with `(index->scalar ...)`"
if seen_i32 || (seen_int_const && !seen_ratio) {
Ok(WasmType::I32)
Ok(WasmType::Ratio)
/// Push a comparison operand as a raw i32 Index. ADR-0028: a runtime I32 / an
/// integer literal is accepted; delegates to the canonical `compile_for_stack_as`
/// (clone-probe, non-literal effect preservation, i32-range check) — the single
/// source of truth. `validate_cmp_args` has already rejected non-numeric/Bool/Nil
/// operands before this is reached.
pub(super) fn compile_for_stack_i32(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
expr: &Expr,
) -> Result<()> {
compile_for_stack_as(ctx, emit, symbols, expr, WasmType::I32)
/// Push a comparison operand as a Ratio. ADR-0028: a numeric LITERAL coerces
/// across the Index↔Scalar boundary, but a RUNTIME index never does (the
/// implicit I32→Ratio bridge is removed) — delegates to the canonical
/// `compile_for_stack_as`, the single source of truth for that coercion.
pub(super) fn compile_for_stack_as_ratio(
compile_for_stack_as(ctx, emit, symbols, expr, WasmType::Ratio)
/// Push a value onto the WASM stack asserting it is a Commodity.
/// Refuses every other type — commodity comparisons must be homogeneous.
pub(super) fn compile_for_stack_as_commodity(
let ty = compile_for_stack(ctx, emit, symbols, expr)?;
match ty {
WasmType::Commodity => Ok(()),
_ => Err(Error::Compile(format!(
"commodity comparison requires commodity-bearing operand, got {ty}"
))),
pub(super) fn emit_i32_cmp(emit: &mut FunctionEmitter, name: &str) {
match name {
"=" => emit.i32_eq(),
"<" => emit.i32_lt_s(),
_ => unreachable!("unsupported i32 comparison: {name}"),