Lines
100 %
Functions
45 %
Branches
//! Expression-level compile + eval dispatch.
//!
//! Split into topic-focused submodules so each file stays under the
//! ~500-line CLAUDE.md guideline:
//! - [`atoms`] — primitive emit (nil, bool, number, string, symbol,
//! push_ratio) and the `compile_text` plumbing they share.
//! - [`compile`] — the generic `compile_expr` / `compile_quoted_expr`
//! dispatch + `serialize_stack_to_output`.
//! - [`effect`] — effect-position codegen (`compile_for_effect` plus
//! the SETF / IF specialisations it owns).
//! - [`stack`] — stack-position codegen (`compile_for_stack` and the
//! ratio/numeric refinements).
//! - [`call`] — function call dispatch (symbol calls, lambda calls,
//! lambda-param binding).
//! - [`eval`] — eval-only path (`call`, `eval_value`, `dispatch_symbol`,
//! macro expansion, lambda-call eval, `resolve_arg`).
//! - [`quasiquote`] — quasiquote / unquote expansion.
//! - [`format`] — debug/display formatting for Expr and RuntimeValue.
//! `LOCAL_*` indices, `compile_program`, and the two `compile_body*`
//! helpers stay here in `mod.rs` since they're the cross-submodule
//! glue every entry point lands on.
mod atoms;
mod call;
mod compile;
mod effect;
mod eval;
mod format;
mod quasiquote;
mod stack;
use crate::ast::{Expr, Program, WasmType};
use crate::error::Result;
use crate::runtime::SymbolTable;
use super::context::CompileContext;
use super::emit::FunctionEmitter;
// Reserved local-pool indices the compiler relies on across codegen
// paths. The output-base + GC-array slots come first so they're
// available before the `next_local` allocator kicks in. See
// `CompileContext::build_locals_declaration` for the matching wasm-
// side declaration.
pub const LOCAL_GC_ARR: u32 = 0;
pub const LOCAL_IDX: u32 = 1;
pub const LOCAL_OUTPUT_BASE: u32 = 2;
pub const LOCAL_TEMP_RATIO: u32 = 3;
pub const LOCAL_TEMP_I32: u32 = 4;
/// Absolute address of the entity currently being written to the output
/// buffer (`output_base + next_write_pos`). Every output writer positions its
/// entity header + data fields relative to this so writes APPEND at the runtime
/// `next_write_pos` — the single output protocol that lets compile-time-known
/// writes (a program result) and dynamic-count writes (a `create-tag` loop)
/// share one buffer without overwriting each other.
pub const LOCAL_ENTITY_BASE: u32 = 5;
// Submodule re-exports. Keeps `super::expr::<name>` callers stable
// across the refactor so neighbouring modules (`native`, `special`,
// the compiler root) don't have to learn the submodule layout.
pub(super) use atoms::{compile_bool, compile_nil, compile_string, emit_nil_default, push_ratio};
pub(super) use call::compile_call;
pub(super) use compile::{compile_expr, compile_quoted_expr, serialize_stack_to_output};
pub(super) use effect::compile_for_effect;
pub(super) use stack::{
classify_stack_type, compile_call_for_stack, compile_for_stack, compile_for_stack_as,
};
pub(crate) use eval::{call, eval_value, expand_macro, resolve_arg};
pub(crate) use format::format_expr;
pub fn compile_program(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
program: &Program,
) -> Result<()> {
ctx.serializer().begin_output(emit);
if program.exprs.is_empty() {
compile_nil(ctx, emit);
return Ok(());
}
let has_trigger = program.exprs.iter().any(|e| {
matches!(
e,
Expr::List(elems)
if matches!(elems.first(), Some(Expr::Symbol(name)) if name == "DEFUN")
&& matches!(elems.get(1), Some(Expr::Symbol(name)) if name == "SHOULD-APPLY")
)
});
if has_trigger {
for expr in &program.exprs {
compile_for_effect(ctx, emit, symbols, expr)?;
Ok(())
} else {
for expr in &program.exprs[..program.exprs.len() - 1] {
compile_expr(ctx, emit, symbols, program.exprs.last().unwrap())
pub(super) fn compile_body(
body: &[Expr],
for expr in &body[..body.len() - 1] {
compile_expr(ctx, emit, symbols, body.last().unwrap())
pub(super) fn compile_body_for_stack(
) -> Result<WasmType> {
compile_for_stack(ctx, emit, symbols, body.last().unwrap())