Lines
91.01 %
Functions
31.37 %
Branches
100 %
//! Result-type reconciliation shared by the multi-arm special forms (IF,
//! HANDLER-CASE). Each reachable arm is an already-compiled value, a
//! dimension-flexible numeric literal, or `nil`; the latter two are emitted only
//! once the unified block type is known, so a literal crosses Index↔Scalar
//! (ADR-0028) and a `nil` beside a list stays a null pair.
use crate::ast::{Expr, PairElement, WasmType};
use crate::compiler::context::CompileContext;
use crate::compiler::emit::FunctionEmitter;
use crate::compiler::expr::{
classify_stack_type, compile_for_effect, compile_for_stack, compile_for_stack_as,
emit_box_anyref, emit_relabel_chain, eval_value,
};
use crate::compiler::native::{chain_pair_element, compile_constant_list};
use crate::error::{Error, Result};
use crate::runtime::SymbolTable;
use super::block_exits::form_diverges;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Arm {
/// Compiled: a value of this type is already on the arm's stack.
Value(WasmType),
/// A numeric literal of this default stratum, not yet emitted.
NumberLiteral(WasmType),
/// A form resolving to `nil`, not yet emitted.
Nil,
/// A form resolving to a constant list of this chain element, materialized
/// at the reconciled element once the result is known.
ConstantList(PairElement),
}
/// How genuinely disagreeing arm types unify.
#[derive(Clone, Copy)]
pub(super) enum OnConflict {
Widen,
Reject(&'static str),
/// Compiles one arm into `scratch`: a diverging arm emits its exit and yields
/// `None`; a literal / `nil` arm is deferred to [`finish_arm`]; any other arm
/// is compiled now as a [`Arm::Value`].
pub(super) fn compile_arm(
ctx: &mut CompileContext,
scratch: &mut FunctionEmitter,
symbols: &mut SymbolTable,
expr: &Expr,
) -> Result<Option<Arm>> {
if form_diverges(&mut symbols.clone(), expr)? {
compile_for_stack(ctx, scratch, symbols, expr)?;
return Ok(None);
if let Some(arm) = deferred_arm(symbols, expr) {
return Ok(Some(arm));
Ok(Some(Arm::Value(compile_for_stack(
ctx, scratch, symbols, expr,
)?)))
/// The deferred arm of a non-diverging `expr` that resolves to a numeric
/// literal or `nil`; `None` when the arm must be compiled up front. Probes on a
/// clone so no compile-time effect is applied.
fn deferred_arm(symbols: &SymbolTable, expr: &Expr) -> Option<Arm> {
if ends_in_nil(expr) {
return Some(Arm::Nil);
match eval_value(&mut symbols.clone(), expr).ok()? {
Expr::Nil => Some(Arm::Nil),
Expr::Quote(inner) => match *inner {
Expr::List(members) => chain_pair_element(&members).ok().map(Arm::ConstantList),
_ => None,
},
value => literal_arm(&value),
/// Whether `expr` is a BEGIN / PROGN whose value form is literally `nil`: its
/// value is nil whatever runtime effects precede it.
fn ends_in_nil(expr: &Expr) -> bool {
let mut form = expr;
while let Expr::List(elems) = form
&& let (Some(Expr::Symbol(head)), Some(last)) = (elems.first(), elems.last())
&& elems.len() > 1
&& matches!(head.as_str(), "BEGIN" | "PROGN")
{
if matches!(last, Expr::Nil) {
return true;
form = last;
false
/// The arm a non-diverging `expr` contributes on the ctx-less eval surface,
/// peeked on a clone — the mirror of [`compile_arm`]. A constant list lowers to
/// a pair chain typed by the LIST rule; an unclassifiable value is `I32`.
pub(super) fn eval_arm(symbols: &SymbolTable, expr: &Expr) -> Result<Option<Arm>> {
let value = eval_value(&mut symbols.clone(), expr)?;
Ok(Some(arm_of_form(expr, &value)?))
/// The arm of a form whose resolved eval value is `value`.
pub(super) fn arm_of_form(expr: &Expr, value: &Expr) -> Result<Arm> {
return Ok(Arm::Nil);
arm_of_value(value)
/// The arm a resolved eval value contributes.
fn arm_of_value(value: &Expr) -> Result<Arm> {
match value {
Expr::Nil => Ok(Arm::Nil),
Expr::Number(_) => Ok(literal_arm(value).unwrap_or(Arm::Value(WasmType::I32))),
Expr::Quote(inner) => match inner.as_ref() {
Expr::List(members) => Ok(Arm::Value(WasmType::PairRef(chain_pair_element(members)?))),
_ => Ok(Arm::Value(WasmType::I32)),
_ => Ok(Arm::Value(
classify_stack_type(value).unwrap_or(WasmType::I32),
)),
fn literal_arm(value: &Expr) -> Option<Arm> {
Expr::Number(_) => classify_stack_type(value).map(Arm::NumberLiteral),
/// Unifies reachable arms: compiled values join (pair elements widen), a
/// numeric literal adopts an Index/Scalar value's stratum, and `nil` rides a
/// list / anyref / bool result. No reachable arm is the polymorphic `I32`.
pub(super) fn reconcile_arms(arms: &[Arm], on_conflict: OnConflict) -> Result<WasmType> {
let mut value: Option<WasmType> = None;
let mut literal: Option<WasmType> = None;
let mut nil = false;
for arm in arms {
match *arm {
Arm::Value(ty) => value = Some(joined(value, ty, on_conflict)?),
Arm::ConstantList(elem) => {
value = Some(joined(value, WasmType::PairRef(elem), on_conflict)?);
Arm::NumberLiteral(ty) => {
literal = Some(match literal {
Some(prev) if prev != ty => WasmType::Ratio,
_ => ty,
});
Arm::Nil => nil = true,
let typed = match (value, literal) {
(base, None) => base,
(None, lit) => lit,
(Some(WasmType::AnyRef), Some(_)) => Some(WasmType::AnyRef),
(Some(base), Some(lit)) if literal_fits(base, lit) => Some(base),
(Some(base), Some(lit)) => Some(join(base, lit, on_conflict)?),
match (typed, nil) {
(ty, false) => Ok(ty.unwrap_or(WasmType::I32)),
(None, true) => Ok(WasmType::Bool),
(Some(ty @ (WasmType::PairRef(_) | WasmType::AnyRef | WasmType::Bool)), true) => Ok(ty),
(Some(ty), true) => nil_beside(ty, on_conflict),
/// A nil arm beside a result that cannot be nil (an index, scalar, money,
/// string, …) widens the block to `AnyRef`, or is rejected by name.
fn nil_beside(ty: WasmType, on_conflict: OnConflict) -> Result<WasmType> {
match on_conflict {
OnConflict::Widen => Ok(WasmType::AnyRef),
OnConflict::Reject(form) => Err(Error::Compile(format!(
"{form}: a nil arm cannot join a {ty} result"
))),
fn join(a: WasmType, b: WasmType, on_conflict: OnConflict) -> Result<WasmType> {
match (a, b, on_conflict) {
_ if a == b => Ok(a),
(WasmType::PairRef(x), WasmType::PairRef(y), _) => Ok(WasmType::PairRef(x.widen(y))),
(_, _, OnConflict::Widen) => Ok(WasmType::AnyRef),
(_, _, OnConflict::Reject(form)) => Err(Error::Compile(format!(
"{form}: conflicting result types {a} and {b}"
fn joined(prev: Option<WasmType>, ty: WasmType, on_conflict: OnConflict) -> Result<WasmType> {
match prev {
Some(prev) => join(prev, ty, on_conflict),
None => Ok(ty),
/// Whether a literal of stratum `lit` can adopt `base`: an integer literal
/// becomes an index or a scalar, a fractional one only a scalar, and no literal
/// becomes Money (it has no currency).
fn literal_fits(base: WasmType, lit: WasmType) -> bool {
matches!(
(base, lit),
(WasmType::I32 | WasmType::Ratio, WasmType::I32) | (WasmType::Ratio, WasmType::Ratio)
)
/// Completes an arm into its `scratch` at the unified `result`: a compiled
/// value is boxed when the block is `AnyRef`, a deferred literal is compiled at
/// the result stratum, a deferred `nil` emits its effects then the typed nil.
pub(super) fn finish_arm(
arm: Arm,
result: WasmType,
) -> Result<()> {
match arm {
Arm::Value(ty) => {
box_arm_value(ctx, scratch, ty, result);
Ok(())
Arm::NumberLiteral(lit) if literal_fits(result, lit) => {
compile_for_stack_as(ctx, scratch, symbols, expr, result)
Arm::NumberLiteral(_) => {
let raw = compile_for_stack(ctx, scratch, symbols, expr)?;
box_arm_value(ctx, scratch, raw, result);
Arm::Nil => {
if !matches!(expr, Expr::Nil) {
compile_for_effect(ctx, scratch, symbols, expr)?;
emit_typed_nil(ctx, scratch, result)
Arm::ConstantList(own) => {
let elem = match result {
WasmType::PairRef(elem) => elem,
_ => own,
let members = constant_list_members(symbols, expr)?;
compile_constant_list(ctx, scratch, symbols, members, elem)?;
/// The members of a constant-list arm, resolved on the live table so its
/// compile-time effects apply exactly once (a constant list has no runtime ones).
fn constant_list_members(symbols: &mut SymbolTable, expr: &Expr) -> Result<Vec<Expr>> {
match eval_value(symbols, expr)? {
Expr::List(members) => Ok(members),
other => Err(not_a_constant_list(&other)),
fn not_a_constant_list(value: &Expr) -> Error {
Error::Compile(format!(
"constant-list arm resolved to {value:?}, not a list"
))
/// Retypes an already-emitted arm value of `ty` to the block `result`: an
/// i32-repr value boxes into an anyref block; a bool chain is rebuilt before it
/// rides a widened chain.
fn box_arm_value(
ctx: &CompileContext,
ty: WasmType,
) {
match (ty, result) {
(_, WasmType::AnyRef) => emit_box_anyref(ctx, scratch, ty),
(WasmType::PairRef(from), WasmType::PairRef(to)) => {
emit_relabel_chain(ctx, scratch, from, to);
_ => {}
/// Pushes `nil` typed as the block `result`: a null pair, a null anyref, or a
/// falsy i32.
pub(super) fn emit_typed_nil(
match result {
WasmType::PairRef(_) => scratch.ref_null(ctx.ids.ty_pair),
WasmType::AnyRef => scratch.ref_null_any(),
WasmType::I32 | WasmType::Bool => scratch.i32_const(0),
other => {
return Err(Error::Compile(format!(
"nil cannot be typed as a {other} result"
)));