Lines
94.91 %
Functions
52.81 %
Branches
100 %
mod context;
mod emit;
pub(crate) mod expr;
mod layout;
mod native;
pub mod special;
use tracing::debug;
use crate::ast::{Expr, Program, WasmType};
use crate::error::{Error, Result};
use crate::host_fn::HostFnSpec;
use crate::runtime::SymbolTable;
use context::CompileContext;
use emit::FunctionEmitter;
/// Selects which export shape the compiler emits.
///
/// `Script` keeps the entity-script surface (`should_apply` + `process`
/// exports, env.* imports) the script executor consumes. `Eval` emits a
/// `nomi-eval` export that runs the program and returns the final value
/// via the function's `(ref null any)` return slot; the host decodes it
/// with `scripting::runtime::decode_eval_result`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompileMode {
Script,
Eval,
}
pub struct Compiler {
host_fns: Vec<HostFnSpec>,
impl Compiler {
#[must_use]
pub fn new() -> Self {
Self {
host_fns: Vec::new(),
/// Builds a compiler that recognises the given host fn names in eval-mode
/// programs. Each spec gets a wasm import declared in the module and an
/// entry the native dispatcher consults when emitting calls. Has no effect
/// in `Script` mode (entity-script bytecode imports `env.*`, not `nomi.*`).
pub fn with_host_fns(host_fns: Vec<HostFnSpec>) -> Self {
Self { host_fns }
pub fn compile(&mut self, program: &Program, symbols: &mut SymbolTable) -> Result<Vec<u8>> {
self.compile_with_mode(program, symbols, CompileMode::Script)
pub fn compile_with_mode(
&mut self,
program: &Program,
symbols: &mut SymbolTable,
mode: CompileMode,
) -> Result<Vec<u8>> {
debug!(expr_count = program.exprs.len(), ?mode, "compilation start");
match mode {
CompileMode::Script => self.compile_script(program, symbols),
CompileMode::Eval => self.compile_eval(program, symbols),
fn compile_script(&mut self, program: &Program, symbols: &mut SymbolTable) -> Result<Vec<u8>> {
let mut ctx = CompileContext::new()?;
// Snapshot how many helpers (gcd / ratio_* / commodity_* / pair_*)
// were queued during context bootstrap; these correspond to the
// function-section slots between `should_apply` and `process`,
// so their bodies must drain *with* `should_apply`'s emit. User
// code inside `process` may queue more helpers (e.g. a real
// wasm fn for a `(lambda ...)` value); those slots land *after*
// `process` and have to drain *with* `process`'s emit so the
// code-section ordering matches the function-section.
let bootstrap_helper_count = ctx.pending_helper_count();
let mut process = FunctionEmitter::new();
// Wrap the body in the Tier 3 boundary `try_table` so an uncaught
// `(error)` throw bridges to `__nomi_raise` (ADR-0026). `process`
// returns void.
ctx.emit_boundary_wrapper(&mut process, None, |ctx, emit| {
// output_base = get_output_offset()
emit.call(ctx.ids.get_output_offset()?);
emit.local_set(expr::LOCAL_OUTPUT_BASE);
expr::compile_program(ctx, emit, symbols, program)
})?;
process.end();
let process_locals = ctx.build_locals_declaration();
ctx.reset_locals();
let should_apply = self.build_should_apply(&mut ctx, symbols)?;
ctx.add_should_apply(should_apply, bootstrap_helper_count);
ctx.add_process(process.finish(&process_locals));
let wasm = ctx.finish();
debug!(wasm_size = wasm.len(), "compilation complete");
Ok(wasm)
fn compile_eval(&mut self, program: &Program, symbols: &mut SymbolTable) -> Result<Vec<u8>> {
let (wasm, _ty) = self.compile_eval_with_type(program, symbols)?;
/// Compiles `program` in eval mode and returns the wasm bytes paired
/// with the form's final result type. `None` for empty programs /
/// definition-only forms (nomi-eval returns `ref.null any`); `Some(ty)`
/// for any other terminator. Hosts decode the returned anyref via the
/// type hint — see `scripting::runtime::decode_eval_result`.
pub fn compile_eval_with_type(
) -> Result<(Vec<u8>, Option<WasmType>)> {
let mut ctx = CompileContext::new_eval_with_host_fns(&self.host_fns)?;
let mut emit = FunctionEmitter::new();
// `nomi-eval` returns anyref. The boundary wrapper bridges an
// uncaught `(error)` to `__nomi_raise` (ADR-0026). The body's
// final result type is computed inside the closure and surfaced
// out for the host's `decode_eval_result` hint.
let mut result_ty: Option<WasmType> = None;
ctx.emit_boundary_wrapper(&mut emit, Some(WasmType::AnyRef), |ctx, emit| {
if program.exprs.is_empty() {
emit.ref_null_any();
return Ok(());
let last_idx = program.exprs.len() - 1;
for expr in &program.exprs[..last_idx] {
expr::compile_for_effect(ctx, emit, symbols, expr)?;
let last = &program.exprs[last_idx];
if is_definition_form(last) {
expr::compile_for_effect(ctx, emit, symbols, last)?;
} else {
let ty = expr::compile_for_stack(ctx, emit, symbols, last)?;
emit_to_anyref(emit, ty);
result_ty = Some(ty);
Ok(())
emit.end();
let locals = ctx.build_locals_declaration();
ctx.add_nomi_eval(emit.finish(&locals));
debug!(wasm_size = wasm.len(), "eval compilation complete");
Ok((wasm, result_ty))
fn build_should_apply(
&self,
ctx: &mut CompileContext,
) -> Result<wasm_encoder::Function> {
let body = symbols
.lookup("SHOULD-APPLY")
.and_then(|s| s.function().cloned());
let Some(Expr::Lambda(params, body)) = body else {
return Ok(CompileContext::default_should_apply());
};
if !params.required.is_empty() {
return Err(Error::Compile(
"SHOULD-APPLY must take no parameters".to_string(),
));
debug!("compiling custom should-apply");
// Boundary wrapper bridges an uncaught `(error)` in the body to
// `__nomi_raise` (ADR-0026). `should_apply` returns i32.
ctx.emit_boundary_wrapper(&mut emit, Some(WasmType::I32), |ctx, emit| {
let ty = expr::compile_for_stack(ctx, emit, symbols, &body)?;
match ty {
// Bool is the natural should-apply result (a predicate); I32
// (a raw count used as truthy) is also accepted. Both are
// i32-repr, so the i32 return passes through unchanged.
WasmType::I32 | WasmType::Bool => Ok(()),
WasmType::Ratio => {
emit.struct_get(ctx.ids.ty_ratio, 0);
emit.i64_const(0);
emit.i64_ne();
_ => Err(Error::Compile(
"SHOULD-APPLY must return a boolean or numeric value".to_string(),
)),
Ok(emit.finish(&locals))
/// Promotes the form's final value (already on the wasm stack as its
/// declared `WasmType`) to an anyref-subtype so it satisfies nomi-eval's
/// `(ref null any)` return slot. Reference-typed values (ratio_ref,
/// commodity_ref, i8_array, pair_ref, entity_ref) are anyref subtypes
/// already; primitive `i32` needs `ref.i31` boxing.
fn emit_to_anyref(emit: &mut FunctionEmitter, ty: WasmType) {
// Both i32-repr value types box into an `(ref i31)`.
WasmType::I32 | WasmType::Bool => emit.ref_i31(),
WasmType::Ratio
| WasmType::Commodity
| WasmType::StringRef
| WasmType::PairRef(_)
| WasmType::EntityRef(_)
| WasmType::Closure(_)
| WasmType::AnyRef => {}
fn is_definition_form(expr: &Expr) -> bool {
let Expr::List(elems) = expr else {
return false;
let Some(Expr::Symbol(name)) = elems.first() else {
matches!(
name.as_str(),
"DEFUN" | "DEFVAR" | "DEFMACRO" | "DEFPARAMETER" | "DEFSTRUCT"
)
impl Default for Compiler {
fn default() -> Self {
Self::new()
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::Expr;
use crate::runtime::{Symbol, SymbolKind};
#[test]
fn test_compile_empty_program() {
let program = Program::default();
let mut compiler = Compiler::new();
let wasm = compiler.compile(&program, &mut SymbolTable::new()).unwrap();
assert!(!wasm.is_empty());
assert_eq!(&wasm[0..4], b"\0asm");
fn test_compile_eval_empty_program_emits_capture_nil() {
let wasm = compiler
.compile_with_mode(&program, &mut SymbolTable::new(), CompileMode::Eval)
.unwrap();
fn test_compile_eval_integer_literal() {
let program = Program::new(vec![Expr::Number(num_rational::Ratio::from_integer(7))]);
fn test_compile_eval_arithmetic() {
let program = Program::new(vec![Expr::List(vec![
Expr::Symbol("+".into()),
Expr::Number(num_rational::Ratio::from_integer(1)),
Expr::Number(num_rational::Ratio::from_integer(2)),
])]);
let mut symbols = SymbolTable::with_builtins();
.compile_with_mode(&program, &mut symbols, CompileMode::Eval)
fn test_compile_default_uses_script_mode() {
let program = Program::new(vec![Expr::Bool(true)]);
let mut symbols = SymbolTable::new();
let default_bytes = compiler.compile(&program, &mut symbols).unwrap();
let explicit_bytes = compiler
.compile_with_mode(&program, &mut symbols, CompileMode::Script)
assert_eq!(default_bytes, explicit_bytes);
fn test_compile_eval_and_script_produce_distinct_bytes() {
let program = Program::new(vec![Expr::Number(num_rational::Ratio::from_integer(1))]);
let mut s1 = SymbolTable::new();
let mut s2 = SymbolTable::new();
let script_bytes = compiler
.compile_with_mode(&program, &mut s1, CompileMode::Script)
let eval_bytes = compiler
.compile_with_mode(&program, &mut s2, CompileMode::Eval)
assert_ne!(script_bytes, eval_bytes);
fn test_compile_nil() {
let program = Program::new(vec![Expr::Nil]);
fn test_compile_bool() {
fn test_compile_number() {
use num_rational::Ratio;
let program = Program::new(vec![Expr::Number(Ratio::new(1, 2))]);
fn test_compile_string() {
let program = Program::new(vec![Expr::String("hello".into())]);
fn test_compile_symbol_with_value() {
symbols.define(Symbol::new("REVISION", SymbolKind::Variable).with_value(Expr::Bool(true)));
let program = Program::new(vec![Expr::Symbol("REVISION".into())]);
let wasm = compiler.compile(&program, &mut symbols).unwrap();
fn test_compile_undefined_symbol() {
let program = Program::new(vec![Expr::Symbol("UNKNOWN".into())]);
let result = compiler.compile(&program, &mut SymbolTable::new());
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, crate::error::Error::UndefinedSymbol(_)));
fn test_defun_populates_function_cell() {
Expr::Symbol("DEFUN".into()),
Expr::Symbol("SUM".into()),
Expr::List(vec![
Expr::Symbol("A".into()),
Expr::Symbol("B".into()),
Expr::Symbol("C".into()),
]),
Expr::String("Sums A, B, C".into()),
compiler.compile(&program, &mut symbols).unwrap();
let sym = symbols.lookup("SUM").expect("SUM should be defined");
assert!(sym.function().is_some());
assert!(matches!(sym.function(), Some(Expr::Lambda(_, _))));
assert_eq!(sym.doc(), Some("Sums A, B, C"));
fn test_defun_no_doc_populates_function_cell() {
Expr::Symbol("ADD".into()),
Expr::List(vec![Expr::Symbol("A".into()), Expr::Symbol("B".into())]),
let sym = symbols.lookup("ADD").expect("ADD should be defined");
assert!(sym.doc().is_none());