Lines
50.62 %
Functions
18.21 %
Branches
100 %
use crate::ast::{Expr, WasmType};
use crate::error::{Error, Result};
use crate::runtime::{Symbol, SymbolKind, SymbolTable};
use super::super::context::CompileContext;
use super::super::emit::FunctionEmitter;
use super::super::expr::{
compile_body, compile_expr, compile_for_effect, compile_for_stack, compile_nil, eval_value,
};
use super::binding::eval_body;
use super::control::is_truthy;
pub(super) fn compile_do(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
args: &[Expr],
) -> Result<()> {
if args.len() < 2 {
return Err(Error::Compile(
"DO requires a variable list and an end clause".to_string(),
));
}
let vars = parse_do_vars("DO", &args[0])?;
let (end_test, result_forms) = parse_end_clause("DO", &args[1])?;
let end_test = end_test.clone();
let result_forms: Vec<Expr> = result_forms.to_vec();
let body = &args[2..];
let init_results: Vec<Expr> = vars
.iter()
.map(|v| match &v.init {
Some(expr) => eval_value(symbols, expr),
None => Ok(Expr::Nil),
})
.collect::<Result<_>>()?;
let mut needs_runtime = init_results.iter().any(Expr::is_wasm_runtime);
if !needs_runtime {
let mut trial = symbols.clone();
for (v, val) in vars.iter().zip(&init_results) {
trial.define(Symbol::new(&v.name, SymbolKind::Variable).with_value(val.clone()));
let test_result = eval_value(&mut trial, &end_test)?;
needs_runtime = test_result.is_wasm_runtime();
if needs_runtime {
let dl = DoLoop {
vars: &vars,
end_test: &end_test,
result_forms: &result_forms,
body,
sequential: false,
return compile_do_runtime(ctx, emit, symbols, &dl);
let mut local = symbols.clone();
let mut stepped: Vec<(String, Option<Expr>)> = Vec::new();
local.define(Symbol::new(&v.name, SymbolKind::Variable).with_value(val.clone()));
stepped.push((v.name.clone(), v.step.clone()));
if !static_loop_terminates(&local, &end_test, &stepped, false) {
loop {
let test = eval_value(&mut local, &end_test)?;
if is_truthy(&test) {
return if result_forms.is_empty() {
compile_nil(ctx, emit);
Ok(())
} else {
compile_body(ctx, emit, &mut local, &result_forms)
for expr in body {
compile_for_effect(ctx, emit, &mut local, expr)?;
let new_values: Vec<(&str, Expr)> = stepped
.filter_map(|(name, step)| {
step.as_ref()
.map(|s| eval_value(&mut local, s).map(|v| (name.as_str(), v)))
for (name, val) in new_values {
local
.lookup_mut(name)
.expect("DO variable must exist")
.set_value(val);
pub(super) fn compile_do_for_stack(
) -> Result<WasmType> {
compile_do_runtime_for_stack(ctx, emit, symbols, &dl)
pub(super) fn compile_do_star_for_stack(
"DO* requires a variable list and an end clause".to_string(),
let vars = parse_do_vars("DO*", &args[0])?;
let (end_test, result_forms) = parse_end_clause("DO*", &args[1])?;
sequential: true,
pub(super) fn compile_do_star(
// DO* evaluates inits sequentially — each binding is visible to later inits
let mut needs_runtime = false;
for v in &vars {
let val = match &v.init {
Some(expr) => eval_value(&mut local, expr)?,
None => Expr::Nil,
if val.is_wasm_runtime() {
needs_runtime = true;
local.define(Symbol::new(&v.name, SymbolKind::Variable).with_value(val));
let test_result = eval_value(&mut local, &end_test)?;
if !static_loop_terminates(&local, &end_test, &stepped, true) {
for (name, step) in &stepped {
if let Some(s) = step {
let val = eval_value(&mut local, s)?;
.expect("DO* variable must exist")
pub(super) fn compile_do_for_effect(
return compile_do_runtime_for_effect(ctx, emit, symbols, &dl);
let stepped: Vec<(String, Option<Expr>)> = vars
.map(|v| (v.name.clone(), v.step.clone()))
.collect();
for expr in &result_forms {
return Ok(());
pub(super) fn compile_do_star_for_effect(
pub(super) fn compile_dolist_for_effect(
"DOLIST requires a variable specification and at least one body form".to_string(),
let var_spec = args[0].as_list().ok_or_else(|| {
Error::Compile(format!(
"DOLIST: expected variable specification list, got {:?}",
args[0]
))
})?;
if var_spec.len() < 2 || var_spec.len() > 3 {
"DOLIST: variable specification must be (var list) or (var list result)".to_string(),
let var_name = var_spec[0].as_symbol().ok_or_else(|| {
"DOLIST: variable must be a symbol, got {:?}",
var_spec[0]
let list_expr = &var_spec[1];
let body = &args[1..];
let list_value = eval_value(symbols, list_expr)?;
if list_value.wasm_type() == Some(WasmType::ConsRef) {
return compile_dolist_runtime_for_effect(ctx, emit, symbols, var_name, list_expr, body);
let elements = match &list_value {
Expr::List(elems) => elems.clone(),
Expr::Nil => vec![],
Expr::Quote(inner) => match inner.as_ref() {
_ => {
"DOLIST: list expression must evaluate to a list".to_string(),
},
let saved_loop_var = symbols.lookup(var_name).cloned();
for element in elements {
symbols.define(Symbol::new(var_name, SymbolKind::Variable).with_value(element));
compile_for_effect(ctx, emit, symbols, expr)?;
if let Some(saved) = saved_loop_var {
symbols.define(saved);
symbols.remove(var_name);
fn compile_dolist_runtime_for_effect(
var_name: &str,
list_expr: &Expr,
body: &[Expr],
for (target, rhs) in collect_setf_targets(body) {
if target != var_name {
promote_to_wasm_local(ctx, emit, symbols, &target, rhs.as_ref())?;
let cons_local = ctx.alloc_local(WasmType::ConsRef);
compile_for_stack(ctx, emit, symbols, list_expr)?;
emit.local_set(cons_local);
let var_local = ctx.alloc_local(WasmType::I32);
symbols.define(
Symbol::new(var_name, SymbolKind::Variable)
.with_value(Expr::WasmLocal(var_local, WasmType::I32)),
);
emit.block_start();
emit.loop_start();
emit.local_get(cons_local);
emit.ref_is_null();
emit.br_if(1);
emit.struct_get(ctx.type_idx("cons"), 0);
emit.local_set(var_local);
emit.struct_get(ctx.type_idx("cons"), 1);
emit.br(0);
emit.block_end();
fn infer_wasm_type(expr: &Expr, step_hint: Option<&Expr>) -> WasmType {
match expr {
Expr::WasmRuntime(ty) | Expr::WasmLocal(_, ty) => *ty,
Expr::Number(_) => WasmType::Ratio,
Expr::Nil => {
if let Some(step) = step_hint
&& step_contains_cons(step)
{
return WasmType::ConsRef;
WasmType::I32
_ => WasmType::I32,
fn step_contains_cons(expr: &Expr) -> bool {
Expr::List(elems) => {
if let Some(Expr::Symbol(name)) = elems.first()
&& name == "CONS"
return true;
elems.iter().any(step_contains_cons)
_ => false,
struct DoLoop<'a> {
vars: &'a [DoVar],
end_test: &'a Expr,
result_forms: &'a [Expr],
body: &'a [Expr],
sequential: bool,
fn compile_do_runtime_loop(
local: &mut SymbolTable,
dl: &DoLoop<'_>,
let do_var_names: Vec<_> = dl.vars.iter().map(|v| v.name.clone()).collect();
for (target, rhs) in collect_setf_targets(dl.body) {
if !do_var_names.contains(&target) {
promote_to_wasm_local(ctx, emit, local, &target, rhs.as_ref())?;
let mut wasm_vars: Vec<(String, u32, WasmType, Option<Expr>)> = Vec::new();
for v in dl.vars {
let init_expr = v.init.clone().unwrap_or(Expr::Nil);
let init_val = eval_value(local, &init_expr)?;
let ty = infer_wasm_type(&init_val, v.step.as_ref());
let idx = ctx.alloc_local(ty);
if ty == WasmType::ConsRef && matches!(init_val, Expr::Nil) {
emit.ref_null(ctx.type_idx("cons"));
compile_for_stack(ctx, emit, local, &init_expr)?;
emit.local_set(idx);
local.define(
Symbol::new(&v.name, SymbolKind::Variable).with_value(Expr::WasmLocal(idx, ty)),
wasm_vars.push((v.name.clone(), idx, ty, v.step.clone()));
// block $exit
// loop $continue
compile_for_stack(ctx, emit, local, dl.end_test)?;
for expr in dl.body {
compile_for_effect(ctx, emit, local, expr)?;
if dl.sequential {
for (_, idx, _, step) in &wasm_vars {
if let Some(step_expr) = step {
compile_for_stack(ctx, emit, local, step_expr)?;
emit.local_set(*idx);
let step_vars: Vec<_> = wasm_vars
.filter(|(_, _, _, step)| step.is_some())
for (_, _, _, step) in &step_vars {
compile_for_stack(ctx, emit, local, step.as_ref().unwrap())?;
for (_, idx, _, _) in step_vars.iter().rev() {
emit.block_end(); // end loop
emit.block_end(); // end block
fn compile_do_runtime(
compile_do_runtime_loop(ctx, emit, &mut local, dl)?;
if dl.result_forms.is_empty() {
compile_body(ctx, emit, &mut local, dl.result_forms)?;
fn compile_do_runtime_for_effect(
for expr in dl.result_forms {
fn compile_do_runtime_for_stack(
emit.i32_const(0);
Ok(WasmType::I32)
let last = dl.result_forms.last().unwrap();
for expr in &dl.result_forms[..dl.result_forms.len() - 1] {
compile_for_stack(ctx, emit, &mut local, last)
pub(super) fn compile_dolist(
let result_expr = var_spec.get(2).cloned();
return compile_dolist_runtime(ctx, emit, symbols, var_name, list_expr, result_expr, body);
if let Some(result) = result_expr {
compile_expr(ctx, emit, symbols, &result)
fn collect_setf_targets(exprs: &[Expr]) -> Vec<(String, Option<Expr>)> {
let mut targets = Vec::new();
for expr in exprs {
collect_setf_targets_inner(expr, &mut targets);
targets
fn collect_setf_targets_inner(expr: &Expr, targets: &mut Vec<(String, Option<Expr>)>) {
if let Expr::List(elems) = expr {
&& name == "SETF"
for pair in elems[1..].chunks(2) {
if let Some(Expr::Symbol(var)) = pair.first()
&& !targets.iter().any(|(n, _)| n == var)
targets.push((var.clone(), pair.get(1).cloned()));
for elem in elems {
collect_setf_targets_inner(elem, targets);
fn promote_to_wasm_local(
name: &str,
step_hint: Option<&Expr>,
let sym = symbols
.lookup(name)
.ok_or_else(|| Error::UndefinedSymbol(name.to_string()))?;
if matches!(
sym.value(),
Some(Expr::WasmLocal(_, _) | Expr::WasmRuntime(_))
) {
let val = sym.value().cloned().unwrap_or(Expr::Nil);
let ty = infer_wasm_type(&val, step_hint);
if ty == WasmType::ConsRef && matches!(val, Expr::Nil) {
compile_for_stack(ctx, emit, symbols, &val)?;
symbols.define(Symbol::new(name, SymbolKind::Variable).with_value(Expr::WasmLocal(idx, ty)));
fn compile_dolist_runtime(
result_expr: Option<Expr>,
// Promote SETF targets in body to WASM locals so runtime updates persist
// test: ref.is_null → exit
// car → loop variable
// cdr → update cons local
// Restore loop variable
pub(super) fn do_form(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
let init_values: Vec<(String, Expr, Option<Expr>)> = vars
.into_iter()
.map(|v| {
let val = match v.init {
Some(expr) => eval_value(symbols, &expr)?,
Ok((v.name, val, v.step))
let needs_runtime = init_values.iter().any(|(_, v, _)| v.is_wasm_runtime());
for (name, val, step) in &init_values {
local.define(Symbol::new(name, SymbolKind::Variable).with_value(val.clone()));
stepped.push((name.clone(), step.clone()));
return Ok(Expr::WasmRuntime(WasmType::ConsRef));
if test_result.is_wasm_runtime() {
Ok(Expr::Nil)
eval_body(&mut local, &result_forms)
eval_value(&mut local, expr)?;
pub(super) fn do_star_form(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
for v in vars {
Some(expr) => eval_value(&mut local, &expr)?,
stepped.push((v.name, v.step));
pub(super) fn dolist_form(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
eval_value(symbols, expr)?;
eval_value(symbols, &result)
struct DoVar {
name: String,
init: Option<Expr>,
step: Option<Expr>,
fn parse_do_vars(context: &str, expr: &Expr) -> Result<Vec<DoVar>> {
let list = match expr {
Expr::List(elems) => elems,
return Err(Error::Compile(format!(
"{context}: expected variable list, got {expr:?}"
)));
list.iter()
.map(|spec| match spec {
Expr::Symbol(name) => Ok(DoVar {
name: name.clone(),
init: None,
step: None,
}),
Expr::List(elems) if !elems.is_empty() && elems.len() <= 3 => {
let name = elems[0].as_symbol().ok_or_else(|| {
"{context}: variable name must be a symbol, got {:?}",
elems[0]
Ok(DoVar {
name: name.to_string(),
init: elems.get(1).cloned(),
step: elems.get(2).cloned(),
_ => Err(Error::Compile(format!(
"{context}: malformed variable spec: {spec:?}"
))),
.collect()
const MAX_STATIC_LOOP_ITERS: usize = 64;
fn static_loop_terminates(
local: &SymbolTable,
end_test: &Expr,
stepped: &[(String, Option<Expr>)],
) -> bool {
let mut sim = local.clone();
for _ in 0..MAX_STATIC_LOOP_ITERS {
match eval_value(&mut sim, end_test) {
Ok(test) if is_truthy(&test) => return true,
Ok(_) => {}
Err(_) => return false,
if sequential {
for (name, step) in stepped {
let Some(s) = step else { continue };
match eval_value(&mut sim, s) {
Ok(val) => {
if let Some(sym) = sim.lookup_mut(name) {
sym.set_value(val);
let new_vals: Vec<_> = stepped
.and_then(|s| eval_value(&mut sim, s).ok().map(|v| (name.clone(), v)))
for (name, val) in new_vals {
if let Some(sym) = sim.lookup_mut(&name) {
false
fn parse_end_clause<'a>(context: &str, expr: &'a Expr) -> Result<(&'a Expr, &'a [Expr])> {
let elems = expr.as_list().ok_or_else(|| {
"{context}: end clause must be a list, got {expr:?}"
if elems.is_empty() {
"{context}: end clause must have a test form"
Ok((&elems[0], &elems[1..]))