Lines
83.11 %
Functions
40 %
Branches
100 %
//! Effect-position codegen.
//!
//! `compile_for_effect` is the entry point — it inspects the leading
//! form and either dispatches into the specialised SETF / IF / BEGIN
//! handlers below, hands off to a special form's effect path, or
//! falls back to value-position emit when the form has a runtime
//! result that must be consumed.
use crate::ast::{Expr, WasmType};
use crate::compiler::context::CompileContext;
use crate::compiler::emit::FunctionEmitter;
use crate::error::{Error, Result};
use crate::runtime::{SymbolKind, SymbolTable};
use super::call::{compile_and_bind_lambda_params, try_compile_runtime_call};
use super::eval::{eval_value, expand_macro_then};
use super::stack::{compile_for_stack, compile_for_stack_as};
pub(in crate::compiler) fn compile_for_effect(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
expr: &Expr,
) -> Result<()> {
if let Expr::List(elems) = expr
&& let Some(Expr::Symbol(name)) = elems.first()
{
let args = &elems[1..];
// Expand macros (e.g. WHEN → IF) and recurse
if let Some(sym) = symbols.lookup(name)
&& sym.kind() == SymbolKind::Macro
&& let Some(Expr::Lambda(params, body)) = sym.function().cloned()
return expand_macro_then(symbols, ¶ms, &body, args, |symbols, code| {
compile_for_effect(ctx, emit, symbols, &code)
});
}
match name.as_str() {
"DEBUG" => {
return crate::compiler::native::compile_debug_effect(ctx, emit, symbols, args);
"PRINT" | "DISPLAY" => {
return crate::compiler::native::compile_print_effect(ctx, emit, symbols, args);
"NEWLINE" => {
return crate::compiler::native::compile_newline_effect(ctx, emit, symbols, args);
"SETF" => {
return compile_setf_for_effect(ctx, emit, symbols, args);
"DOLIST" | "DO" | "DO*" | "TAGBODY" | "GO" | "BLOCK" | "RETURN-FROM"
| "HANDLER-CASE" | "UNWIND-PROTECT" => {
return crate::compiler::special::compile_for_effect(
ctx, emit, symbols, name, args,
);
"CREATE-TAG" | "DELETE-ENTITY" => {
return crate::compiler::native::compile(ctx, emit, symbols, name, args);
"IF" => {
return compile_if_for_effect(ctx, emit, symbols, args);
"BEGIN" => {
for arg in args {
compile_for_effect(ctx, emit, symbols, arg)?;
return Ok(());
"LET" | "LET*" | "COND" | "AND" | "OR" => {
// Always compile for effect — never skip on an eval-fold to a
// const. `eval_value` is the const-fold surface and does NOT
// model side effects (`create-tag`, `debug`, host fns), so an
// effectful body (or a runtime condition eval mis-resolves to a
// const) would fold to `nil` and get silently dropped. Compiling
// for effect emits the body's effects; a genuinely pure const
// body lowers to a harmless no-op (same as `BEGIN`).
"DEFVAR" | "DEFPARAMETER" => {
// Route definition forms through their compile-side
// wrappers so the runtime-init promotion (allocating a
// wasm local + emitting the init's wasm into it) fires.
// Without this, the eval-only path stores the
// `Expr::WasmRuntime(_)` placeholder on the symbol and
// later uses resolve to a value that was never put on
// the stack — emitted wasm fails validation.
_ => {
let val = eval_value(symbols, expr)?;
if val.is_wasm_runtime() {
// A recursive runtime-arg call routes to the monomorph
// helper (its result is computed then dropped here);
// otherwise inline the body, depth-guarded so a
// non-terminating recursion is a structured error, not
// a native compiler-stack overflow — same contract as
// the value/stack call paths.
if let Some(_ty) = try_compile_runtime_call(
ctx, emit, symbols, name, ¶ms, &body, args,
)? {
emit.drop_value();
let mut local =
compile_and_bind_lambda_params(ctx, emit, symbols, ¶ms, args)?;
ctx.push_inlining_frame(name)?;
let result = compile_for_effect(ctx, emit, &mut local, &body);
ctx.pop_inlining_frame(name);
result?;
// A value-producing native / host-fn call (LIST, CONS, +, an accessor,
// …) whose result is discarded in effect position: COMPILE it and drop
// the value, so effects in its ARGUMENTS (a nested `setf`, a promoted
// accumulator update, …) actually emit. `eval_value` alone const-folds
// without emitting — that silently dropped those arg effects. Definition
// / non-value special forms (DEFUN, DEFSTRUCT, QUOTE, …) are NOT
// compilable for stack and stay on the eval path below.
if !crate::compiler::special::is_special_form(name) {
// A host fn — including a VOID one (`result: None`, e.g. `rpc-log`) —
// routes through its effect compiler, which emits the import call and
// drops any result. `compile_for_stack` would reject a void host fn
// ("no return type"). A non-host value-producing native is compiled
// and dropped.
if ctx.lookup_host_fn(name).is_some() {
compile_for_stack(ctx, emit, symbols, expr)?;
// A bare atom, or a definition / non-value special form, in effect position:
// const-fold it (no stack value to emit). Definition forms register into the
// symbol table here.
eval_value(symbols, expr)?;
Ok(())
fn compile_if_for_effect(
args: &[Expr],
if args.len() < 2 || args.len() > 3 {
return Err(Error::Compile(
"IF requires a test, a then-form, and an optional else-form".to_string(),
));
// Classify on a CLONE — the test's compile-time effects reach the live
// table only via the single emit below.
let test = eval_value(&mut symbols.clone(), &args[0])?;
let test_diverges =
crate::compiler::special::form_diverges_for_test(&mut symbols.clone(), &args[0])?;
crate::compiler::special::reject_non_boolean_runtime_test(&test, test_diverges)?;
if test_diverges {
// The test transfers control before producing a condition; compile it
// for effect so the exit fires. Both branches are dead.
return compile_for_effect(ctx, emit, symbols, &args[0]);
if crate::compiler::special::is_runtime_test(&test) {
compile_for_stack(ctx, emit, symbols, &args[0])?;
emit.if_block(wasm_encoder::BlockType::Empty);
compile_for_effect(ctx, emit, symbols, &args[1])?;
if args.len() == 3 {
emit.else_block();
compile_for_effect(ctx, emit, symbols, &args[2])?;
emit.block_end();
// Const test: apply its effects to the live table once, then the live arm.
let test = eval_value(symbols, &args[0])?;
if crate::compiler::special::is_truthy(&test) {
compile_for_effect(ctx, emit, symbols, &args[1])
} else if args.len() == 3 {
compile_for_effect(ctx, emit, symbols, &args[2])
} else {
fn compile_setf_for_effect(
if !args.len().is_multiple_of(2) {
"SETF requires an even number of arguments".to_string(),
for pair in args.chunks(2) {
let place = &pair[0];
let value_expr = &pair[1];
if let Expr::Symbol(name) = place {
let wasm_local = symbols
.lookup(name)
.and_then(|s| s.value())
.and_then(|v| match v {
Expr::WasmLocal(idx, ty) => Some((*idx, *ty)),
_ => None,
if let Some((idx, ty)) = wasm_local {
// Coerce the rhs to the local's declared type (literal Index↔Scalar
// crossing, nil → typed default); a real clash is a clean compile
// error, not an invalid `local.set`.
compile_for_stack_as(ctx, emit, symbols, value_expr, ty)?;
// Reassigning a closure local invalidates the body recorded for it
// at bind time — the local now holds a different closure, so a later
// FOLD inlining the stale body would call the wrong fn. Forget AFTER
// compiling the rhs (which may legitimately FOLD the OLD closure)
// and before the store.
if matches!(ty, WasmType::Closure(_)) {
ctx.forget_closure_body(idx);
emit.local_set(idx);
continue;
// const / struct-field place. Emit any nested runtime-local `setf` in the
// value expr for effect (the eval-rebind path is a no-op for a WasmLocal
// and would drop the store); take the place's value from a clone so the
// live table isn't double-mutated.
let value = if crate::compiler::special::rhs_has_runtime_store(value_expr, symbols) {
compile_for_effect(ctx, emit, symbols, value_expr)?;
eval_value(&mut symbols.clone(), value_expr)?
eval_value(symbols, value_expr)?
};
crate::compiler::special::setf_set_place(symbols, place, value)?;