Lines
81.88 %
Functions
26.67 %
Branches
100 %
//! `DEFUN`, `DEFVAR`, `DEFPARAMETER` — name-binding forms that mutate
//! the symbol table in place.
//!
//! The compile-side wrappers run the eval-side handler first (to
//! register the symbol), then call `promote_def_binding` so a runtime
//! init expression actually lands its value in a wasm local. Without
//! that promotion, later uses of the symbol resolve to a
//! `WasmRuntime` placeholder and emit no value at codegen.
use crate::ast::Expr;
use crate::compiler::context::CompileContext;
use crate::compiler::emit::FunctionEmitter;
use crate::compiler::expr::{compile_expr, compile_for_stack, eval_value};
use crate::error::{Error, Result};
use crate::runtime::{Symbol, SymbolKind, SymbolTable};
use super::parse::parse_lambda_params;
pub(super) fn compile_defun(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
args: &[Expr],
) -> Result<()> {
let result = defun(symbols, args)?;
compile_expr(ctx, emit, symbols, &result)
}
pub(super) fn compile_defvar(
// defvar / defparameter are pure side-effects on the symbol
// table — they don't emit a value into the script's output stream
// and don't push a value onto the wasm stack. The
// `promote_def_binding` step is the only wasm emission: when the
// init expression is a runtime form (e.g. `(entity-count)`), it
// allocates a local and emits the init's producer into it so
// subsequent uses of the symbol resolve to a real `WasmLocal`.
// Constant inits emit no wasm here at all.
defvar(symbols, args)?;
promote_def_binding(ctx, emit, symbols, args.first(), args.get(1))
pub(super) fn compile_defparam(
defparameter(symbols, args)?;
/// After `defvar` / `defparameter` evaluates the init expression and
/// stores its result as the symbol's value, replace any
/// `Expr::WasmRuntime(_)` placeholder with `Expr::WasmLocal(idx, ty)`
/// — and emit the init's wasm into that local. Without this, the
/// symbol's value claims to live at runtime but nothing put it on the
/// stack; later uses resolve to the placeholder and `compile_for_stack`
/// silently emits no value.
fn promote_def_binding(
name_expr: Option<&Expr>,
init_expr: Option<&Expr>,
let (Some(Expr::Symbol(name)), Some(init)) = (name_expr, init_expr) else {
return Ok(());
};
if !matches!(
symbols.lookup(name).and_then(|s| s.value()),
Some(Expr::WasmRuntime(_))
) {
// 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),
// and trusting the placeholder would `local.set` a mistyped value.
let actual_ty = compile_for_stack(ctx, emit, symbols, init)?;
let idx = ctx.alloc_local(actual_ty)?;
emit.local_set(idx);
if let Some(sym) = symbols.lookup_mut(name) {
sym.set_value(Expr::WasmLocal(idx, actual_ty));
Ok(())
pub(super) fn defun(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
if args.len() < 3 {
return Err(Error::Compile(
"DEFUN requires a name, parameter list, and body".to_string(),
));
let name = match &args[0] {
Expr::Symbol(s) => s.clone(),
other => {
return Err(Error::Compile(format!(
"DEFUN: expected symbol name, got {other:?}"
)));
let params = parse_lambda_params("DEFUN", &args[1])?;
let (doc, body_idx) = match args.get(2) {
Some(Expr::String(s)) if args.len() > 3 => (Some(s.clone()), 3),
_ => (None, 2),
if body_idx >= args.len() {
return Err(Error::Compile("DEFUN: missing body".to_string()));
let body = if args.len() == body_idx + 1 {
args[body_idx].clone()
} else {
let mut forms = Vec::with_capacity(args.len() - body_idx + 1);
forms.push(Expr::Symbol("BEGIN".to_string()));
forms.extend_from_slice(&args[body_idx..]);
Expr::List(forms)
let lambda = Expr::Lambda(params, Box::new(body));
if let Some(sym) = symbols.lookup_mut(&name) {
sym.set_function(lambda);
if let Some(d) = doc {
sym.set_doc(d);
let mut sym = Symbol::new(&name, SymbolKind::Variable).with_function(lambda);
sym = sym.with_doc(d);
symbols.define(sym);
Ok(Expr::Quote(Box::new(Expr::Symbol(name))))
pub(super) fn defvar(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
if args.is_empty() {
"DEFVAR requires at least a name".to_string(),
"DEFVAR: expected symbol name, got {other:?}"
let initial = args.get(1).map(|e| eval_value(symbols, e)).transpose()?;
let doc = match args.get(2) {
Some(Expr::String(s)) => Some(s.clone()),
_ => None,
if sym.value().is_none()
&& let Some(val) = initial
{
sym.set_value(val);
let mut sym = Symbol::new(&name, SymbolKind::Variable);
if let Some(val) = initial {
sym = sym.with_value(val);
pub(super) fn defparameter(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
if args.len() < 2 {
"DEFPARAMETER requires a name and initial value".to_string(),
"DEFPARAMETER: expected symbol name, got {other:?}"
let value = eval_value(symbols, &args[1])?;
sym.set_value(value);
let mut sym = Symbol::new(&name, SymbolKind::Variable).with_value(value);