Lines
60 %
Functions
21.63 %
Branches
100 %
use super::super::context::CompileContext;
use super::super::emit::FunctionEmitter;
use super::super::expr::{
compile_bool, compile_for_stack, compile_nil, eval_value, format_expr,
serialize_stack_to_output,
};
use crate::ast::{Expr, Fraction, WasmType};
use crate::error::{Error, Result};
use crate::runtime::SymbolTable;
pub(super) fn bool_result(value: bool) -> Expr {
if value { Expr::Bool(true) } else { Expr::Nil }
}
fn emit_bool(ctx: &mut CompileContext, emit: &mut FunctionEmitter, value: bool) {
if value {
compile_bool(ctx, emit, true);
} else {
compile_nil(ctx, emit);
fn has_runtime(resolved: &[Expr]) -> bool {
resolved.iter().any(crate::ast::Expr::is_wasm_runtime)
fn resolve_all(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Vec<Expr>> {
args.iter().map(|arg| eval_value(symbols, arg)).collect()
fn extract_numbers(resolved: &[Expr]) -> Option<Vec<Fraction>> {
resolved
.iter()
.map(|e| match e {
Expr::Number(n) => Some(*n),
_ => None,
})
.collect()
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;
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::I32) => seen_i32 = true,
other => {
return Err(Error::Compile(format!(
"{name} expects numeric arguments, got {}",
format_expr(other)
)));
// When mixing I32 and Ratio, promote I32 to Ratio via ratio_from_i64
if seen_ratio && seen_i32 {
return Ok(WasmType::Ratio);
if seen_i32 || (seen_int_const && !seen_ratio) {
Ok(WasmType::I32)
Ok(WasmType::Ratio)
/// Push a value onto the WASM stack as an i32.
/// Handles WasmRuntime(I32), `WasmLocal`(_, I32), integer constants, and Bool/Nil.
fn compile_for_stack_i32(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
expr: &Expr,
) -> Result<()> {
let resolved = eval_value(symbols, expr)?;
match &resolved {
Expr::WasmRuntime(WasmType::I32) => {
compile_for_stack(ctx, emit, symbols, expr)?;
Expr::WasmLocal(_, WasmType::I32) => {
Expr::Number(n) if *n.denom() == 1 => {
emit.i32_const(*n.numer() as i32);
Expr::Bool(b) => emit.i32_const(i32::from(*b)),
Expr::Nil => emit.i32_const(0),
_ => {
"cannot compile {} as i32",
format_expr(expr)
Ok(())
// --- Eval functions ---
pub(super) fn num_cmp(
args: &[Expr],
name: &str,
cmp: fn(&Fraction, &Fraction) -> bool,
) -> Result<Expr> {
if args.is_empty() {
"{name} requires at least 1 argument"
let resolved = resolve_all(symbols, args)?;
if let Some(nums) = extract_numbers(&resolved) {
let result = nums.windows(2).all(|w| cmp(&w[0], &w[1]));
return Ok(bool_result(result));
if has_runtime(&resolved) {
validate_cmp_args(&resolved, name)?;
return Ok(Expr::WasmRuntime(WasmType::I32));
Err(Error::Compile(format!("{name} expects numeric arguments")))
pub(super) fn num_neq(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
return Err(Error::Compile(
"/= requires at least 1 argument".to_string(),
));
let result = (0..nums.len()).all(|i| (i + 1..nums.len()).all(|j| nums[i] != nums[j]));
validate_cmp_args(&resolved, "/=")?;
Err(Error::Compile("/= expects numeric arguments".to_string()))
pub(super) fn eql(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
if args.len() != 2 {
return Err(Error::Arity {
name: "EQL".to_string(),
expected: 2,
actual: args.len(),
});
let a = eval_value(symbols, &args[0])?;
let b = eval_value(symbols, &args[1])?;
if a.is_wasm_runtime() || b.is_wasm_runtime() {
Ok(bool_result(a == b))
pub(super) fn equal(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
name: "EQUAL".to_string(),
// --- Compile functions (serializing) ---
pub(super) fn compile_num_cmp(
emit_bool(ctx, emit, result);
return Ok(());
let ty = compile_cmp_to_stack_by_name(ctx, emit, symbols, args, name, cmp)?;
serialize_stack_to_output(ctx, emit, ty);
pub(super) fn compile_num_neq(
let ty = compile_neq_to_stack(ctx, emit, symbols, args)?;
pub(super) fn compile_eql(
let ty = compile_eq_to_stack(ctx, emit, symbols, args)?;
emit_bool(ctx, emit, a == b);
pub(super) fn compile_equal(
// --- Stack compile functions (for sub-expressions) ---
fn ratio_cmp_helper(name: &str) -> &str {
match name {
"=" => "ratio_eq",
"<" => "ratio_lt",
_ => unreachable!("unsupported comparison: {name}"),
/// Push a value onto the WASM stack as a Ratio, promoting I32 if necessary.
fn compile_for_stack_as_ratio(
let ty = compile_for_stack(ctx, emit, symbols, expr)?;
match ty {
WasmType::Ratio => Ok(()),
WasmType::I32 => {
emit.i64_extend_i32_u();
emit.call(ctx.func("ratio_from_i64"));
WasmType::ConsRef | WasmType::StringRef => Err(Error::Compile(
"cannot use cons cell in numeric comparison".to_string(),
)),
fn emit_i32_cmp(emit: &mut FunctionEmitter, name: &str) {
"=" => emit.i32_eq(),
"<" => emit.i32_lt_s(),
_ => unreachable!("unsupported i32 comparison: {name}"),
pub(super) fn compile_eq_to_stack(
) -> Result<WasmType> {
compile_cmp_to_stack(ctx, emit, symbols, args, "=", |a, b| a == b)
pub(super) fn compile_neq_to_stack(
emit.i32_const(i32::from(result));
return Ok(WasmType::I32);
if args.len() == 2 {
let arg_ty = validate_cmp_args(&resolved, "/=")?;
match arg_ty {
WasmType::Ratio => {
compile_for_stack_as_ratio(ctx, emit, symbols, &args[0])?;
compile_for_stack_as_ratio(ctx, emit, symbols, &args[1])?;
emit.call(ctx.func("ratio_eq"));
emit.i32_eqz();
compile_for_stack_i32(ctx, emit, symbols, &args[0])?;
compile_for_stack_i32(ctx, emit, symbols, &args[1])?;
emit.i32_ne();
WasmType::ConsRef | WasmType::StringRef => {
"cannot compare cons cells with /=".to_string(),
Err(Error::Compile(
"/= with runtime args requires exactly 2 arguments".to_string(),
))
pub(super) fn compile_lt_to_stack(
compile_cmp_to_stack(ctx, emit, symbols, args, "<", |a, b| a < b)
pub(super) fn compile_gt_to_stack(
// a > b ≡ b < a
compile_cmp_to_stack_reversed(ctx, emit, symbols, args, ">", |a, b| a > b)
pub(super) fn compile_le_to_stack(
// a <= b ≡ !(b < a)
"<= requires exactly 2 arguments for runtime comparison".to_string(),
emit.i32_const(i32::from(nums[0] <= nums[1]));
let arg_ty = validate_cmp_args(&resolved, "<=")?;
emit.call(ctx.func("ratio_lt"));
emit.i32_le_s();
"cannot compare cons cells with <=".to_string(),
pub(super) fn compile_ge_to_stack(
// a >= b ≡ !(a < b)
">= requires exactly 2 arguments for runtime comparison".to_string(),
emit.i32_const(i32::from(nums[0] >= nums[1]));
let arg_ty = validate_cmp_args(&resolved, ">=")?;
emit.i32_ge_s();
"cannot compare cons cells with >=".to_string(),
fn compile_cmp_to_stack_by_name(
const_cmp: fn(&Fraction, &Fraction) -> bool,
"=" => compile_eq_to_stack(ctx, emit, symbols, args),
"<" => compile_lt_to_stack(ctx, emit, symbols, args),
">" => compile_gt_to_stack(ctx, emit, symbols, args),
"<=" => compile_le_to_stack(ctx, emit, symbols, args),
">=" => compile_ge_to_stack(ctx, emit, symbols, args),
_ => compile_cmp_to_stack(ctx, emit, symbols, args, name, const_cmp),
fn compile_cmp_to_stack(
let result = nums.windows(2).all(|w| const_cmp(&w[0], &w[1]));
"{name} with runtime args requires exactly 2 arguments"
let arg_ty = validate_cmp_args(&resolved, name)?;
emit.call(ctx.func(ratio_cmp_helper(name)));
emit_i32_cmp(emit, name);
WasmType::ConsRef | WasmType::StringRef => Err(Error::Compile(format!(
"cannot compare cons cells with {name}"
))),
fn compile_cmp_to_stack_reversed(
emit.i32_gt_s();
// --- NOT / NULL? ---
fn is_truthy(expr: &Expr) -> bool {
!matches!(expr, Expr::Nil | Expr::Bool(false))
pub(super) fn not_fn(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
if args.len() != 1 {
name: "NOT".to_string(),
expected: 1,
let val = eval_value(symbols, &args[0])?;
if val.wasm_type().is_some() {
Ok(bool_result(!is_truthy(&val)))
pub(super) fn null_p(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
name: "NULL?".to_string(),
Ok(bool_result(matches!(val, Expr::Nil)))
pub(super) fn compile_not(
compile_not_to_stack(ctx, emit, symbols, args)?;
serialize_stack_to_output(ctx, emit, WasmType::I32);
emit_bool(ctx, emit, !is_truthy(&val));
pub(super) fn compile_null_p(
compile_null_p_to_stack(ctx, emit, symbols, args)?;
emit_bool(ctx, emit, matches!(val, Expr::Nil));
pub(super) fn compile_not_to_stack(
match val.wasm_type() {
Some(WasmType::ConsRef | WasmType::StringRef) => {
compile_for_stack(ctx, emit, symbols, &args[0])?;
emit.ref_is_null();
Some(WasmType::I32) => {
_ => {}
emit.i32_const(i32::from(!is_truthy(&val)));
pub(super) fn compile_null_p_to_stack(
emit.i32_const(i32::from(matches!(val, Expr::Nil)));