nomiscript/runtime/symbol/table.rs
1use std::collections::HashMap;
2
3use crate::ast::Expr;
4
5use super::entry::{Symbol, SymbolKind};
6
7#[derive(Debug, Clone, Default)]
8pub struct SymbolTable {
9 pub(super) symbols: HashMap<String, Symbol>,
10 pub(super) struct_fields: HashMap<String, Vec<String>>,
11 /// Registered nomiscript tests, in declaration order. Populated
12 /// by `DEFTEST`, drained by `RUN-TESTS`. Each entry is
13 /// `(name, body)` where `body` is the test's wrapped (BEGIN ...)
14 /// form. Lives on the table so each compiler/evaluator instance
15 /// gets its own isolated registry — no global state.
16 pub(super) tests: Vec<(String, Expr)>,
17 /// Per-native-fn compile-time reference counter. Bumped each time
18 /// the compiler emits a `call $<host-fn-idx>` for a registered
19 /// `HostFnSpec`. Read by `(coverage-dump)` to certify the test
20 /// surface reaches every native. Compile-time count (not runtime
21 /// hits) — sufficient for the parity contract "every native has
22 /// at least one test that compiles against it"; runtime hit
23 /// counts via a wasm-global counter array land in a follow-up.
24 pub(super) native_coverage: HashMap<String, u32>,
25 /// Current macro-expansion nesting depth. Bumped on entry to each
26 /// `expand_macro` (which clones the table for the expansion body), so
27 /// the count accumulates down a chain of nested expansions and a
28 /// self-referential macro is caught before it overflows the stack.
29 /// Lives on the table because every expansion site threads
30 /// `&mut SymbolTable`; a top-level form starts at 0.
31 pub(super) macro_depth: usize,
32 /// Names of defuns currently being inlined on the eval path, in entry
33 /// order. The eval-side `call_lambda` walks a defun body inline for
34 /// constant folding; a recursive defun whose recursion doesn't fold to a
35 /// base case (e.g. a runtime-arg call) would otherwise recurse the
36 /// compiler's native stack forever. A name already on this stack signals
37 /// re-entry, so the call site short-circuits to a runtime placeholder
38 /// (handing the real lowering to the codegen monomorph path) rather than
39 /// inlining again. Threaded on the table because the eval path only ever
40 /// carries `&mut SymbolTable`.
41 pub(super) inline_stack: Vec<String>,
42 /// Result `WasmType` of each emitted closure, keyed by `ClosureSigId`'s raw
43 /// id. A closure value carries only `WasmType::Closure(sig)` — the sig's
44 /// param/result types live on the codegen `CompileContext`, invisible to the
45 /// eval surface. The closure-emit sites (which have the context) record the
46 /// result here so the ctx-less eval paths — FOLD's accumulator-type probe,
47 /// MAP/FILTER element typing, binding-local sizing — predict a HOF result
48 /// type that AGREES with what `compile_*_to_stack` emits (which reads the
49 /// real sig). Without it, eval guesses from the seed and drifts from
50 /// codegen, mis-sizing a local (invalid wasm). Carried on the table because
51 /// the eval recursion only ever threads `&mut SymbolTable`.
52 pub(super) closure_results: HashMap<u32, crate::ast::WasmType>,
53}
54
55/// Hard ceiling on nested macro expansion. A self-reproducing macro
56/// (`(defmacro m () '(m))`) would otherwise recurse until the compiler's
57/// native stack overflows; this turns that into a structured compile
58/// error. Each level clones the symbol table and re-enters the full
59/// eval/compile recursion, so the per-level native-stack cost is high —
60/// `64` (matching `MAX_STATIC_LOOP_ITERS`) stays comfortably within a
61/// worker-thread stack while far exceeding any legitimate macro nesting.
62pub const MAX_MACRO_EXPANSION_DEPTH: usize = 64;
63
64/// Hard ceiling on nested eval-time function inlining. The eval path walks a
65/// defun body inline for constant folding; a recursive defun whose recursion
66/// doesn't fold to a base case (a runtime-arg call, or genuinely unbounded
67/// recursion) would otherwise recurse the compiler's native stack until it
68/// overflows. This turns that into a structured compile error.
69///
70/// Set conservatively below the macro ceiling: an inline level is far heavier
71/// on the native stack than a macro-expansion level (it clones the table,
72/// binds params, and re-enters the full `eval_value` recursion through
73/// `if`/arithmetic/etc.), and compilation is not guaranteed to run on a large
74/// stack — empirically a default ~2 MiB thread overflows around ~64 inline
75/// levels. `32` leaves roughly a 2× native-stack margin while still allowing
76/// far deeper const-folded recursion than any realistic accounting script
77/// uses (the deepest legitimate cases are a handful of levels).
78pub const MAX_INLINE_DEPTH: usize = 32;
79
80impl SymbolTable {
81 #[must_use]
82 pub fn new() -> Self {
83 Self::default()
84 }
85
86 /// Enter one macro-expansion level, erroring once the depth ceiling is
87 /// crossed. Bumped before a macro's expansion is re-evaluated (where a
88 /// self-referential macro would otherwise recurse forever) and undone
89 /// by [`Self::exit_macro_expansion`] once that subtree finishes, so
90 /// sibling expansions don't accumulate. Turns a non-terminating macro
91 /// into a structured compile error instead of a native stack overflow.
92 pub(crate) fn enter_macro_expansion(&mut self) -> crate::error::Result<()> {
93 if self.macro_depth >= MAX_MACRO_EXPANSION_DEPTH {
94 return Err(crate::error::Error::Compile(format!(
95 "macro expansion exceeded depth {MAX_MACRO_EXPANSION_DEPTH} \
96 (recursive or non-terminating macro?)"
97 )));
98 }
99 self.macro_depth += 1;
100 Ok(())
101 }
102
103 pub(crate) fn exit_macro_expansion(&mut self) {
104 self.macro_depth = self.macro_depth.saturating_sub(1);
105 }
106
107 /// Whether `name` is already being inlined further up the eval-time call
108 /// chain — i.e. inlining it again would be (mutual) recursion.
109 #[must_use]
110 pub(crate) fn is_inlining(&self, name: &str) -> bool {
111 self.inline_stack.iter().any(|n| n == name)
112 }
113
114 /// Mark `name` as entering an eval-time inline walk. Paired with
115 /// [`Self::exit_inline`]; the depth ceiling turns a deeply-nested
116 /// (or unbounded) inline recursion into a structured compile error
117 /// instead of a native stack overflow — the eval-path analogue of
118 /// [`Self::enter_macro_expansion`].
119 pub(crate) fn enter_inline(&mut self, name: &str) -> crate::error::Result<()> {
120 if self.inline_stack.len() >= MAX_INLINE_DEPTH {
121 return Err(crate::error::Error::Compile(format!(
122 "function inlining exceeded depth {MAX_INLINE_DEPTH} \
123 (recursive or non-terminating call to '{name}'?)"
124 )));
125 }
126 self.inline_stack.push(name.to_string());
127 Ok(())
128 }
129
130 pub(crate) fn exit_inline(&mut self) {
131 self.inline_stack.pop();
132 }
133
134 /// Records the result `WasmType` for a closure signature id (see
135 /// [`Self::closure_results`]). Called by the closure-emit sites, which hold
136 /// the `CompileContext` and so know the sig's true result type.
137 pub fn record_closure_result(
138 &mut self,
139 sig: crate::ast::ClosureSigId,
140 result: crate::ast::WasmType,
141 ) {
142 self.closure_results.insert(sig.0, result);
143 }
144
145 /// The recorded result `WasmType` for a closure signature id, or `None` if
146 /// the closure wasn't emitted through a recording site (e.g. a closure
147 /// reaching the eval surface from a path that doesn't record).
148 pub fn closure_result(&self, sig: crate::ast::ClosureSigId) -> Option<crate::ast::WasmType> {
149 self.closure_results.get(&sig.0).copied()
150 }
151
152 #[must_use]
153 pub fn with_builtins() -> Self {
154 let mut table = Self::new();
155 table.register_builtins();
156 table.load_standard_library();
157 // Re-register entity accessors after stdlib to override DEFSTRUCT-generated ones
158 table.register_entity_accessors();
159 // Prelude loads last so its DEFUNs resolve the builtins + accessors above.
160 table.load_prelude();
161 table
162 }
163
164 #[must_use]
165 pub fn with_builtins_for_wasm() -> Self {
166 let mut table = Self::new();
167 table.register_builtins();
168 table.load_standard_library();
169 table.register_entity_accessors();
170 table.load_prelude();
171 table
172 }
173
174 pub fn define(&mut self, symbol: Symbol) {
175 self.symbols.insert(symbol.name.clone(), symbol);
176 }
177
178 /// Register a named test (`DEFTEST`); `RUN-TESTS` reads it via [`tests`].
179 /// Re-registering an existing name overwrites its body in place (matching
180 /// [`define`]'s overwrite semantics) rather than appending, so the
181 /// two-surface compile — which may evaluate a `DEFTEST` on both the eval
182 /// and codegen passes — stays idempotent and never double-runs a test.
183 pub fn register_test(&mut self, name: impl Into<String>, body: Expr) {
184 let name = name.into();
185 match self
186 .tests
187 .iter_mut()
188 .find(|(existing, _)| *existing == name)
189 {
190 Some(entry) => entry.1 = body,
191 None => self.tests.push((name, body)),
192 }
193 }
194
195 /// Snapshot of registered tests in declaration order. Cloned so
196 /// `RUN-TESTS` can iterate without holding a borrow on the table
197 /// it needs to evaluate against.
198 #[must_use]
199 pub fn tests(&self) -> Vec<(String, Expr)> {
200 self.tests.clone()
201 }
202
203 /// Records a compile-time reference to native fn `name`. Called
204 /// from the compiler's `host_fn::compile_host_fn_for_*` whenever
205 /// a wasm import call is emitted for `name`.
206 pub fn mark_native_referenced(&mut self, name: &str) {
207 *self.native_coverage.entry(name.to_string()).or_insert(0) += 1;
208 }
209
210 /// Compile-time reference counts per native fn name. The map only
211 /// contains names that were emitted at least once; callers that
212 /// need a complete-vs-missing report should cross-reference
213 /// against the registered `HostFnSpec` list.
214 #[must_use]
215 pub fn native_coverage(&self) -> &HashMap<String, u32> {
216 &self.native_coverage
217 }
218
219 /// Pre-registers the lisp-side symbol for each host fn so the compiler's
220 /// resolve_arg + native-dispatch path picks them up. The symbol's value
221 /// slot carries a `WasmRuntime(result-type)` stand-in so callers that
222 /// eval the host-fn form during compile-time analysis (CONS element
223 /// inference, arithmetic dispatch type lookup, etc.) see the right
224 /// result type without needing to actually run the host fn. The
225 /// matching wasm import is registered separately by
226 /// `CompileContext::new_eval_with_host_fns`.
227 pub fn register_host_fns(&mut self, host_fns: &[crate::host_fn::HostFnSpec]) {
228 for spec in host_fns {
229 let stand_in = match spec.result {
230 Some(ty) => Expr::WasmRuntime(ty),
231 None => Expr::Nil,
232 };
233 self.define(
234 Symbol::new(spec.nomi_name.clone(), SymbolKind::Native).with_value(stand_in),
235 );
236 }
237 }
238
239 #[must_use]
240 pub fn lookup(&self, name: &str) -> Option<&Symbol> {
241 self.symbols.get(name)
242 }
243
244 /// Looks up a (possibly namespaced) name via its canonical key (ADR-0029).
245 /// Resolution is strict and lexical: a qualified `(Some(ns), name)` looks
246 /// up only `NS:NAME`, an unqualified `(None, name)` looks up only `NAME` —
247 /// there is no cross-namespace fallback and no ambient "current namespace".
248 /// Most call sites pass the already-canonical `Expr::Symbol` string straight
249 /// to [`Self::lookup`]; this helper is for callers that hold the split parts.
250 #[must_use]
251 pub fn lookup_qualified(&self, namespace: Option<&str>, name: &str) -> Option<&Symbol> {
252 self.lookup(&crate::ast::canonical_symbol(namespace, name))
253 }
254
255 pub fn lookup_mut(&mut self, name: &str) -> Option<&mut Symbol> {
256 self.symbols.get_mut(name)
257 }
258
259 pub fn remove(&mut self, name: &str) -> Option<Symbol> {
260 self.symbols.remove(name)
261 }
262
263 #[must_use]
264 pub fn contains(&self, name: &str) -> bool {
265 self.symbols.contains_key(name)
266 }
267
268 pub fn iter(&self) -> impl Iterator<Item = (&String, &Symbol)> {
269 self.symbols.iter()
270 }
271
272 pub fn define_struct_fields(&mut self, name: impl Into<String>, fields: Vec<String>) {
273 self.struct_fields.insert(name.into(), fields);
274 }
275
276 #[must_use]
277 pub fn struct_fields(&self, name: &str) -> Option<&[String]> {
278 self.struct_fields.get(name).map(Vec::as_slice)
279 }
280}