Lines
86.21 %
Functions
30.91 %
Branches
100 %
//! Eval-only path — value-level interpretation that runs at compile
//! time for constant folding, macro expansion, and the type-inference
//! peek-ahead. `eval_value` is the entry point; everything else here
//! exists to support it.
//!
//! Distinct from the codegen path: `eval_value` doesn't emit any
//! wasm. The codegen entry points in [`super::compile`] /
//! [`super::effect`] / [`super::stack`] are the ones that produce
//! bytes; they use `eval_value` to decide whether a form is
//! constant-foldable or needs runtime emit.
use crate::ast::{Expr, LambdaParams};
use crate::error::{Error, Result};
use crate::runtime::{Symbol, SymbolKind, SymbolTable};
use super::quasiquote::expand_quasiquote;
pub(crate) fn call(symbols: &mut SymbolTable, elems: &[Expr]) -> Result<Expr> {
let (head, args) = elems
.split_first()
.ok_or_else(|| Error::Compile("empty function call".to_string()))?;
match head {
Expr::Symbol(name) => dispatch_symbol(symbols, name, args),
Expr::Quote(inner) => match inner.as_ref() {
_ => Err(Error::Compile(format!("not callable: {head:?}"))),
},
Expr::Lambda(params, body) => call_lambda(symbols, params, body, args),
Expr::List(inner) => {
let resolved = call(symbols, inner)?;
match resolved {
Expr::Lambda(params, body) => call_lambda(symbols, ¶ms, &body, args),
_ => Err(Error::Compile("not callable".to_string())),
}
pub(crate) fn eval_value(symbols: &mut SymbolTable, expr: &Expr) -> Result<Expr> {
match expr {
Expr::List(elems) if !elems.is_empty() => call(symbols, elems),
_ => resolve_arg(symbols, expr),
fn dispatch_symbol(symbols: &mut SymbolTable, name: &str, args: &[Expr]) -> Result<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 matches!(kind, SymbolKind::Native)
&& let Some(stand_in @ Expr::WasmRuntime(_)) = value
{
// Host-fn shim: the symbol carries the result type as a
// WasmRuntime stand-in. Eval each arg so any nested constant
// folding still runs, then surface the stand-in as the call's
// value — no host-side dispatch happens until codegen.
for arg in args {
eval_value(symbols, arg)?;
return Ok(stand_in);
if kind == SymbolKind::Macro
&& let Some(Expr::Lambda(params, body)) = func
return expand_macro_then(symbols, ¶ms, &body, args, |symbols, code| {
eval_value(symbols, &code)
});
if let Some(Expr::Lambda(params, body)) = func {
// A (mutually) recursive defun re-entered with a runtime arg never
// folds to a base case — inlining it again would recurse the
// compiler's stack forever. Hand it to the codegen monomorph path by
// surfacing a runtime placeholder of the call's inferred result type;
// the inline depth guard below is the backstop for any recursion the
// re-entry check misses (e.g. const recursion with no base case).
if symbols.is_inlining(name)
&& let Some(ret_ty) = recursive_runtime_call_type(symbols, ¶ms, args)
return Ok(Expr::WasmRuntime(ret_ty));
symbols.enter_inline(name)?;
let result = call_lambda(symbols, ¶ms, &body, args);
symbols.exit_inline();
return result;
match kind {
SymbolKind::Native | SymbolKind::Operator => {
crate::compiler::native::call(symbols, name, args)
SymbolKind::SpecialForm => crate::compiler::special::call(symbols, name, args),
_ => Err(Error::Compile(format!("symbol '{name}' is not callable"))),
/// The runtime result type to surface for a re-entrant recursive defun call,
/// or `None` when no argument resolves to a runtime value (so the call is
/// still const-foldable and should keep inlining). Must agree with what the
/// codegen monomorph path emits the helper at: `monomorph::initial_ret_guess`
/// is the FIRST required parameter's signature type, where each arg's
/// signature type comes from the shared `expr::classify_stack_type`. Each arg
/// is resolved on a CLONE so this probe applies no side effects to the live
/// table.
fn recursive_runtime_call_type(
symbols: &mut SymbolTable,
params: &LambdaParams,
args: &[Expr],
) -> Option<crate::ast::WasmType> {
let resolved: Vec<Expr> = args
.iter()
.map(|arg| eval_value(&mut symbols.clone(), arg).unwrap_or(Expr::Nil))
.collect();
if !resolved.iter().any(Expr::is_wasm_runtime) {
return None;
// `initial_ret_guess` = first param's signature type, classified the same
// way the monomorph path classifies it; falls back to Ratio for a nullary
// signature (matching `monomorph::initial_ret_guess`'s `unwrap_or`).
let first = params.required.first().and(resolved.first());
Some(
first
.and_then(super::stack::classify_stack_type)
.unwrap_or(crate::ast::WasmType::Ratio),
)
/// Expand a macro call and run `process` on the resulting code, with a
/// depth guard spanning the whole expand-then-reprocess step. Every
/// expansion site (eval / effect / stack / call compile paths) routes
/// through here so a self-referential macro — whose re-expansion happens
/// inside `process` — is bounded uniformly and turned into a structured
/// compile error instead of a native stack overflow. The depth is carried
/// on the persistent `symbols` (threaded through the recursion) and
/// restored afterwards so sibling expansions don't accumulate.
pub(crate) fn expand_macro_then<T>(
body: &Expr,
process: impl FnOnce(&mut SymbolTable, Expr) -> Result<T>,
) -> Result<T> {
symbols.enter_macro_expansion()?;
let result = expand_macro(symbols, params, body, args).and_then(|expansion| {
let code = match expansion {
Expr::Quote(inner) => *inner,
other => other,
process(symbols, code)
symbols.exit_macro_expansion();
result
pub(crate) fn expand_macro(
) -> Result<Expr> {
if !params.aux.is_empty() {
return Err(Error::Compile(
"&aux not yet supported in macros".to_string(),
));
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 || max_args.is_some_and(|max| args.len() > max) {
return Err(Error::Arity {
name: "macro".to_string(),
expected: min_args,
actual: args.len(),
let mut local_symbols = symbols.clone();
let mut arg_idx = 0;
for param in ¶ms.required {
local_symbols
.define(Symbol::new(param, SymbolKind::Variable).with_value(args[arg_idx].clone()));
arg_idx += 1;
for (param, default) in ¶ms.optional {
let value = if arg_idx < args.len() {
let arg = args[arg_idx].clone();
arg
} else if let Some(default_expr) = default {
default_expr.clone()
Expr::Nil
local_symbols.define(Symbol::new(param, SymbolKind::Variable).with_value(value));
if let Some(rest_param) = ¶ms.rest {
let rest = Expr::Quote(Box::new(Expr::List(args[arg_idx..].to_vec())));
local_symbols.define(Symbol::new(rest_param, SymbolKind::Variable).with_value(rest));
// Bind key parameters in macros
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;
// Look for keyword argument pairs
for i in (0..remaining_args.len() - 1).step_by(2) {
if remaining_args[i] == keyword {
found_value = Some(remaining_args[i + 1].clone());
break;
let value = if let Some(val) = found_value {
val
eval_value(&mut local_symbols, body)
fn call_lambda(
// If we have &key or &rest, we can have unlimited args
if args.len() < min_args {
name: "lambda".to_string(),
if let Some(max) = max_args
&& args.len() > max
expected: max,
// Bind required parameters
let resolved = eval_value(symbols, &args[arg_idx])?;
local_symbols.define(Symbol::new(param, SymbolKind::Variable).with_value(resolved));
// Bind optional parameters
eval_value(symbols, &args[arg_idx])?
eval_value(symbols, default_expr)?
if arg_idx < args.len() {
// Bind rest parameter
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_symbols.define(Symbol::new(rest_param, SymbolKind::Variable).with_value(rest_list));
// Bind key parameters
// Find keyword arguments in the remaining args
if remaining_args.len() >= 2 {
found_value = Some(eval_value(symbols, &remaining_args[i + 1])?);
// Bind aux parameters (auxiliary variables)
for (param, init) in ¶ms.aux {
let value = if let Some(init_expr) = init {
eval_value(&mut local_symbols, init_expr)?
pub(crate) fn resolve_arg(symbols: &mut SymbolTable, expr: &Expr) -> Result<Expr> {
| Expr::Bool(_)
| Expr::Number(_)
| Expr::String(_)
| Expr::Bytes(_)
| Expr::Quote(_)
| Expr::Keyword(_)
| Expr::RuntimeValue(_) => Ok(expr.clone()),
Expr::Lambda(_, _) => Ok(expr.clone()),
Expr::Cons(_, _) | Expr::List(_) => Ok(expr.clone()),
Expr::Quasiquote(inner) => expand_quasiquote(symbols, inner),
Expr::Unquote(_) | Expr::UnquoteSplicing(_) => {
Err(Error::Compile("unquote outside of quasiquote".to_string()))
Expr::Symbol(name) => {
let (value, kind, has_fn) = {
.ok_or_else(|| Error::UndefinedSymbol(name.clone()))?;
(sym.value().cloned(), sym.kind(), sym.function().is_some())
if let Some(value) = value {
return resolve_arg(symbols, &value);
SymbolKind::Native | SymbolKind::Operator => Err(Error::Compile(format!(
"'{name}' is a function and cannot be used as a value"
))),
SymbolKind::SpecialForm => Err(Error::Compile(format!(
"'{name}' is a special form and cannot be used as a value"
SymbolKind::Macro => Err(Error::Compile(format!(
"'{name}' is a macro and cannot be used as a value"
_ if has_fn => Err(Error::Compile(format!(
"'{name}' is a function; use #' or FUNCTION to access"
_ => Err(Error::Compile(format!("symbol '{name}' has no value"))),
Expr::WasmRuntime(_) | Expr::WasmLocal(_, _) => Ok(expr.clone()),