Lines
74.75 %
Functions
33.33 %
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;
/// 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 [`Expr::is_nil`]. 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? …)`)"
))
/// Compiles an operand as a 0/1 truth value: a bool as itself, an index — a
/// count, never nil — for its effects and then true, matching the constant
/// [`Expr::is_nil`] fold.
fn compile_truth(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
op: &str,
arg: &Expr,
) -> Result<()> {
let ty = compile_for_stack(ctx, emit, symbols, arg)?;
require_i32_shaped(op, ty)?;
if ty == WasmType::I32 {
emit.drop_value();
emit.i32_const(1);
Ok(())
pub(super) fn compile_and(
args: &[Expr],
if args.is_empty() {
return compile_expr(ctx, emit, symbols, &Expr::T);
let mut last = Expr::T;
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 value.is_nil() {
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 !value.is_nil() {
pub(super) fn compile_and_for_stack(
) -> Result<WasmType> {
return Ok(WasmType::Bool);
if args.len() == 1 {
classify_runtime_operand("AND", &eval_value(symbols, &args[0])?)?;
compile_truth(ctx, emit, symbols, "AND", &args[0])?;
for arg in &args[1..] {
emit.if_block(BlockType::Result(wasm_encoder::ValType::I32));
compile_truth(ctx, emit, symbols, "AND", arg)?;
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])?)?;
compile_truth(ctx, emit, symbols, "OR", &args[0])?;
emit.i32_eqz();
compile_truth(ctx, emit, symbols, "OR", arg)?;
pub(super) fn compile_and_for_effect(
// i32-shaped runtime condition: the rest runs only if it's truthy.
emit.if_block(BlockType::Empty);
for remaining in &args[i + 1..] {
compile_for_effect(ctx, emit, symbols, remaining)?;
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::T);
for arg in args {
return Ok(Expr::WasmRuntime(WasmType::Bool));
return Ok(value);
Ok(last)
pub(super) fn or_form(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
return Ok(Expr::Nil);