Lines
74.13 %
Functions
27.69 %
Branches
100 %
//! `AND` / `OR` short-circuit boolean special forms. Each provides
//! three compile paths (effect, stack, runtime short-circuit) plus
//! the constant-folding eval path. The `_runtime` helpers handle
//! the mixed compile-time-known + runtime-tail case where the prefix
//! folds but a later arg is a runtime value.
use wasm_encoder::BlockType;
use crate::ast::{Expr, WasmType};
use crate::compiler::context::CompileContext;
use crate::compiler::emit::FunctionEmitter;
use crate::compiler::expr::{
compile_expr, compile_for_effect, compile_for_stack, compile_nil, eval_value,
serialize_stack_to_output,
};
use crate::error::{Error, Result};
use crate::runtime::SymbolTable;
use super::is_truthy;
/// A runtime `and`/`or` short-circuit chain lowers each operand into an
/// `if (result i32)` whose other edge is a 0/1 filler, so every operand value
/// must be i32-shaped (`I32` or `Bool`). A ref-typed operand (Ratio / String /
/// Pair / …) can't ride that block, and forcing it through would emit a
/// malformed module. Reject it with a structured error instead — the strict
/// numeric/bool lattice (ADR-0014) does not implicitly truthify ref values.
fn require_i32_shaped(op: &str, ty: WasmType) -> Result<()> {
match ty {
WasmType::I32 | WasmType::Bool => Ok(()),
other => Err(error_ref_operand(op, other)),
}
/// Reject a *runtime* operand that isn't an i32-shaped truth value, mirroring
/// IF/COND's [`reject_non_boolean_runtime_test`]: nomiscript does NOT do
/// implicit truthiness for runtime ref values (a runtime ref may be null, so
/// "always truthy" would silently mis-branch). A ref must be tested explicitly
/// (e.g. `(null? x)`). Const-foldable values pass through untouched — they
/// fold via [`is_truthy`]. Returns the operand's i32-shaped type when it IS a
/// valid runtime truth value, so callers can keep routing through the chain.
fn classify_runtime_operand(op: &str, value: &Expr) -> Result<Option<WasmType>> {
match value {
Expr::WasmRuntime(t @ (WasmType::I32 | WasmType::Bool))
| Expr::WasmLocal(_, t @ (WasmType::I32 | WasmType::Bool)) => Ok(Some(*t)),
Expr::WasmRuntime(ty) | Expr::WasmLocal(_, ty) => Err(error_ref_operand(op, *ty)),
_ => Ok(None),
fn error_ref_operand(op: &str, ty: WasmType) -> Error {
Error::Compile(format!(
"{op}: runtime operands must be boolean / i32 truth values, got {ty}; \
wrap a ref-typed value in an explicit predicate (e.g. `(null? …)`)"
))
/// Result type of a unary runtime `(and x)` / `(or x)` ≡ `x`. The operand is
/// always i32-shaped here — ref operands are rejected upstream by
/// [`classify_runtime_operand`] — and is TRUTHIFIED to `Bool`: `and`/`or` are
/// truth-value producers, so `(and <count>)` answers "is the count truthy" and
/// must serialize as Nil/Bool, not Number (the `v != 0` the Bool serializer
/// computes is exactly that test). Codegen and the eval path share this so a
/// binder sizing the result agrees with the stack value.
fn unary_runtime_type(ty: WasmType) -> WasmType {
WasmType::I32 | WasmType::Bool => WasmType::Bool,
other => other,
pub(super) fn compile_and(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
args: &[Expr],
) -> Result<()> {
if args.is_empty() {
return compile_expr(ctx, emit, symbols, &Expr::Bool(true));
let mut last = Expr::Bool(true);
for (i, arg) in args.iter().enumerate() {
let value = eval_value(symbols, arg)?;
if classify_runtime_operand("AND", &value)?.is_some() {
// Emit ONE merged `if (result T)` chain via the stack path and
// serialize the single result. The old effect-only short-circuit
// helper produced NO value on the early-exit branch (a top-level
// `(and …)` / `(or …)` that short-circuited emitted no output
// entity). The stack path yields a value on every path. Same
// remedy as the IF/COND single-serialization fix.
let ty = compile_and_for_stack(ctx, emit, symbols, &args[i..])?;
return serialize_stack_to_output(ctx, emit, ty);
if !is_truthy(&value) {
return compile_expr(ctx, emit, symbols, &value);
last = value;
compile_expr(ctx, emit, symbols, &last)
pub(super) fn compile_or(
compile_nil(ctx, emit);
return Ok(());
let mut last = Expr::Nil;
if classify_runtime_operand("OR", &value)?.is_some() {
// See `compile_and`: route through the value-producing stack path
// so the short-circuit branch still yields a serializable result.
let ty = compile_or_for_stack(ctx, emit, symbols, &args[i..])?;
if is_truthy(&value) {
pub(super) fn compile_and_for_stack(
) -> Result<WasmType> {
emit.i32_const(1);
return Ok(WasmType::Bool);
if args.len() == 1 {
classify_runtime_operand("AND", &eval_value(symbols, &args[0])?)?;
let ty = compile_for_stack(ctx, emit, symbols, &args[0])?;
return Ok(unary_runtime_type(ty));
let first_ty = compile_for_stack(ctx, emit, symbols, &args[0])?;
require_i32_shaped("AND", first_ty)?;
for arg in &args[1..] {
emit.if_block(BlockType::Result(wasm_encoder::ValType::I32));
let arg_ty = compile_for_stack(ctx, emit, symbols, arg)?;
require_i32_shaped("AND", arg_ty)?;
emit.else_block();
emit.i32_const(0);
emit.block_end();
Ok(WasmType::Bool)
pub(super) fn compile_or_for_stack(
classify_runtime_operand("OR", &eval_value(symbols, &args[0])?)?;
require_i32_shaped("OR", first_ty)?;
emit.i32_eqz();
require_i32_shaped("OR", arg_ty)?;
pub(super) fn compile_and_for_effect(
// i32-shaped runtime condition: the rest runs only if it's truthy.
compile_for_stack(ctx, emit, symbols, arg)?;
emit.if_block(BlockType::Empty);
for remaining in &args[i + 1..] {
compile_for_effect(ctx, emit, symbols, remaining)?;
Ok(())
pub(super) fn compile_or_for_effect(
// i32-shaped runtime condition: the rest runs only if it's falsy.
pub(super) fn and_form(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
return Ok(Expr::Bool(true));
if let Some(t) = classify_runtime_operand("AND", &value)? {
return Ok(Expr::WasmRuntime(runtime_form_type(args.len() - i, t)));
return Ok(value);
Ok(last)
pub(super) fn or_form(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
return Ok(Expr::Nil);
if let Some(t) = classify_runtime_operand("OR", &value)? {
/// Eval-time mirror of the codegen result type for a runtime `and`/`or`.
/// `compile_and`/`compile_or` route the suffix `&args[i..]` through the
/// for-stack path, so the type depends on the REMAINING arg count: a lone
/// runtime tail (`remaining == 1`) is `unary_runtime_type` (the value ≡ `x`),
/// a longer chain always coerces to `Bool` (the `if`-chain emits 0/1 fillers).
fn runtime_form_type(remaining: usize, runtime_ty: WasmType) -> WasmType {
match remaining {
1 => unary_runtime_type(runtime_ty),
_ => WasmType::Bool,