Lines
98.18 %
Functions
24 %
Branches
100 %
//! Closure-signature registry.
//!
//! Tier 1.5 promotes lambdas from compile-time stringification to real
//! wasm functions. To pass a lambda around as a runtime value we need
//! a wasm GC type that bundles a typed funcref with its captured-env
//! ref — that's `$closure_<sig>`. Closures sharing the same
//! `(arg-types) -> ret-type` signature share the wasm closure-struct
//! type and the funcref signature; their env-struct types vary
//! independently per scope so two closures with disjoint captures
//! don't collide.
//! This slice ships the per-signature interner that the
//! `WasmType::Closure(sig_id)` arm of `wasm_val_type` resolves through;
//! the env-struct allocator and monomorph cache (used to lower
//! `(funcall closure-arg ...)` and `(defun ...)` recursion through a
//! real fn) land alongside their first call-site so the no-dead-code
//! rule stays clean. See `doc/adr/0027-lambda-real-wasm-fn.org` for the
//! full design.
#[cfg(test)]
mod tests;
use std::collections::HashMap;
use wasm_encoder::{
CompositeInnerType, CompositeType, FieldType, Function, HeapType, RefType, StorageType,
StructType, SubType, ValType,
};
use super::CompileContext;
use crate::ast::{ClosureSigId, Expr, LambdaParams, WasmType};
use crate::error::{Error, Result};
/// Per-signature closure type metadata. Held in `CompileContext` so
/// the compiler can look up `$closure_<sig>` and `$fn_<sig>` indices
/// from a [`ClosureSigId`] without rehashing the signature on each
/// reference. `fn_type_idx` is the typed funcref's wasm type — needed
/// at every `call_ref` site so the engine can validate the funcref's
/// shape statically.
#[derive(Debug, Clone)]
pub(crate) struct ClosureSig {
pub closure_type_idx: u32,
pub fn_type_idx: u32,
pub params: Vec<WasmType>,
pub result: WasmType,
}
/// Layout of a per-scope `$env_<id>` struct holding values captured by
/// a lambda body. One entry per `CaptureSet` name, in insertion order
/// so the outer site (which fills the struct) and the helper-fn body
/// (which reads it) walk the same field indices. Field types are
/// captured at the point [`CompileContext::intern_env_struct`] is
/// called from the lookup of each name in the caller's symbol table.
pub(crate) struct EnvStructLayout {
pub type_idx: u32,
pub fields: Vec<EnvField>,
pub(crate) struct EnvField {
pub name: String,
pub ty: WasmType,
#[derive(Debug, Default, Clone)]
pub(crate) struct ClosureRegistry {
sigs: HashMap<(Vec<WasmType>, WasmType), ClosureSigId>,
by_id: Vec<ClosureSig>,
next_sig_id: u32,
helper_count: u32,
env_struct_count: u32,
closure_literal_data_idx: Option<u32>,
/// Snapshot of `CompileContext`'s local-pool fields so a helper-fn
/// emit can carve out a fresh allocator without clobbering the caller's
/// in-flight state. Rebound by [`CompileContext::restore_local_pool`].
pub(crate) struct LocalPoolSnapshot {
next_local: u32,
local_types: Vec<(WasmType, u32)>,
closure_bodies: HashMap<u32, (LambdaParams, Expr)>,
serializer: super::super::layout::OutputSerializer,
impl CompileContext {
/// Returns a `(ref null $closure_<sig>)` valtype. Helper for the
/// `WasmType::Closure(sig)` arm of `wasm_val_type`.
pub(crate) fn closure_ref(&self, sig: ClosureSigId) -> ValType {
let entry = &self.closures.by_id[sig.0 as usize];
ValType::Ref(RefType {
nullable: true,
heap_type: HeapType::Concrete(entry.closure_type_idx),
})
pub(crate) fn closure_sig(&self, sig: ClosureSigId) -> &ClosureSig {
&self.closures.by_id[sig.0 as usize]
/// Records a value-position lambda's source, keyed by the wasm local its
/// closure value was bound to (see [`Self::closure_bodies`]).
pub(in crate::compiler) fn record_closure_body(
&mut self,
local_idx: u32,
params: LambdaParams,
body: Expr,
) {
self.closure_bodies.insert(local_idx, (params, body));
/// The source `(params, body)` of the closure value bound to `local_idx`,
/// if one was recorded — lets a higher-order native inline the body per
/// element with the actual element type instead of `call_ref`ing the
/// fixed-signature closure.
pub(in crate::compiler) fn closure_body(
&self,
) -> Option<&(LambdaParams, Expr)> {
self.closure_bodies.get(&local_idx)
/// Drops any recorded body for `local_idx`. Called when a `setf` overwrites
/// a closure local: the recorded source no longer describes the value now in
/// that local, so an inline would apply the WRONG closure. Forgetting it
/// falls the higher-order native back to the (always-correct) `call_ref`.
pub(in crate::compiler) fn forget_closure_body(&mut self, local_idx: u32) {
self.closure_bodies.remove(&local_idx);
/// Look up or register the wasm types behind a closure signature.
/// Returns the same id for repeat calls with the same signature so
/// distinct lambdas with the same shape share `$fn_<sig>` and
/// `$closure_<sig>`.
pub(crate) fn intern_closure_signature(
params: &[WasmType],
result: WasmType,
) -> Result<ClosureSigId> {
let key = (params.to_vec(), result);
if let Some(id) = self.closures.sigs.get(&key) {
return Ok(*id);
let env_anyref = self.anyref();
let mut fn_params = Vec::with_capacity(params.len() + 1);
fn_params.push(env_anyref);
fn_params.extend(params.iter().map(|ty| self.wasm_val_type(*ty)));
let result_vt = self.wasm_val_type(result);
let fn_type_idx = self.get_or_create_func_type(&fn_params, &[result_vt])?;
let closure_type_idx = self.type_count;
let closure_fields = vec![
FieldType {
element_type: StorageType::Val(ValType::Ref(RefType {
nullable: false,
heap_type: HeapType::Concrete(fn_type_idx),
})),
mutable: false,
},
element_type: StorageType::Val(env_anyref),
];
self.types.ty().subtype(&SubType {
is_final: true,
supertype_idx: None,
composite_type: CompositeType {
inner: CompositeInnerType::Struct(StructType {
fields: closure_fields.into_boxed_slice(),
}),
shared: false,
describes: None,
descriptor: None,
});
self.type_count = self
.type_count
.checked_add(1)
.ok_or_else(|| Error::Compile("wasm type index space exhausted".to_string()))?;
let id = ClosureSigId(self.closures.next_sig_id);
self.closures.next_sig_id =
self.closures.next_sig_id.checked_add(1).ok_or_else(|| {
Error::Compile("closure signature id space exhausted".to_string())
})?;
self.closures.by_id.push(ClosureSig {
closure_type_idx,
fn_type_idx,
params: params.to_vec(),
result,
self.closures.sigs.insert(key, id);
Ok(id)
/// Registers a fresh `$env_<id>` struct type for one captured-name
/// scope. Each call allocates a new wasm type (no dedup) — different
/// lambdas captures different names even when shapes coincide, and
/// the per-call cost is one type-section entry. Caller's
/// responsibility to populate the layout in declared order.
pub(crate) fn intern_env_struct(&mut self, fields: &[EnvField]) -> Result<EnvStructLayout> {
let env_type_idx = self.type_count;
let struct_fields: Vec<FieldType> = fields
.iter()
.map(|f| FieldType {
element_type: StorageType::Val(self.wasm_val_type(f.ty)),
.collect();
fields: struct_fields.into_boxed_slice(),
self.closures.env_struct_count = self
.closures
.env_struct_count
.ok_or_else(|| Error::Compile("env-struct id space exhausted".to_string()))?;
Ok(EnvStructLayout {
type_idx: env_type_idx,
fields: fields.to_vec(),
/// Allocates a unique helper-fn name for a lambda body. Used by the
/// emit path so distinct lambda bodies get stable wasm names even
/// when sharing a `ClosureSigId`.
pub(crate) fn next_lambda_helper_name(&mut self) -> Result<String> {
let id = self.closures.helper_count;
self.closures.helper_count = self
.helper_count
.ok_or_else(|| Error::Compile("lambda helper id space exhausted".to_string()))?;
Ok(format!("__lambda_{id}"))
/// Saves the in-flight local-pool state (local allocator + types +
/// serializer cursor) so a helper-fn body can be emitted into a
/// fresh pool. The new pool numbers locals from `next_local_base`,
/// which the caller picks to match the helper fn's wasm-level
/// layout — script-mode entry points use `LOCAL_POOL_BASE` (5
/// preallocated slots), lambda helpers use `param_count` (no
/// preallocated slots, locals begin immediately after params).
/// Pair with [`Self::restore_local_pool`] once the helper's
/// `Function` has been finished and queued.
pub(crate) fn take_local_pool(&mut self, next_local_base: u32) -> LocalPoolSnapshot {
let saved = LocalPoolSnapshot {
next_local: self.next_local,
local_types: std::mem::take(&mut self.local_types),
closure_bodies: std::mem::take(&mut self.closure_bodies),
serializer: std::mem::replace(
&mut self.serializer,
super::super::layout::OutputSerializer::new(super::super::expr::LOCAL_OUTPUT_BASE),
),
self.next_local = next_local_base;
saved
pub(crate) fn restore_local_pool(&mut self, snapshot: LocalPoolSnapshot) {
self.next_local = snapshot.next_local;
self.local_types = snapshot.local_types;
self.closure_bodies = snapshot.closure_bodies;
self.serializer = snapshot.serializer;
/// Builds the locals declaration for a lambda helper fn whose
/// `param_count` slots are reserved (env + user params, all func
/// args). Mirrors [`Self::build_locals_declaration`] but starts
/// numbering after the params and skips the script-mode preallocated
/// pool — helper bodies use the local-pool allocator only.
pub(crate) fn build_helper_locals(&self, param_count: u32) -> Vec<(u32, ValType)> {
let mut locals: Vec<(u32, ValType)> = Vec::with_capacity(self.local_types.len());
let _ = param_count;
for &(ty, _) in &self.local_types {
locals.push((1, self.wasm_val_type(ty)));
locals
pub(crate) fn queue_helper(&mut self, helper: Function) {
self.pending_helpers.push(helper);
/// Returns the passive-data-segment idx for the literal `<closure>`
/// rendering used by [`OutputSerializer::write_debug_closure_from_stack`].
/// The bytes are interned on first use so a module that constructs
/// many closures shares one data segment.
pub(crate) fn closure_literal_data_idx(&mut self) -> Result<u32> {
if let Some(idx) = self.closures.closure_literal_data_idx {
return Ok(idx);
let idx = self.add_data(CLOSURE_LITERAL_BYTES)?;
self.closures.closure_literal_data_idx = Some(idx);
Ok(idx)
pub(crate) const CLOSURE_LITERAL_BYTES: &[u8] = b"<closure>";
pub(crate) const CLOSURE_LITERAL_LEN: u32 = CLOSURE_LITERAL_BYTES.len() as u32;