Lines
88.48 %
Functions
31 %
Branches
100 %
//! `DO` (parallel-step variant) compile + eval handlers.
//!
//! The three compile entry points (`compile_do`, `_for_effect`,
//! `_for_stack`) each handle the static-fold path inline; if the
//! end-test or any init is runtime-typed, they hand off to the
//! corresponding `runtime::compile_do_runtime{,_for_effect,_for_stack}`.
//! `do_form` is the eval-only handler for value-position use.
use crate::ast::{Expr, PairElement, WasmType};
use crate::compiler::context::CompileContext;
use crate::compiler::emit::FunctionEmitter;
use crate::compiler::expr::{
classify_stack_type, compile_body, compile_for_effect, compile_nil, eval_value,
};
use crate::error::{Error, Result};
use crate::runtime::{Symbol, SymbolKind, SymbolTable};
use super::super::binding::eval_body;
use super::super::control::is_truthy;
use super::common::{
DoLoop, infer_result_pair_element, infer_wasm_type, parse_do_vars, parse_end_clause,
static_loop_terminates,
use super::runtime::{
compile_do_runtime, compile_do_runtime_for_effect, compile_do_runtime_for_stack,
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_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(());
/// The runtime type a DO loop yields, mirroring `compile_do_runtime_for_stack`
/// (which returns `compile_for_stack` of the LAST result form, or `Bool`/nil
/// when there is none). A result form the body grows via
/// `(setf acc (cons V acc))` is a list whose element comes from the cons car
/// (`infer_result_pair_element`); any other form — a counter, a do var — takes
/// its own resolved runtime type. Hard-coding `PairRef` mistyped a counting
/// loop `(do … (end n) (setf n (+ n 1)))` as a pair, so `(= (count …) 0)`
/// rejected it as "= expects numeric arguments, got pair".
pub(super) fn runtime_result_type(
result_forms: &[Expr],
body: &[Expr],
stepped: &[(String, Option<Expr>)],
infer_env: &SymbolTable,
) -> WasmType {
let Some(last) = result_forms.last() else {
return WasmType::Bool;
if let Some(elem) = infer_result_pair_element(last, body, stepped, infer_env) {
return WasmType::PairRef(elem);
// Resolve the result form against the runtime-typed loop env, then type it
// the way codegen's `compile_for_stack` would: a runtime value reports its
// own `wasm_type`; a const numeric literal goes through `classify_stack_type`
// (an integer counter → I32, matching the let-promoted local codegen emits).
let resolved = eval_value(&mut infer_env.clone(), last).unwrap_or(Expr::Nil);
resolved
.wasm_type()
.or_else(|| classify_stack_type(&resolved))
.unwrap_or(WasmType::PairRef(PairElement::I32))
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)?,
None => Expr::Nil,
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()));
// Compute the static return type. When a result-form names a single
// accumulator that the body builds via `(setf acc (cons V acc))`,
// we peek at the CONS car's type so the static type matches the
// PairElement the codegen path actually emits. Without this, the
// hardcoded PairRef(I32) disagrees with the codegen (which
// allocates the accumulator's element from the car's WasmType),
// and consumer dolists end up downcasting to the wrong element
// shape — a runtime cast failure.
// Resolve the accumulator's element type against an env where each DO var
// is bound as its RUNTIME type, not its const init. The codegen loop var is
// runtime (an integer init `(i 0 …)` makes `i` a runtime Index/I32), so a
// `(cons i acc)` cell is an I32 cell; resolving `i` to the const `0` would
// type it as Ratio (the numeric-literal cell slot) and a consumer dolist
// would downcast to the wrong element → a runtime cast trap.
let mut infer_env = symbols.clone();
let ty = infer_wasm_type(val, step.as_ref(), &infer_env);
infer_env.define(Symbol::new(name, SymbolKind::Variable).with_value(Expr::WasmRuntime(ty)));
let result_ty = runtime_result_type(&result_forms, body, &stepped, &infer_env);
return Ok(Expr::WasmRuntime(result_ty));
let test_result = eval_value(&mut local, &end_test)?;
if test_result.is_wasm_runtime() {
Ok(Expr::Nil)
eval_body(&mut local, &result_forms)
eval_value(&mut local, expr)?;