Lines
82.72 %
Functions
36.92 %
Branches
100 %
//! Stack-position codegen.
//!
//! `compile_for_stack` is the entry — it emits an expression as a
//! single wasm stack value and returns the `WasmType` that landed on
//! the stack. The two numeric refinements (`compile_for_stack_ratio`,
//! `compile_for_stack_index`) wrap it for the Scalar / Index dispatch
//! in `compiler::native`. Function calls in value position
//! route through `compile_call_for_stack` and the symbol-call
//! dispatcher below.
use crate::ast::{Expr, WasmType};
use crate::compiler::context::CompileContext;
use crate::compiler::emit::FunctionEmitter;
use crate::error::{Error, Result};
use crate::runtime::{SymbolKind, SymbolTable};
use super::atoms::{emit_nil_default, push_ratio};
use super::call::{compile_call_ref, compile_lambda_call_for_stack, try_compile_runtime_call};
use super::effect::compile_for_effect;
use super::eval::{call, eval_value, expand_macro_then, resolve_arg};
use super::format::format_expr;
use super::quasiquote::expand_quasiquote;
pub(in crate::compiler) fn compile_for_stack(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
expr: &Expr,
) -> Result<WasmType> {
match expr {
Expr::Number(n) if *n.denom() == 1 => {
// ADR-0028: a dimensionless integer literal defaults to Index (I32).
// Operator/binding sites that need a Scalar coerce it explicitly.
emit.i32_const(i32::try_from(*n.numer()).map_err(|_| {
Error::Compile(format!("integer literal {} exceeds i32 range", n.numer()))
})?);
Ok(WasmType::I32)
}
Expr::Number(n) => {
push_ratio(ctx, emit, *n.numer(), *n.denom());
Ok(WasmType::Ratio)
Expr::Bool(b) => {
emit.i32_const(i32::from(*b));
Ok(WasmType::Bool)
Expr::Nil => {
emit.i32_const(0);
Expr::WasmRuntime(_) => Err(Error::Compile(
"internal: compile_for_stack saw an Expr::WasmRuntime placeholder. \
A binder failed to promote the runtime value into a WasmLocal \
(allocate a wasm local, emit the producer expression, local.set) \
before the symbol was referenced. Check the let / let* / dolist \
/ do / lambda-arg / defvar / defparameter call sites."
.to_string(),
)),
Expr::WasmLocal(idx, ty) => {
emit.local_get(*idx);
Ok(*ty)
Expr::String(s) => {
let data_idx = ctx.add_data(s.as_bytes())?;
emit.i32_const(s.len() as i32);
emit.array_new_data(ctx.ids.ty_i8_array, data_idx);
Ok(WasmType::StringRef)
Expr::Symbol(_) => {
let resolved = resolve_arg(symbols, expr)?;
compile_for_stack(ctx, emit, symbols, &resolved)
Expr::List(elems) if elems.is_empty() => {
Expr::List(elems) => compile_call_for_stack(ctx, emit, symbols, elems),
Expr::Quasiquote(inner) => {
let expanded = expand_quasiquote(symbols, inner)?;
compile_for_stack(ctx, emit, symbols, &expanded)
_ => Err(Error::Compile(format!(
"cannot compile to WASM stack value: {}",
format_expr(expr)
))),
/// The `WasmType` that [`compile_for_stack`] pushes for a literal or
/// already-resolved runtime `expr`, or `None` for a shape whose stack type is
/// only known by actually evaluating/emitting it (a call/list, a quote, a
/// lambda, …). This is the SINGLE SOURCE OF TRUTH for the compile-time
/// "stack-type mirrors" — the eval-side functions that PREDICT a form's stack
/// type to size let/do/binding locals or unify branch results. Every such
/// mirror routes through this so the prediction can't drift from what codegen
/// emits (the class of bug that produced the i32/Bool, list-element, and
/// recursion-type mismatches). The classified arms match `compile_for_stack`
/// exactly: a numeric literal is always a `Ratio` (never a count), bool/nil are
/// `Bool`, a string literal is `StringRef`, a runtime placeholder/local carries
/// its own type. NOTE the empty-list literal is intentionally `None` (not
/// `compile_for_stack`'s degenerate `()`→I32): a mirror that sees a bare
/// `Expr::List(vec![])` reaches it only as a non-value-position fallback and
/// keeps its own default — the reader emits `()` as `Nil`, so this case is
/// internal-only and never the actual stack value of a binding.
#[must_use]
pub(in crate::compiler) fn classify_stack_type(expr: &Expr) -> Option<WasmType> {
Expr::WasmRuntime(ty) | Expr::WasmLocal(_, ty) => Some(*ty),
// ADR-0028: an integer literal (denom == 1) defaults to Index (I32);
// a fractional literal is a dimensionless Scalar (Ratio). Mirrors the
// `denom() == 1` split in `compile_for_stack`.
Expr::Number(n) if *n.denom() == 1 => Some(WasmType::I32),
Expr::Number(_) => Some(WasmType::Ratio),
Expr::Bool(_) | Expr::Nil => Some(WasmType::Bool),
Expr::String(_) => Some(WasmType::StringRef),
_ => None,
pub(in crate::compiler) fn compile_for_stack_ratio(
) -> Result<()> {
// ADR-0028: a numeric literal is a dimension-flexible token — coerce it to
// Scalar (the sanctioned Index↔Scalar crossing). A RUNTIME index never
// coerces; it must bridge explicitly via `(index->scalar …)`. A non-literal
// operand that *resolves* to a number still emits its effects on the live
// table first (the probe runs on a clone, so it applies none).
if let Expr::Number(n) = eval_value(&mut symbols.clone(), expr)? {
if !matches!(expr, Expr::Number(_)) {
compile_for_effect(ctx, emit, symbols, expr)?;
return Ok(());
let ty = compile_for_stack(ctx, emit, symbols, expr)?;
match ty {
WasmType::Ratio => Ok(()),
WasmType::Commodity => Err(Error::Compile(
"commodity-bearing values cannot mix with pure-rational arithmetic; \
use `(convert-commodity ...)` to bridge"
WasmType::I32 => Err(Error::Compile(
"a runtime index (count) cannot be used as a scalar; \
bridge explicitly with `(index->scalar ...)`"
WasmType::Bool
| WasmType::PairRef(_)
| WasmType::StringRef
| WasmType::EntityRef(_)
| WasmType::Closure(_)
| WasmType::AnyRef => Err(Error::Compile(
"arithmetic requires ratio values".to_string(),
/// Compiles `expr` as a raw i32 Index value: a runtime `I32` (a count/length/
/// index) or an integer literal. A fractional literal is a Scalar, and a
/// runtime Ratio/Commodity/ref is not an Index — all rejected (ADR-0028:
/// Index combines only with Index + integer literals).
pub(in crate::compiler) fn compile_for_stack_index(
if *n.denom() == 1 {
return Err(Error::Compile(
"a fractional literal is a scalar, not an index".to_string(),
));
WasmType::I32 => Ok(()),
WasmType::Ratio | WasmType::Commodity => Err(Error::Compile(
"a scalar/money value cannot be used as an index; \
bridge explicitly with `(scalar->index ...)`"
| WasmType::AnyRef => Err(Error::Compile(format!(
"index arithmetic requires an integer count, got {ty}"
/// Emits `expr` coerced to the `target` wasm type — the type-directed boundary
/// primitive for closure args, host-fn args, and accumulator seeds. A nil
/// resolves to the target's typed default; a numeric literal coerces across the
/// sanctioned Index↔Scalar boundary; everything else must already match. The
/// nil/literal probe runs on a clone, so a non-literal that resolves to one
/// emits its effects on the live table first.
pub(in crate::compiler) fn compile_for_stack_as(
target: WasmType,
if matches!(eval_value(&mut symbols.clone(), expr)?, Expr::Nil) {
if !matches!(expr, Expr::Nil) {
return emit_nil_default(ctx, emit, target);
match target {
WasmType::Ratio => compile_for_stack_ratio(ctx, emit, symbols, expr),
WasmType::I32 => compile_for_stack_index(ctx, emit, symbols, expr),
_ => {
if ty == target {
Ok(())
} else {
Err(Error::Compile(format!(
"type mismatch: expected {target}, got {ty}"
)))
pub(in crate::compiler) fn compile_call_for_stack(
elems: &[Expr],
let (head, args) = elems
.split_first()
.ok_or_else(|| Error::Compile("empty function call".to_string()))?;
match head {
Expr::Symbol(name) => compile_symbol_call_for_stack(ctx, emit, symbols, name, args),
Expr::Lambda(params, body) => {
compile_lambda_call_for_stack(ctx, emit, symbols, params, body, args)
Expr::List(inner) => {
let resolved = call(symbols, inner)?;
match resolved {
compile_lambda_call_for_stack(ctx, emit, symbols, ¶ms, &body, args)
_ => Err(Error::Compile("not callable".to_string())),
let result = call(symbols, elems)?;
compile_for_stack(ctx, emit, symbols, &result)
fn compile_symbol_call_for_stack(
name: &str,
args: &[Expr],
let (func, kind, value) = {
let sym = symbols
.lookup(name)
.ok_or_else(|| Error::UndefinedSymbol(name.to_string()))?;
(sym.function().cloned(), sym.kind(), sym.value().cloned())
};
if kind == SymbolKind::Macro
&& let Some(Expr::Lambda(params, body)) = func
{
return expand_macro_then(symbols, ¶ms, &body, args, |symbols, code| {
compile_for_stack(ctx, emit, symbols, &code)
});
if let Some(Expr::Lambda(params, body)) = func {
if let Some(ty) = try_compile_runtime_call(ctx, emit, symbols, name, ¶ms, &body, args)?
return Ok(ty);
ctx.push_inlining_frame(name)?;
let result = compile_lambda_call_for_stack(ctx, emit, symbols, ¶ms, &body, args);
ctx.pop_inlining_frame(name);
return result;
if let Some(Expr::WasmLocal(idx, WasmType::Closure(sig))) = value {
return compile_call_ref(ctx, emit, symbols, idx, sig, args);
match kind {
SymbolKind::Native | SymbolKind::Operator => {
crate::compiler::native::compile_for_stack(ctx, emit, symbols, name, args)
SymbolKind::SpecialForm => {
crate::compiler::special::compile_for_stack(ctx, emit, symbols, name, args)
"symbol '{name}' is not callable for stack value"
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{ClosureSigId, EntityKind, Fraction, PairElement};
use crate::runtime::SymbolTable;
/// The contract `classify_stack_type` exists to uphold: for every literal /
/// already-resolved runtime `Expr` it classifies (returns `Some`), the
/// prediction equals the `WasmType` `compile_for_stack` actually pushes.
/// Any drift here is the class of bug (#56/#57/#58/#59) that motivated the
/// single source of truth, so this test compiles each shape for real and
/// compares. (`None`-classified shapes — empty list, calls, quotes — are
/// not value literals and aren't asserted here.)
#[test]
fn classify_stack_type_matches_compile_for_stack() {
let runtime = [
WasmType::I32,
WasmType::Bool,
WasmType::Ratio,
WasmType::Commodity,
WasmType::StringRef,
WasmType::PairRef(PairElement::Bool),
WasmType::EntityRef(EntityKind::Account),
WasmType::Closure(ClosureSigId(0)),
WasmType::AnyRef,
];
let mut cases: Vec<Expr> = vec![
Expr::Number(Fraction::from_integer(7)),
Expr::Number(Fraction::new(1, 2)),
Expr::Bool(true),
Expr::Bool(false),
Expr::Nil,
Expr::String("hi".to_string()),
// A `WasmLocal` of every wasm type — `compile_for_stack` emits
// `local.get` and returns the local's declared type unchanged.
cases.extend(runtime.iter().map(|ty| Expr::WasmLocal(0, *ty)));
for expr in &cases {
let predicted = classify_stack_type(expr)
.unwrap_or_else(|| panic!("classify_stack_type returned None for {expr:?}"));
let mut ctx = CompileContext::new().expect("ctx");
let mut emit = FunctionEmitter::new();
let mut symbols = SymbolTable::with_builtins_for_wasm();
let emitted = compile_for_stack(&mut ctx, &mut emit, &mut symbols, expr)
.unwrap_or_else(|e| panic!("compile_for_stack {expr:?}: {e}"));
assert_eq!(
predicted, emitted,
"classify_stack_type disagrees with compile_for_stack for {expr:?}"
);
/// Shapes that aren't a directly-emittable literal/runtime value (they need
/// a call/list walk or aren't stack values) classify as `None`.
fn classify_stack_type_is_none_for_non_literal_shapes() {
assert_eq!(classify_stack_type(&Expr::Symbol("X".to_string())), None);
// A namespaced symbol (ADR-0029) is still just a Symbol — its canonical
// `NS:NAME` key must not be mistaken for any typed literal shape.
classify_stack_type(&Expr::Symbol("FOO:BAR".to_string())),
None
assert_eq!(classify_stack_type(&Expr::List(vec![])), None);
assert_eq!(classify_stack_type(&Expr::Quote(Box::new(Expr::Nil))), None);
classify_stack_type(&Expr::WasmRuntime(WasmType::Ratio)),
Some(WasmType::Ratio)