Lines
80 %
Functions
16 %
Branches
100 %
//! Tier 1.5 monomorph emit — closes Gap B.
//!
//! When a defun is called with a runtime arg the inline const-fold path
//! has no terminating walk (each step re-enters the body without
//! reducing toward a base case). The runtime-call path emits the body
//! once as a real wasm fn keyed on the call-site argument signature
//! and lowers each call site to `call $monomorph_idx`. The recursion
//! itself terminates at runtime via the same mechanism every wasm fn
//! uses — the wasm call stack — so the compile-time walk no longer has
//! to.
//! Cache: [`super::super::context::monomorph::MonomorphCache`] is
//! keyed by `(defun_name, [arg_wasm_types])`. Distinct call-site
//! signatures emit distinct wasm fns; identical ones share.
//! Type inference: we don't have a declared return type at the call
//! site. The body's return type is determined by emitting the body —
//! but the body may self-call, and that self-call needs a concrete
//! return type up front. We resolve the chicken-egg via
//! [`snapshot`]/[`restore`] on the [`super::super::context::CompileContext`]:
//! 1. Insert a placeholder cache entry whose `ret_ty` matches the
//! first param's type (or `Ratio` for nullary recursion).
//! 2. Reserve the wasm fn slot and snapshot the wasm-section state.
//! 3. Emit the body with `compile_for_stack`. Self-calls inside the
//! body resolve through the cache and emit `call $self_idx`
//! against the placeholder return type.
//! 4. Compare observed return type to placeholder. If they match,
//! queue the helper and we're done.
//! 5. If they differ, restore the snapshot (rolling back the trial
//! wasm bytes) and re-emit with the observed type substituted in.
//! Hard cap at 4 iterations — anything that doesn't converge is a
//! structured `Error::Compile`, not an unbounded loop.
use crate::ast::{Expr, LambdaParams, WasmType};
use crate::compiler::context::CompileContext;
use crate::compiler::context::monomorph::{MonomorphEntry, MonomorphKey};
use crate::compiler::emit::FunctionEmitter;
use crate::compiler::expr::compile_for_stack;
use crate::error::{Error, Result};
use crate::runtime::{Symbol, SymbolKind, SymbolTable};
const FIXPOINT_ITER_CAP: usize = 4;
/// Lookup-or-emit pipeline. Returns the cache entry the caller emits
/// `call <entry.func_idx>` against. Body is the defun's `Expr::Lambda`
/// body; `params` is the defun's parameter list; `arg_types` is the
/// call-site argument signature (one wasm type per required param).
pub(in crate::compiler) fn lookup_or_emit_monomorph(
ctx: &mut CompileContext,
symbols: &SymbolTable,
name: &str,
params: &LambdaParams,
body: &Expr,
arg_types: &[WasmType],
) -> Result<MonomorphEntry> {
if !is_v1_signature(params) {
return Err(Error::Compile(format!(
"runtime-call lowering for '{name}' requires required-only \
parameters; lambdas with &optional/&rest/&key/&aux still go \
through the inline path"
)));
}
if params.required.len() != arg_types.len() {
return Err(Error::Arity {
name: name.to_string(),
expected: params.required.len(),
actual: arg_types.len(),
});
let key = MonomorphKey {
params: arg_types.to_vec(),
};
if let Some(entry) = ctx.monomorph_cache().get(&key) {
return Ok(entry.clone());
emit_monomorph(ctx, symbols, key, params, body, arg_types)
fn is_v1_signature(params: &LambdaParams) -> bool {
params.optional.is_empty()
&& params.rest.is_none()
&& params.key.is_empty()
&& params.aux.is_empty()
fn initial_ret_guess(arg_types: &[WasmType]) -> WasmType {
arg_types.first().copied().unwrap_or(WasmType::Ratio)
fn emit_monomorph(
key: MonomorphKey,
let mut guess = initial_ret_guess(arg_types);
let pre = ctx.snapshot();
for _ in 0..FIXPOINT_ITER_CAP {
ctx.restore(pre.clone());
let entry = MonomorphEntry {
func_idx: u32::MAX,
ret_ty: guess,
ctx.monomorph_cache_mut().insert(key.clone(), entry.clone());
match try_emit_body(ctx, symbols, &key, params, body, arg_types, guess) {
Ok(observed) if observed.ret_ty == guess => {
ctx.monomorph_cache_mut().insert(key, observed.clone());
return Ok(observed);
Ok(observed) => {
guess = observed.ret_ty;
Err(e) => {
ctx.restore(pre);
return Err(e);
Err(Error::Compile(format!(
"type inference for '{}' did not converge within {} iterations; \
the body's return type appears to depend on itself",
key.name, FIXPOINT_ITER_CAP
)))
fn try_emit_body(
key: &MonomorphKey,
expected_ret: WasmType,
let param_vts: Vec<_> = arg_types.iter().map(|t| ctx.wasm_val_type(*t)).collect();
let result_vt = ctx.wasm_val_type(expected_ret);
let helper_name = next_monomorph_helper_name(&key.name, arg_types);
let func_idx = ctx.register_function(&helper_name, ¶m_vts, &[result_vt])?;
func_idx,
ret_ty: expected_ret,
ctx.monomorph_cache_mut().insert(key.clone(), entry);
let param_count = u32::try_from(arg_types.len())
.map_err(|_| Error::Compile("monomorph param count exceeds u32 range".to_string()))?;
let mut local_symbols = symbols.clone();
for (idx, (param_name, ty)) in params.required.iter().zip(arg_types.iter()).enumerate() {
let local_idx = u32::try_from(idx)
.map_err(|_| Error::Compile("monomorph param index exceeds u32 range".to_string()))?;
local_symbols.define(
Symbol::new(param_name, SymbolKind::Variable)
.with_value(Expr::WasmLocal(local_idx, *ty)),
);
let snapshot = ctx.take_local_pool(param_count);
let mut helper_emit = FunctionEmitter::new();
if let Err(e) = ctx.push_inlining_frame(&key.name) {
ctx.restore_local_pool(snapshot);
let body_result = compile_for_stack(ctx, &mut helper_emit, &mut local_symbols, body);
ctx.pop_inlining_frame(&key.name);
let observed_ret = match body_result {
Ok(ty) => ty,
helper_emit.end();
let helper_locals = ctx.build_helper_locals(param_count);
let helper_fn = helper_emit.finish(&helper_locals);
if observed_ret != expected_ret {
return Ok(MonomorphEntry {
ret_ty: observed_ret,
ctx.queue_helper(helper_fn);
Ok(MonomorphEntry {
})
fn next_monomorph_helper_name(defun: &str, arg_types: &[WasmType]) -> String {
let mut name = format!("__defun_{defun}");
for ty in arg_types {
name.push('_');
name.push_str(&format_wasm_type(*ty));
name
fn format_wasm_type(ty: WasmType) -> String {
match ty {
WasmType::Ratio => "ratio".to_string(),
WasmType::Commodity => "commodity".to_string(),
WasmType::I32 => "i32".to_string(),
WasmType::Bool => "bool".to_string(),
WasmType::StringRef => "string".to_string(),
WasmType::PairRef(_) => "pair".to_string(),
WasmType::EntityRef(kind) => format!("entity_{}", kind.type_name()),
WasmType::Closure(sig) => format!("closure{}", sig.0),
WasmType::AnyRef => "any".to_string(),