1
use std::collections::HashMap;
2

            
3
use crate::ast::Expr;
4

            
5
use super::entry::{Symbol, SymbolKind};
6

            
7
#[derive(Debug, Clone, Default)]
8
pub 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
    /// First parameter `WasmType` of each emitted closure, recorded alongside
54
    /// [`Self::closure_results`] so FILTER's eval mirror coerces a constant
55
    /// list's members to the predicate's parameter exactly as codegen does.
56
    pub(super) closure_params: HashMap<u32, crate::ast::WasmType>,
57
}
58

            
59
/// Hard ceiling on nested macro expansion. A self-reproducing macro
60
/// (`(defmacro m () '(m))`) would otherwise recurse until the compiler's
61
/// native stack overflows; this turns that into a structured compile
62
/// error. Each level clones the symbol table and re-enters the full
63
/// eval/compile recursion, so the per-level native-stack cost is high —
64
/// `64` (matching `MAX_STATIC_LOOP_ITERS`) stays comfortably within a
65
/// worker-thread stack while far exceeding any legitimate macro nesting.
66
pub const MAX_MACRO_EXPANSION_DEPTH: usize = 64;
67

            
68
/// Hard ceiling on nested eval-time function inlining. The eval path walks a
69
/// defun body inline for constant folding; a recursive defun whose recursion
70
/// doesn't fold to a base case (a runtime-arg call, or genuinely unbounded
71
/// recursion) would otherwise recurse the compiler's native stack until it
72
/// overflows. This turns that into a structured compile error.
73
///
74
/// Set conservatively below the macro ceiling: an inline level is far heavier
75
/// on the native stack than a macro-expansion level (it clones the table,
76
/// binds params, and re-enters the full `eval_value` recursion through
77
/// `if`/arithmetic/etc.), and compilation is not guaranteed to run on a large
78
/// stack — empirically a default ~2 MiB thread overflows around ~64 inline
79
/// levels. `32` leaves roughly a 2× native-stack margin while still allowing
80
/// far deeper const-folded recursion than any realistic accounting script
81
/// uses (the deepest legitimate cases are a handful of levels).
82
pub const MAX_INLINE_DEPTH: usize = 32;
83

            
84
impl SymbolTable {
85
    #[must_use]
86
119545
    pub fn new() -> Self {
87
119545
        Self::default()
88
119545
    }
89

            
90
    /// Enter one macro-expansion level, erroring once the depth ceiling is
91
    /// crossed. Bumped before a macro's expansion is re-evaluated (where a
92
    /// self-referential macro would otherwise recurse forever) and undone
93
    /// by [`Self::exit_macro_expansion`] once that subtree finishes, so
94
    /// sibling expansions don't accumulate. Turns a non-terminating macro
95
    /// into a structured compile error instead of a native stack overflow.
96
32121
    pub(crate) fn enter_macro_expansion(&mut self) -> crate::error::Result<()> {
97
32121
        if self.macro_depth >= MAX_MACRO_EXPANSION_DEPTH {
98
166
            return Err(crate::error::Error::Compile(format!(
99
166
                "macro expansion exceeded depth {MAX_MACRO_EXPANSION_DEPTH} \
100
166
                 (recursive or non-terminating macro?)"
101
166
            )));
102
31955
        }
103
31955
        self.macro_depth += 1;
104
31955
        Ok(())
105
32121
    }
106

            
107
31955
    pub(crate) fn exit_macro_expansion(&mut self) {
108
31955
        self.macro_depth = self.macro_depth.saturating_sub(1);
109
31955
    }
110

            
111
    /// Whether `name` is already being inlined further up the eval-time call
112
    /// chain — i.e. inlining it again would be (mutual) recursion.
113
    #[must_use]
114
22659
    pub(crate) fn is_inlining(&self, name: &str) -> bool {
115
22659
        self.inline_stack.iter().any(|n| n == name)
116
22659
    }
117

            
118
    /// Mark `name` as entering an eval-time inline walk. Paired with
119
    /// [`Self::exit_inline`]; the depth ceiling turns a deeply-nested
120
    /// (or unbounded) inline recursion into a structured compile error
121
    /// instead of a native stack overflow — the eval-path analogue of
122
    /// [`Self::enter_macro_expansion`].
123
20501
    pub(crate) fn enter_inline(&mut self, name: &str) -> crate::error::Result<()> {
124
20501
        if self.inline_stack.len() >= MAX_INLINE_DEPTH {
125
            return Err(crate::error::Error::Compile(format!(
126
                "function inlining exceeded depth {MAX_INLINE_DEPTH} \
127
                 (recursive or non-terminating call to '{name}'?)"
128
            )));
129
20501
        }
130
20501
        self.inline_stack.push(name.to_string());
131
20501
        Ok(())
132
20501
    }
133

            
134
20501
    pub(crate) fn exit_inline(&mut self) {
135
20501
        self.inline_stack.pop();
136
20501
    }
137

            
138
    /// Records the result `WasmType` for a closure signature id (see
139
    /// [`Self::closure_results`]). Called by the closure-emit sites, which hold
140
    /// the `CompileContext` and so know the sig's true result type.
141
3569
    pub fn record_closure_result(
142
3569
        &mut self,
143
3569
        sig: crate::ast::ClosureSigId,
144
3569
        result: crate::ast::WasmType,
145
3569
    ) {
146
3569
        self.closure_results.insert(sig.0, result);
147
3569
    }
148

            
149
    /// The recorded result `WasmType` for a closure signature id, or `None` if
150
    /// the closure wasn't emitted through a recording site (e.g. a closure
151
    /// reaching the eval surface from a path that doesn't record).
152
1909
    pub fn closure_result(&self, sig: crate::ast::ClosureSigId) -> Option<crate::ast::WasmType> {
153
1909
        self.closure_results.get(&sig.0).copied()
154
1909
    }
155

            
156
    /// Records the first parameter `WasmType` for a closure signature id (see
157
    /// [`Self::closure_params`]).
158
3569
    pub fn record_closure_param(
159
3569
        &mut self,
160
3569
        sig: crate::ast::ClosureSigId,
161
3569
        param: crate::ast::WasmType,
162
3569
    ) {
163
3569
        self.closure_params.insert(sig.0, param);
164
3569
    }
165

            
166
    /// The recorded first parameter `WasmType` for a closure signature id, or
167
    /// `None` when the closure takes no parameters or wasn't recorded.
168
332
    pub fn closure_param(&self, sig: crate::ast::ClosureSigId) -> Option<crate::ast::WasmType> {
169
332
        self.closure_params.get(&sig.0).copied()
170
332
    }
171

            
172
    #[must_use]
173
70335
    pub fn with_builtins() -> Self {
174
70335
        let mut table = Self::new();
175
70335
        table.register_builtins();
176
70335
        table.load_standard_library();
177
        // Re-register entity accessors after stdlib to override DEFSTRUCT-generated ones
178
70335
        table.register_entity_accessors();
179
        // Prelude loads last so its DEFUNs resolve the builtins + accessors above.
180
70335
        table.load_prelude();
181
70335
        table
182
70335
    }
183

            
184
    #[must_use]
185
44434
    pub fn with_builtins_for_wasm() -> Self {
186
44434
        let mut table = Self::new();
187
44434
        table.register_builtins();
188
44434
        table.load_standard_library();
189
44434
        table.register_entity_accessors();
190
44434
        table.load_prelude();
191
44434
        table
192
44434
    }
193

            
194
33412336
    pub fn define(&mut self, symbol: Symbol) {
195
33412336
        self.symbols.insert(symbol.name.clone(), symbol);
196
33412336
    }
197

            
198
    /// Register a named test (`DEFTEST`); `RUN-TESTS` reads it via [`tests`].
199
    /// Re-registering an existing name overwrites its body in place (matching
200
    /// [`define`]'s overwrite semantics) rather than appending, so the
201
    /// two-surface compile — which may evaluate a `DEFTEST` on both the eval
202
    /// and codegen passes — stays idempotent and never double-runs a test.
203
3658
    pub fn register_test(&mut self, name: impl Into<String>, body: Expr) {
204
3658
        let name = name.into();
205
3658
        match self
206
3658
            .tests
207
3658
            .iter_mut()
208
16191
            .find(|(existing, _)| *existing == name)
209
        {
210
1
            Some(entry) => entry.1 = body,
211
3657
            None => self.tests.push((name, body)),
212
        }
213
3658
    }
214

            
215
    /// Snapshot of registered tests in declaration order. Cloned so
216
    /// `RUN-TESTS` can iterate without holding a borrow on the table
217
    /// it needs to evaluate against.
218
    #[must_use]
219
835
    pub fn tests(&self) -> Vec<(String, Expr)> {
220
835
        self.tests.clone()
221
835
    }
222

            
223
    /// Records a compile-time reference to native fn `name`. Called
224
    /// from the compiler's `host_fn::compile_host_fn_for_*` whenever
225
    /// a wasm import call is emitted for `name`.
226
366695
    pub fn mark_native_referenced(&mut self, name: &str) {
227
366695
        *self.native_coverage.entry(name.to_string()).or_insert(0) += 1;
228
366695
    }
229

            
230
    /// Compile-time reference counts per native fn name. The map only
231
    /// contains names that were emitted at least once; callers that
232
    /// need a complete-vs-missing report should cross-reference
233
    /// against the registered `HostFnSpec` list.
234
    #[must_use]
235
249
    pub fn native_coverage(&self) -> &HashMap<String, u32> {
236
249
        &self.native_coverage
237
249
    }
238

            
239
    /// Pre-registers the lisp-side symbol for each host fn so the compiler's
240
    /// resolve_arg + native-dispatch path picks them up. The symbol's value
241
    /// slot carries a `WasmRuntime(result-type)` stand-in so callers that
242
    /// eval the host-fn form during compile-time analysis (CONS element
243
    /// inference, arithmetic dispatch type lookup, etc.) see the right
244
    /// result type without needing to actually run the host fn. The
245
    /// matching wasm import is registered separately by
246
    /// `CompileContext::new_eval_with_host_fns`.
247
33533
    pub fn register_host_fns(&mut self, host_fns: &[crate::host_fn::HostFnSpec]) {
248
1488523
        for spec in host_fns {
249
1488523
            let stand_in = match spec.result {
250
1488274
                Some(ty) => Expr::WasmRuntime(ty),
251
249
                None => Expr::Nil,
252
            };
253
1488523
            self.define(
254
1488523
                Symbol::new(spec.nomi_name.clone(), SymbolKind::Native).with_value(stand_in),
255
            );
256
        }
257
33533
    }
258

            
259
    #[must_use]
260
2411165
    pub fn lookup(&self, name: &str) -> Option<&Symbol> {
261
2411165
        self.symbols.get(name)
262
2411165
    }
263

            
264
    /// Looks up a (possibly namespaced) name via its canonical key (ADR-0029).
265
    /// Resolution is strict and lexical: a qualified `(Some(ns), name)` looks
266
    /// up only `NS:NAME`, an unqualified `(None, name)` looks up only `NAME` —
267
    /// there is no cross-namespace fallback and no ambient "current namespace".
268
    /// Most call sites pass the already-canonical `Expr::Symbol` string straight
269
    /// to [`Self::lookup`]; this helper is for callers that hold the split parts.
270
    #[must_use]
271
3
    pub fn lookup_qualified(&self, namespace: Option<&str>, name: &str) -> Option<&Symbol> {
272
3
        self.lookup(&crate::ast::canonical_symbol(namespace, name))
273
3
    }
274

            
275
576606
    pub fn lookup_mut(&mut self, name: &str) -> Option<&mut Symbol> {
276
576606
        self.symbols.get_mut(name)
277
576606
    }
278

            
279
4316
    pub fn remove(&mut self, name: &str) -> Option<Symbol> {
280
4316
        self.symbols.remove(name)
281
4316
    }
282

            
283
    #[must_use]
284
1538
    pub fn contains(&self, name: &str) -> bool {
285
1538
        self.symbols.contains_key(name)
286
1538
    }
287

            
288
834
    pub fn iter(&self) -> impl Iterator<Item = (&String, &Symbol)> {
289
834
        self.symbols.iter()
290
834
    }
291

            
292
578908
    pub fn define_struct_fields(&mut self, name: impl Into<String>, fields: Vec<String>) {
293
578908
        self.struct_fields.insert(name.into(), fields);
294
578908
    }
295

            
296
    #[must_use]
297
249
    pub fn struct_fields(&self, name: &str) -> Option<&[String]> {
298
249
        self.struct_fields.get(name).map(Vec::as_slice)
299
249
    }
300
}