Lines
96.4 %
Functions
41.67 %
Branches
100 %
use std::collections::HashMap;
use crate::ast::Expr;
use super::entry::{Symbol, SymbolKind};
#[derive(Debug, Clone, Default)]
pub struct SymbolTable {
pub(super) symbols: HashMap<String, Symbol>,
pub(super) struct_fields: HashMap<String, Vec<String>>,
/// Registered nomiscript tests, in declaration order. Populated
/// by `DEFTEST`, drained by `RUN-TESTS`. Each entry is
/// `(name, body)` where `body` is the test's wrapped (BEGIN ...)
/// form. Lives on the table so each compiler/evaluator instance
/// gets its own isolated registry — no global state.
pub(super) tests: Vec<(String, Expr)>,
/// Per-native-fn compile-time reference counter. Bumped each time
/// the compiler emits a `call $<host-fn-idx>` for a registered
/// `HostFnSpec`. Read by `(coverage-dump)` to certify the test
/// surface reaches every native. Compile-time count (not runtime
/// hits) — sufficient for the parity contract "every native has
/// at least one test that compiles against it"; runtime hit
/// counts via a wasm-global counter array land in a follow-up.
pub(super) native_coverage: HashMap<String, u32>,
/// Current macro-expansion nesting depth. Bumped on entry to each
/// `expand_macro` (which clones the table for the expansion body), so
/// the count accumulates down a chain of nested expansions and a
/// self-referential macro is caught before it overflows the stack.
/// Lives on the table because every expansion site threads
/// `&mut SymbolTable`; a top-level form starts at 0.
pub(super) macro_depth: usize,
/// Names of defuns currently being inlined on the eval path, in entry
/// order. The eval-side `call_lambda` walks a defun body inline for
/// constant folding; a recursive defun whose recursion doesn't fold to a
/// base case (e.g. a runtime-arg call) would otherwise recurse the
/// compiler's native stack forever. A name already on this stack signals
/// re-entry, so the call site short-circuits to a runtime placeholder
/// (handing the real lowering to the codegen monomorph path) rather than
/// inlining again. Threaded on the table because the eval path only ever
/// carries `&mut SymbolTable`.
pub(super) inline_stack: Vec<String>,
/// Result `WasmType` of each emitted closure, keyed by `ClosureSigId`'s raw
/// id. A closure value carries only `WasmType::Closure(sig)` — the sig's
/// param/result types live on the codegen `CompileContext`, invisible to the
/// eval surface. The closure-emit sites (which have the context) record the
/// result here so the ctx-less eval paths — FOLD's accumulator-type probe,
/// MAP/FILTER element typing, binding-local sizing — predict a HOF result
/// type that AGREES with what `compile_*_to_stack` emits (which reads the
/// real sig). Without it, eval guesses from the seed and drifts from
/// codegen, mis-sizing a local (invalid wasm). Carried on the table because
/// the eval recursion only ever threads `&mut SymbolTable`.
pub(super) closure_results: HashMap<u32, crate::ast::WasmType>,
}
/// Hard ceiling on nested macro expansion. A self-reproducing macro
/// (`(defmacro m () '(m))`) would otherwise recurse until the compiler's
/// native stack overflows; this turns that into a structured compile
/// error. Each level clones the symbol table and re-enters the full
/// eval/compile recursion, so the per-level native-stack cost is high —
/// `64` (matching `MAX_STATIC_LOOP_ITERS`) stays comfortably within a
/// worker-thread stack while far exceeding any legitimate macro nesting.
pub const MAX_MACRO_EXPANSION_DEPTH: usize = 64;
/// Hard ceiling on nested eval-time function inlining. The eval path walks a
/// defun body inline for constant folding; a recursive defun whose recursion
/// doesn't fold to a base case (a runtime-arg call, or genuinely unbounded
/// recursion) would otherwise recurse the compiler's native stack until it
/// overflows. This turns that into a structured compile error.
///
/// Set conservatively below the macro ceiling: an inline level is far heavier
/// on the native stack than a macro-expansion level (it clones the table,
/// binds params, and re-enters the full `eval_value` recursion through
/// `if`/arithmetic/etc.), and compilation is not guaranteed to run on a large
/// stack — empirically a default ~2 MiB thread overflows around ~64 inline
/// levels. `32` leaves roughly a 2× native-stack margin while still allowing
/// far deeper const-folded recursion than any realistic accounting script
/// uses (the deepest legitimate cases are a handful of levels).
pub const MAX_INLINE_DEPTH: usize = 32;
impl SymbolTable {
#[must_use]
pub fn new() -> Self {
Self::default()
/// Enter one macro-expansion level, erroring once the depth ceiling is
/// crossed. Bumped before a macro's expansion is re-evaluated (where a
/// self-referential macro would otherwise recurse forever) and undone
/// by [`Self::exit_macro_expansion`] once that subtree finishes, so
/// sibling expansions don't accumulate. Turns a non-terminating macro
/// into a structured compile error instead of a native stack overflow.
pub(crate) fn enter_macro_expansion(&mut self) -> crate::error::Result<()> {
if self.macro_depth >= MAX_MACRO_EXPANSION_DEPTH {
return Err(crate::error::Error::Compile(format!(
"macro expansion exceeded depth {MAX_MACRO_EXPANSION_DEPTH} \
(recursive or non-terminating macro?)"
)));
self.macro_depth += 1;
Ok(())
pub(crate) fn exit_macro_expansion(&mut self) {
self.macro_depth = self.macro_depth.saturating_sub(1);
/// Whether `name` is already being inlined further up the eval-time call
/// chain — i.e. inlining it again would be (mutual) recursion.
pub(crate) fn is_inlining(&self, name: &str) -> bool {
self.inline_stack.iter().any(|n| n == name)
/// Mark `name` as entering an eval-time inline walk. Paired with
/// [`Self::exit_inline`]; the depth ceiling turns a deeply-nested
/// (or unbounded) inline recursion into a structured compile error
/// instead of a native stack overflow — the eval-path analogue of
/// [`Self::enter_macro_expansion`].
pub(crate) fn enter_inline(&mut self, name: &str) -> crate::error::Result<()> {
if self.inline_stack.len() >= MAX_INLINE_DEPTH {
"function inlining exceeded depth {MAX_INLINE_DEPTH} \
(recursive or non-terminating call to '{name}'?)"
self.inline_stack.push(name.to_string());
pub(crate) fn exit_inline(&mut self) {
self.inline_stack.pop();
/// Records the result `WasmType` for a closure signature id (see
/// [`Self::closure_results`]). Called by the closure-emit sites, which hold
/// the `CompileContext` and so know the sig's true result type.
pub fn record_closure_result(
&mut self,
sig: crate::ast::ClosureSigId,
result: crate::ast::WasmType,
) {
self.closure_results.insert(sig.0, result);
/// The recorded result `WasmType` for a closure signature id, or `None` if
/// the closure wasn't emitted through a recording site (e.g. a closure
/// reaching the eval surface from a path that doesn't record).
pub fn closure_result(&self, sig: crate::ast::ClosureSigId) -> Option<crate::ast::WasmType> {
self.closure_results.get(&sig.0).copied()
pub fn with_builtins() -> Self {
let mut table = Self::new();
table.register_builtins();
table.load_standard_library();
// Re-register entity accessors after stdlib to override DEFSTRUCT-generated ones
table.register_entity_accessors();
// Prelude loads last so its DEFUNs resolve the builtins + accessors above.
table.load_prelude();
table
pub fn with_builtins_for_wasm() -> Self {
pub fn define(&mut self, symbol: Symbol) {
self.symbols.insert(symbol.name.clone(), symbol);
/// Register a named test (`DEFTEST`); `RUN-TESTS` reads it via [`tests`].
/// Re-registering an existing name overwrites its body in place (matching
/// [`define`]'s overwrite semantics) rather than appending, so the
/// two-surface compile — which may evaluate a `DEFTEST` on both the eval
/// and codegen passes — stays idempotent and never double-runs a test.
pub fn register_test(&mut self, name: impl Into<String>, body: Expr) {
let name = name.into();
match self
.tests
.iter_mut()
.find(|(existing, _)| *existing == name)
{
Some(entry) => entry.1 = body,
None => self.tests.push((name, body)),
/// Snapshot of registered tests in declaration order. Cloned so
/// `RUN-TESTS` can iterate without holding a borrow on the table
/// it needs to evaluate against.
pub fn tests(&self) -> Vec<(String, Expr)> {
self.tests.clone()
/// Records a compile-time reference to native fn `name`. Called
/// from the compiler's `host_fn::compile_host_fn_for_*` whenever
/// a wasm import call is emitted for `name`.
pub fn mark_native_referenced(&mut self, name: &str) {
*self.native_coverage.entry(name.to_string()).or_insert(0) += 1;
/// Compile-time reference counts per native fn name. The map only
/// contains names that were emitted at least once; callers that
/// need a complete-vs-missing report should cross-reference
/// against the registered `HostFnSpec` list.
pub fn native_coverage(&self) -> &HashMap<String, u32> {
&self.native_coverage
/// Pre-registers the lisp-side symbol for each host fn so the compiler's
/// resolve_arg + native-dispatch path picks them up. The symbol's value
/// slot carries a `WasmRuntime(result-type)` stand-in so callers that
/// eval the host-fn form during compile-time analysis (CONS element
/// inference, arithmetic dispatch type lookup, etc.) see the right
/// result type without needing to actually run the host fn. The
/// matching wasm import is registered separately by
/// `CompileContext::new_eval_with_host_fns`.
pub fn register_host_fns(&mut self, host_fns: &[crate::host_fn::HostFnSpec]) {
for spec in host_fns {
let stand_in = match spec.result {
Some(ty) => Expr::WasmRuntime(ty),
None => Expr::Nil,
};
self.define(
Symbol::new(spec.nomi_name.clone(), SymbolKind::Native).with_value(stand_in),
);
pub fn lookup(&self, name: &str) -> Option<&Symbol> {
self.symbols.get(name)
/// Looks up a (possibly namespaced) name via its canonical key (ADR-0029).
/// Resolution is strict and lexical: a qualified `(Some(ns), name)` looks
/// up only `NS:NAME`, an unqualified `(None, name)` looks up only `NAME` —
/// there is no cross-namespace fallback and no ambient "current namespace".
/// Most call sites pass the already-canonical `Expr::Symbol` string straight
/// to [`Self::lookup`]; this helper is for callers that hold the split parts.
pub fn lookup_qualified(&self, namespace: Option<&str>, name: &str) -> Option<&Symbol> {
self.lookup(&crate::ast::canonical_symbol(namespace, name))
pub fn lookup_mut(&mut self, name: &str) -> Option<&mut Symbol> {
self.symbols.get_mut(name)
pub fn remove(&mut self, name: &str) -> Option<Symbol> {
self.symbols.remove(name)
pub fn contains(&self, name: &str) -> bool {
self.symbols.contains_key(name)
pub fn iter(&self) -> impl Iterator<Item = (&String, &Symbol)> {
self.symbols.iter()
pub fn define_struct_fields(&mut self, name: impl Into<String>, fields: Vec<String>) {
self.struct_fields.insert(name.into(), fields);
pub fn struct_fields(&self, name: &str) -> Option<&[String]> {
self.struct_fields.get(name).map(Vec::as_slice)