Lines
90.61 %
Functions
30 %
Branches
100 %
//! `LET` / `LET*` codegen (effect, stack, value-position variants).
//!
//! Six entry points share the same three-phase shape: parse the
//! bindings list, resolve each init (parallel for LET, sequential
//! for LET*), bind via `bind_runtime_or_const` to either a constant
//! or a freshly-allocated wasm local, then compile the body. The
//! only difference is what the body-compile target is.
use crate::ast::{Expr, WasmType};
use crate::compiler::context::CompileContext;
use crate::compiler::emit::FunctionEmitter;
use crate::compiler::expr::{
compile_body, compile_body_for_stack, compile_for_effect, compile_for_stack,
compile_for_stack_as, emit_nil_default, eval_value,
};
use crate::compiler::special::try_emit_lambda_for_value;
use crate::error::{Error, Result};
use crate::runtime::{Symbol, SymbolKind, SymbolTable};
use super::infer::assigned_var_runtime_type;
use super::parse::parse_bindings;
pub(super) fn compile_let(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
args: &[Expr],
) -> Result<()> {
if args.len() < 2 {
return Err(Error::Compile(
"LET requires a bindings list and at least one body form".to_string(),
));
}
let bindings = parse_bindings("LET", &args[0])?;
let resolved = resolve_let_bindings(symbols, bindings)?;
let mut local = symbols.clone();
for (name, init_expr, val) in resolved {
let bound = bind_runtime_or_const(
ctx,
emit,
symbols,
&name,
init_expr.as_ref(),
&val,
&args[1..],
)?;
record_closure_result(&mut local, ctx, &bound);
local.define(Symbol::new(&name, SymbolKind::Variable).with_value(bound));
compile_body(ctx, emit, &mut local, &args[1..])
pub(super) fn compile_let_for_stack(
) -> Result<WasmType> {
compile_body_for_stack(ctx, emit, &mut local, &args[1..])
pub(super) fn compile_let_star(
"LET* requires a bindings list and at least one body form".to_string(),
let bindings = parse_bindings("LET*", &args[0])?;
for (name, init) in bindings {
let (init_expr, val) = resolve_sequential_binding(&mut local, init)?;
&mut local,
pub(super) fn compile_let_star_for_stack(
pub(super) fn compile_let_for_effect(
for arg in &args[1..] {
compile_for_effect(ctx, emit, &mut local, arg)?;
Ok(())
pub(super) fn compile_let_star_for_effect(
/// When a binding's emitted value is a closure, record its signature's result
/// type into the body's symbol table so the ctx-less eval surface (FOLD's
/// accumulator probe, binding-local sizing) can predict a HOF result type that
/// agrees with codegen — see `SymbolTable::closure_results`.
fn record_closure_result(local: &mut SymbolTable, ctx: &CompileContext, bound: &Expr) {
if let Expr::WasmLocal(_, WasmType::Closure(sig)) = bound {
local.record_closure_result(*sig, ctx.closure_sig(*sig).result);
fn resolve_let_bindings(
bindings: Vec<(String, Option<Expr>)>,
) -> Result<Vec<(String, Option<Expr>, Expr)>> {
bindings
.into_iter()
.map(|(name, init)| {
let (orig, val) = match init {
Some(expr) => {
let v = eval_value(symbols, &expr)?;
(Some(expr), v)
None => (None, Expr::Nil),
Ok((name, orig, val))
})
.collect()
fn resolve_sequential_binding(
local: &mut SymbolTable,
init: Option<Expr>,
) -> Result<(Option<Expr>, Expr)> {
match init {
let v = eval_value(local, &expr)?;
Ok((Some(expr), v))
None => Ok((None, Expr::Nil)),
/// If val is `WasmRuntime`, compile the init expression to the WASM stack,
/// allocate a local, and return `WasmLocal`. If val is an `Expr::Lambda`
/// that fits the Tier 1.5 v1 emit slice, lift it to a real wasm closure
/// value: emit the closure construction sequence, allocate a
/// `Closure(sig)` local, and return the corresponding `WasmLocal`.
/// Otherwise return the constant value untouched (the body's
/// const-fold path keeps working).
fn bind_runtime_or_const(
name: &str,
init_expr: Option<&Expr>,
val: &Expr,
body: &[Expr],
) -> Result<Expr> {
// A let-var the body mutates via `setf`/`set!` must be a runtime local even
// when its init is a const: `setf` only emits a runtime store for a
// `WasmLocal` place; a const-bound var instead takes the eval-rebind path,
// which emits no wasm and is lost across loop scopes — so a
// `(let ((acc 0)) … (setf acc …))` accumulator inside a DO/dolist would
// silently never update. Allocate the local up front from the (compiled)
// init value.
if !matches!(
val,
Expr::WasmRuntime(_) | Expr::WasmLocal(_, _) | Expr::Lambda(_, _)
) && let Some(ty) = assigned_var_runtime_type(body, name, val, symbols)
{
match init_expr {
compile_for_stack_as(ctx, emit, symbols, expr, ty)?;
None => emit_nil_default(ctx, emit, ty)?,
let idx = ctx.alloc_local(ty)?;
emit.local_set(idx);
return Ok(Expr::WasmLocal(idx, ty));
match val {
Expr::WasmRuntime(_) => {
let expr = init_expr.ok_or_else(|| {
Error::Compile("runtime binding requires an init expression".to_string())
})?;
// Size the local from the type codegen actually pushes, not the
// eval-time placeholder: the two can disagree (e.g. a fold over a
// runtime closure — eval can't see the closure sig and falls back to
// the literal seed's type, while codegen uses the sig result). The
// compiled type is authoritative; trusting the placeholder would
// local.set a value of one type into a local of another (invalid wasm).
let actual_ty = compile_for_stack(ctx, emit, symbols, expr)?;
let idx = ctx.alloc_local(actual_ty)?;
Ok(Expr::WasmLocal(idx, actual_ty))
Expr::WasmLocal(_, _) => Ok(val.clone()),
Expr::Lambda(params, body) => {
if let Some(sig) = try_emit_lambda_for_value(ctx, emit, symbols, params, body)? {
let ty = WasmType::Closure(sig);
// Record the source so a higher-order native can inline this
// closure per element with the actual element type, rather than
// `call_ref`ing its fixed (Ratio-default) signature. ONLY for a
// capture-free closure — inlining a capturing one at the HOF call
// site would resolve its free vars there (possibly shadowed /
// mutated) instead of at the closure's creation site (wrong
// value); a capturing closure keeps the `call_ref` path.
if crate::compiler::special::is_capture_free(symbols, params, body) {
ctx.record_closure_body(idx, params.clone(), (**body).clone());
Ok(val.clone())
_ => Ok(val.clone()),