Lines
82.42 %
Functions
57.45 %
Branches
100 %
mod arithmetic;
mod comparison;
mod convert;
mod entity;
mod error_object;
mod host_fn;
mod io;
mod list;
mod report_node;
mod string;
mod structure;
mod typed_entity;
#[cfg(test)]
mod typed_entity_tests;
use super::context::CompileContext;
use super::emit::FunctionEmitter;
use crate::ast::{Expr, WasmType};
use crate::error::{Error, Result};
use crate::runtime::SymbolTable;
pub(in crate::compiler) use entity::compile_create_tag;
pub(super) use host_fn::{compile_host_fn_for_effect, compile_host_fn_for_stack};
pub(super) use io::compile_debug_effect;
pub(super) use io::{compile_newline_effect, compile_print_effect};
pub(in crate::compiler) use list::emit_pair_car_downcast;
/// Eval-time handler — folds constants, surfaces a `WasmRuntime` /
/// `WasmLocal` stand-in when arguments aren't fully known, validates
/// arity. Required for every native; the compile paths build on top.
pub(super) type EvalFn = fn(&mut SymbolTable, &[Expr]) -> Result<Expr>;
/// Stack-producing codegen — emits wasm that leaves the form's result
/// on the wasm operand stack and returns its `WasmType`. Required for
/// natives that compose as sub-expressions (CONS in `(+ x (car xs))`,
/// every arithmetic op, entity accessors). Natives whose result has
/// no single stack representation (CREATE-TAG, DELETE-ENTITY, MAP)
/// set this to `None` — the dispatcher refuses with a structured error.
pub(super) type StackFn =
fn(&mut CompileContext, &mut FunctionEmitter, &mut SymbolTable, &[Expr]) -> Result<WasmType>;
/// Effect codegen — emits wasm at the top-level / for-effect position.
/// Most natives derive this from `stack` by emitting the stack form and
/// piping the result through the debug serializer; effect-only natives
/// (DEBUG side-effects, MAP folded at compile time) supply their own.
pub(super) type EffectFn =
fn(&mut CompileContext, &mut FunctionEmitter, &mut SymbolTable, &[Expr]) -> Result<()>;
/// Canonical metadata for a built-in native fn. Every name appears
/// exactly once across the three dispatch paths — adding a new native
/// adds one row, and a missing handler is a compile-time error.
/// Replaces the three parallel hand-maintained match tables that
/// diverged silently before P3a 3a.3.
///
/// `effect: None` auto-derives the effect path as `stack + serialize`
/// — useful for the ~20 natives whose effect codegen is exactly
/// "produce the value on the stack, then debug-serialize". Natives
/// with custom effect logic (const-fold short-circuits that write
/// to output directly, structure forms, etc.) supply an explicit
/// EffectFn. `effect: None` requires `stack: Some(_)` — the
/// dispatcher refuses an `effect = None, stack = None` spec.
pub(super) struct NativeSpec {
pub name: &'static str,
pub eval: EvalFn,
pub stack: Option<StackFn>,
pub effect: Option<EffectFn>,
}
const DOMAINS: &[&[NativeSpec]] = &[
arithmetic::NATIVES,
comparison::NATIVES,
convert::NATIVES,
list::NATIVES,
entity::NATIVES,
typed_entity::NATIVES,
report_node::NATIVES,
structure::NATIVES,
string::NATIVES,
io::NATIVES,
error_object::NATIVES,
];
fn lookup(name: &str) -> Option<&'static NativeSpec> {
DOMAINS
.iter()
.flat_map(|d| d.iter())
.find(|s| s.name == name)
pub(super) fn call(symbols: &mut SymbolTable, name: &str, args: &[Expr]) -> Result<Expr> {
match lookup(name) {
Some(spec) => (spec.eval)(symbols, args),
None => Err(Error::Compile(format!(
"native function '{name}' not yet implemented"
))),
pub(super) fn compile_for_stack(
ctx: &mut CompileContext,
emit: &mut FunctionEmitter,
symbols: &mut SymbolTable,
name: &str,
args: &[Expr],
) -> Result<WasmType> {
if ctx.lookup_host_fn(name).is_some() {
return compile_host_fn_for_stack(ctx, emit, symbols, name, args);
Some(spec) => match spec.stack {
Some(f) => f(ctx, emit, symbols, args),
"native function '{name}' cannot produce stack value"
},
pub(super) fn compile(
) -> Result<()> {
return compile_host_fn_for_effect(ctx, emit, symbols, name, args);
Some(spec) => match spec.effect {
None => derived_effect(ctx, emit, symbols, name, args, spec),
/// Default effect path for natives with `effect: None`: compile via
/// the stack handler, then debug-serialize the result. Same shape as
/// the ~20 wrapper fns this replaces. Refuses if the spec also lacks
/// a stack handler — that's a programmer error in the registry, and
/// the registry test enforces "effect None implies stack Some".
fn derived_effect(
spec: &NativeSpec,
let stack_fn = spec.stack.ok_or_else(|| {
Error::Compile(format!(
"native function '{name}' has neither effect nor stack handler"
))
})?;
let ty = stack_fn(ctx, emit, symbols, args)?;
super::expr::serialize_stack_to_output(ctx, emit, ty)?;
Ok(())
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn registry_names_unique() {
let mut seen = HashSet::new();
for spec in DOMAINS.iter().flat_map(|d| d.iter()) {
assert!(
seen.insert(spec.name),
"duplicate native registration: {}",
spec.name
);
fn registry_lookup_covers_every_entry() {
// Every registered name must be reachable via the lookup helper —
// catches accidental shadowing if a domain reorders its slice.
lookup(spec.name).is_some(),
"lookup({}) returned None",
fn registry_none_effect_implies_some_stack() {
// Auto-derived effect path needs the stack handler — a spec with
// both None is a registry programmer error that the dispatcher
// can only surface at runtime. Catch at unit-test time.
if spec.effect.is_none() {
spec.stack.is_some(),
"native {} has neither effect nor stack handler",
/// Every native NAME the reader registers as a builtin (tangled from
/// `builtin_reference.org` into `builtins_generated::NATIVES`) must have a
/// codegen handler reachable via `lookup`. Without this guard a documented
/// native can exist as a symbol the reader accepts yet codegen rejects with
/// "native function '…' not yet implemented" — the "phantom native" class
/// that hid EQUAL?/EQ?/LENGTH/APPEND/PAIR?/PRINT/DISPLAY/NEWLINE until a
/// script happened to call one. The samples are parse-only, so only this
/// test (and real compile coverage) catches the gap.
fn every_builtin_native_name_has_a_handler() {
for name in crate::runtime::registered_native_names() {
lookup(name).is_some(),
"builtin native '{name}' is registered as a symbol but has no \
codegen handler — add a NativeSpec or remove it from the registry"