Lines
86.1 %
Functions
26.96 %
Branches
100 %
//! Function call dispatch.
//!
//! Three call paths coexist; per ADR-0027 the inline path stays as the
//! const-fold fast path and the runtime-call / call_ref paths are
//! additive:
//! - **Inline path** ([`compile_lambda_call`] /
//! [`compile_lambda_call_for_stack`] /
//! [`compile_and_bind_lambda_params`]) — clones the symbol table,
//! binds each arg into the local scope (constants flow through as
//! values; runtime args get an outer-scope local), then walks the
//! body via `compile_expr` / `compile_for_stack`. This is the only
//! path that can const-fold a fully-known argument list down to a
//! single value, and it's the path the test framework, macro
//! expansion, and the commodity-mismatch invariant all sit on.
//! - **Runtime-call path** ([`try_compile_runtime_call`]) — fires when
//! the inline walk would diverge (recursion plus a runtime arg). The
//! defun body is emitted once per call-site signature into a
//! monomorph helper fn; each matching call site lowers to
//! `call $monomorph_idx`. See `compiler/special/lambda/monomorph.rs`.
//! - **Closure call_ref path** ([`compile_call_ref`]) — fires when the
//! call head resolves to a `WasmLocal` of a `Closure(sig)` value
//! (e.g. a `let`-bound result of `(lambda ...)` or a closure passed
//! through MAP/FOLD). Loads `funcref` + env from the struct and
//! `call_ref`s through the typed signature.
//! Entry points:
//! - [`compile_call`] — effect-position call from `compile_expr`'s list
//! branch. Dispatches on the call head: symbol → `compile_symbol_call`,
//! inline lambda → inline path, list head → recurse after `eval`.
//! - [`compile_symbol_call`] (private) — macro-expand if applicable,
//! then dispatch in order: runtime-call → inline → closure call_ref →
//! native → special form.
//! - The stack-position mirrors live in [`super::stack`].
use crate::ast::{ClosureSigId, Expr, LambdaParams, WasmType};
use crate::compiler::context::CompileContext;
use crate::compiler::emit::FunctionEmitter;
use crate::compiler::special::lookup_or_emit_monomorph;
use crate::error::{Error, Result};
use crate::runtime::{Symbol, SymbolKind, SymbolTable};
use super::compile::compile_expr;
use super::eval::{call, eval_value, expand_macro_then};
use super::stack::{compile_for_stack, compile_for_stack_as};
pub(in crate::compiler) fn compile_call(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
elems: &[Expr],
) -> Result<()> {
let (head, args) = elems
.split_first()
.ok_or_else(|| Error::Compile("empty function call".to_string()))?;
match head {
Expr::Symbol(name) => compile_symbol_call(ctx, emit, symbols, name, args),
Expr::Quote(inner) => match inner.as_ref() {
_ => Err(Error::Compile(format!("not callable: {head:?}"))),
},
Expr::Lambda(params, body) => compile_lambda_call(ctx, emit, symbols, params, body, args),
Expr::List(inner) => {
let resolved = call(symbols, inner)?;
match resolved {
Expr::Lambda(params, body) => {
compile_lambda_call(ctx, emit, symbols, ¶ms, &body, args)
}
_ => Err(Error::Compile("not callable".to_string())),
fn compile_symbol_call(
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_expr(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 crate::compiler::expr::serialize_stack_to_output(ctx, emit, ty);
ctx.push_inlining_frame(name)?;
let result = compile_lambda_call(ctx, emit, symbols, ¶ms, &body, args);
ctx.pop_inlining_frame(name);
return result;
if let Some(Expr::WasmLocal(idx, WasmType::Closure(sig))) = value {
let ty = compile_call_ref(ctx, emit, symbols, idx, sig, args)?;
match kind {
SymbolKind::Native | SymbolKind::Operator => {
crate::compiler::native::compile(ctx, emit, symbols, name, args)
SymbolKind::SpecialForm => {
crate::compiler::special::compile(ctx, emit, symbols, name, args)
_ => Err(Error::Compile(format!("symbol '{name}' is not callable"))),
/// Lowers a call against a runtime closure value held in `local idx`.
/// Stack discipline mirrors `$fn_<sig>`'s declared signature
/// `(env, args...)`: load env from the closure, push each arg, then
/// load the funcref and `call_ref`.
pub(in crate::compiler) fn compile_call_ref(
closure_idx: u32,
sig: ClosureSigId,
) -> Result<WasmType> {
let (closure_type_idx, fn_type_idx, expected_params, result_ty) = {
let entry = ctx.closure_sig(sig);
(
entry.closure_type_idx,
entry.fn_type_idx,
entry.params.clone(),
entry.result,
)
if args.len() != expected_params.len() {
return Err(Error::Arity {
name: "closure".to_string(),
expected: expected_params.len(),
actual: args.len(),
// Snapshot the closure value before compiling arguments: an argument that
// reassigns the callee local (e.g. `(begin (setf f g) 1)`) must not let the
// env be read from the old closure and the funcref from the new one.
let saved = ctx.alloc_local(WasmType::Closure(sig))?;
emit.local_get(closure_idx);
emit.local_set(saved);
emit.local_get(saved);
emit.struct_get(closure_type_idx, 1);
for (arg, &expected) in args.iter().zip(expected_params.iter()) {
// Coerce each argument to the closure's declared parameter type: a nil
// becomes the typed default, an integer/fractional literal crosses the
// sanctioned Index↔Scalar boundary, and a runtime value must match.
compile_for_stack_as(ctx, emit, symbols, arg, expected).map_err(|_| {
Error::Compile(format!(
"closure argument type mismatch: expected {expected:?}"
))
})?;
emit.struct_get(closure_type_idx, 0);
emit.call_ref(fn_type_idx);
Ok(result_ty)
/// Tier 1.5 Gap B runtime-call dispatch. If the inline const-fold walk
/// would diverge — `name` is recursive AND at least one arg resolves to
/// a runtime value — we lower the call through a real wasm fn instead.
/// The body is emitted once per call-site signature (cached) and each
/// matching call site emits `call $monomorph_idx`. Returns `None` to
/// signal "stay on the inline path"; `Some(ret_ty)` means the runtime
/// stack now holds the call's result.
pub(super) fn try_compile_runtime_call(
params: &LambdaParams,
body: &Expr,
) -> Result<Option<WasmType>> {
if !needs_runtime_call(ctx, symbols, name, body, args) {
return Ok(None);
let arg_types = infer_arg_types(symbols, args)?;
let entry = lookup_or_emit_monomorph(ctx, symbols, name, params, body, &arg_types)?;
emit_runtime_call(ctx, emit, symbols, args, &arg_types, entry.func_idx)?;
Ok(Some(entry.ret_ty))
fn needs_runtime_call(
ctx: &CompileContext,
self_name: &str,
) -> bool {
let any_runtime = args.iter().any(|arg| arg_resolves_runtime(symbols, arg));
any_runtime && body_calls_inlining_or_self(ctx, self_name, body)
fn arg_resolves_runtime(symbols: &mut SymbolTable, arg: &Expr) -> bool {
matches!(
eval_value(symbols, arg),
Ok(Expr::WasmRuntime(_) | Expr::WasmLocal(_, _))
fn infer_arg_types(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Vec<WasmType>> {
args.iter()
.map(|arg| infer_arg_type(symbols, arg))
.collect()
fn infer_arg_type(symbols: &mut SymbolTable, arg: &Expr) -> Result<WasmType> {
let resolved = eval_value(symbols, arg)?;
// The monomorph parameter slot type is exactly the stack type the arg
// lowers to — the shared classifier keeps the signature, the eval-path
// recursive-call placeholder, and `compile_for_stack` in agreement.
crate::compiler::expr::classify_stack_type(&resolved).ok_or_else(|| {
"runtime-call lowering can't classify argument type for {resolved:?}; \
rewrite the call so each argument is a numeric or runtime value"
})
fn emit_runtime_call(
arg_types: &[WasmType],
func_idx: u32,
for (arg, &expected) in args.iter().zip(arg_types.iter()) {
let actual = compile_for_stack(ctx, emit, symbols, arg)?;
if actual != expected {
return Err(Error::Compile(format!(
"runtime-call argument type mismatch: expected {expected:?}, got {actual:?}"
)));
emit.call(func_idx);
Ok(())
fn body_calls_inlining_or_self(ctx: &CompileContext, self_name: &str, expr: &Expr) -> bool {
match expr {
Expr::List(elems) => {
if let Some(Expr::Symbol(name)) = elems.first()
&& (name == self_name || ctx.is_inlining(name))
return true;
elems
.iter()
.any(|e| body_calls_inlining_or_self(ctx, self_name, e))
Expr::Quasiquote(inner) | Expr::Unquote(inner) | Expr::UnquoteSplicing(inner) => {
body_calls_inlining_or_self(ctx, self_name, inner)
Expr::Cons(car, cdr) => {
body_calls_inlining_or_self(ctx, self_name, car)
|| body_calls_inlining_or_self(ctx, self_name, cdr)
Expr::Lambda(_, body) => body_calls_inlining_or_self(ctx, self_name, body),
_ => false,
fn compile_lambda_call(
let mut local = compile_and_bind_lambda_params(ctx, emit, symbols, params, args)?;
compile_expr(ctx, emit, &mut local, body)
pub(in crate::compiler) fn compile_lambda_call_for_stack(
compile_for_stack(ctx, emit, &mut local, body)
/// Codegen-aware analog of the eval-only lambda-param binder. For
/// each required / optional / rest / key / aux parameter whose
/// argument resolves to a runtime value (host fn call result, prior
/// `WasmLocal`, etc.), this emits the wasm to compute the value once,
/// stashes it in a fresh local, and binds the parameter symbol to
/// `Expr::WasmLocal(idx, ty)` so subsequent body references emit
/// `local.get N`. Constant args (`Number` / `Bool` / `Nil` / `String`
/// / `Bytes` / `Quote(_)` / etc) bind directly to the resolved value
/// — no wasm emitted, body continues to const-fold against the
/// value.
///
/// The runtime/constant split is what lets the inline path remain the
/// const-fold fast path: a defun whose entire arg list resolves at
/// compile time walks the body without emitting a single wasm
/// instruction; introducing one runtime arg promotes only that arg
/// into a local, leaving the rest to fold.
pub(in crate::compiler) fn compile_and_bind_lambda_params(
) -> Result<SymbolTable> {
let min_args = params.required.len();
let max_args = if params.rest.is_some() || !params.key.is_empty() {
None
} else {
Some(min_args + params.optional.len())
if args.len() < min_args {
name: "lambda".to_string(),
expected: min_args,
if let Some(max) = max_args
&& args.len() > max
expected: max,
let mut local = symbols.clone();
let mut arg_idx = 0;
for param in ¶ms.required {
let bound = compile_arg_for_param(ctx, emit, symbols, &args[arg_idx])?;
local.define(Symbol::new(param, SymbolKind::Variable).with_value(bound));
arg_idx += 1;
for (param, default) in ¶ms.optional {
let bound = if arg_idx < args.len() {
let v = compile_arg_for_param(ctx, emit, symbols, &args[arg_idx])?;
v
} else if let Some(default_expr) = default {
eval_value(symbols, default_expr)?
Expr::Nil
if let Some(rest_param) = ¶ms.rest {
let rest_args: Vec<Expr> = args[arg_idx..]
.map(|arg| eval_value(symbols, arg))
.collect::<Result<_>>()?;
let rest_list = if rest_args.is_empty() {
Expr::List(rest_args)
local.define(Symbol::new(rest_param, SymbolKind::Variable).with_value(rest_list));
if !params.key.is_empty() {
let remaining_args = &args[arg_idx..];
for (param, default) in ¶ms.key {
let keyword = Expr::Keyword(param.to_uppercase());
let mut found_value = None;
if remaining_args.len() >= 2 {
for i in (0..remaining_args.len() - 1).step_by(2) {
if remaining_args[i] == keyword {
found_value = Some(eval_value(symbols, &remaining_args[i + 1])?);
break;
let value = if let Some(val) = found_value {
val
local.define(Symbol::new(param, SymbolKind::Variable).with_value(value));
for (param, init) in ¶ms.aux {
let value = if let Some(init_expr) = init {
eval_value(&mut local, init_expr)?
Ok(local)
/// Per-argument binding helper. Runtime values (`Expr::WasmRuntime` /
/// `Expr::WasmLocal`) get emitted onto the stack and stashed in a fresh
/// local; everything else (constants, quoted forms, lambdas) binds
/// directly to the resolved value.
fn compile_arg_for_param(
arg: &Expr,
) -> Result<Expr> {
match &resolved {
Expr::WasmRuntime(ty) => {
compile_for_stack(ctx, emit, symbols, arg)?;
let idx = ctx.alloc_local(*ty)?;
emit.local_set(idx);
Ok(Expr::WasmLocal(idx, *ty))
// Already a local — reusing the index keeps the body's
// local.get emissions pointed at the original storage.
Expr::WasmLocal(_, _) => Ok(resolved),
_ => Ok(resolved),