Skip to main content

symbol_lang/
table.rs

1//! The lexically-scoped symbol table.
2
3use alloc::collections::BTreeMap;
4use alloc::vec::Vec;
5
6use intern_lang::Symbol;
7
8/// A lexically-scoped map from interned names to bindings.
9///
10/// `SymbolTable<T>` is the scope chain a name resolver threads: a stack of scopes,
11/// each mapping a [`Symbol`] to a binding of the language's choosing — a node
12/// handle, a declaration record, a type, whatever the language resolves a name
13/// *to*. The table is generic over that binding `T` and keys every scope on the
14/// four-byte `Symbol`, so a lookup compares integers, not strings.
15///
16/// A resolver [`enter_scope`](SymbolTable::enter_scope) on the way into a block,
17/// [`define`](SymbolTable::define)s names in it, [`lookup`](SymbolTable::lookup)s
18/// names outward through the enclosing scopes, and
19/// [`exit_scope`](SymbolTable::exit_scope) on the way out — or wraps the whole
20/// block in [`scoped`](SymbolTable::scoped). A name resolves to the nearest
21/// enclosing binding, so an inner definition shadows an outer one while its scope
22/// is open.
23///
24/// There is always a root scope, so the table is usable the moment it is built and
25/// the root can never be exited.
26///
27/// # Examples
28///
29/// ```
30/// use symbol_lang::SymbolTable;
31/// use intern_lang::Interner;
32///
33/// let mut names = Interner::new();
34/// let x = names.intern("x");
35///
36/// let mut table: SymbolTable<i32> = SymbolTable::new();
37/// table.define(x, 1); // outer `x` = 1
38/// assert_eq!(table.lookup(x), Some(&1));
39///
40/// table.enter_scope();
41/// table.define(x, 2); // inner `x` shadows the outer one
42/// assert_eq!(table.lookup(x), Some(&2));
43/// table.exit_scope();
44///
45/// assert_eq!(table.lookup(x), Some(&1)); // outer `x` is visible again
46/// ```
47pub struct SymbolTable<T> {
48    /// The scope chain, outermost first. Never empty: `scopes[0]` is the root, and
49    /// [`exit_scope`](SymbolTable::exit_scope) refuses to pop it.
50    scopes: Vec<BTreeMap<Symbol, T>>,
51}
52
53impl<T> SymbolTable<T> {
54    /// Creates a table with a single, empty root scope.
55    ///
56    /// # Examples
57    ///
58    /// ```
59    /// use symbol_lang::SymbolTable;
60    ///
61    /// let table: SymbolTable<()> = SymbolTable::new();
62    /// assert_eq!(table.depth(), 1);
63    /// ```
64    #[must_use]
65    pub fn new() -> Self {
66        Self {
67            scopes: alloc::vec![BTreeMap::new()],
68        }
69    }
70
71    /// A mutable reference to the current (innermost) scope. The chain is never
72    /// empty, so the last element is always present.
73    fn current_mut(&mut self) -> &mut BTreeMap<Symbol, T> {
74        let last = self.scopes.len() - 1; // `len >= 1` by the type invariant
75        &mut self.scopes[last]
76    }
77
78    /// Pushes a new, empty innermost scope.
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// use symbol_lang::SymbolTable;
84    ///
85    /// let mut table: SymbolTable<()> = SymbolTable::new();
86    /// table.enter_scope();
87    /// assert_eq!(table.depth(), 2);
88    /// ```
89    pub fn enter_scope(&mut self) {
90        self.scopes.push(BTreeMap::new());
91    }
92
93    /// Pops the innermost scope, discarding its bindings, and returns `true`.
94    ///
95    /// Exiting the root scope is a no-op that returns `false`, so the chain always
96    /// keeps at least one scope.
97    ///
98    /// # Examples
99    ///
100    /// ```
101    /// use symbol_lang::SymbolTable;
102    ///
103    /// let mut table: SymbolTable<()> = SymbolTable::new();
104    /// table.enter_scope();
105    /// assert!(table.exit_scope()); // popped the inner scope
106    /// assert!(!table.exit_scope()); // root cannot be exited
107    /// assert_eq!(table.depth(), 1);
108    /// ```
109    pub fn exit_scope(&mut self) -> bool {
110        if self.scopes.len() > 1 {
111            let _ = self.scopes.pop();
112            true
113        } else {
114            false
115        }
116    }
117
118    /// Runs `f` inside a freshly entered scope, exiting it afterward, and returns
119    /// what `f` returns.
120    ///
121    /// This is the balanced way to scope a block: exactly one scope is entered and
122    /// exited, so the chain depth is the same before and after no matter what `f`
123    /// does.
124    ///
125    /// # Examples
126    ///
127    /// ```
128    /// use symbol_lang::SymbolTable;
129    /// use intern_lang::Interner;
130    ///
131    /// let mut names = Interner::new();
132    /// let y = names.intern("y");
133    /// let mut table: SymbolTable<i32> = SymbolTable::new();
134    ///
135    /// let seen = table.scoped(|table| {
136    ///     table.define(y, 9);
137    ///     table.lookup(y).copied()
138    /// });
139    /// assert_eq!(seen, Some(9));
140    /// assert_eq!(table.lookup(y), None); // the inner `y` is gone
141    /// assert_eq!(table.depth(), 1);
142    /// ```
143    pub fn scoped<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
144        self.enter_scope();
145        let result = f(self);
146        let _ = self.exit_scope();
147        result
148    }
149
150    /// Binds `name` to `value` in the current scope, returning any binding the same
151    /// name already had *in that scope* (which it replaces).
152    ///
153    /// A `Some` return means the name was already defined in the current scope — a
154    /// duplicate definition the caller can report. It never touches an enclosing
155    /// scope, so it cannot disturb a shadowed outer binding.
156    ///
157    /// # Examples
158    ///
159    /// ```
160    /// use symbol_lang::SymbolTable;
161    /// use intern_lang::Interner;
162    ///
163    /// let mut names = Interner::new();
164    /// let n = names.intern("n");
165    /// let mut table: SymbolTable<i32> = SymbolTable::new();
166    ///
167    /// assert_eq!(table.define(n, 1), None); // fresh
168    /// assert_eq!(table.define(n, 2), Some(1)); // redefined: the old binding comes back
169    /// ```
170    pub fn define(&mut self, name: Symbol, value: T) -> Option<T> {
171        self.current_mut().insert(name, value)
172    }
173
174    /// Looks `name` up through the scope chain, returning the binding from the
175    /// nearest scope that defines it, or `None` if no scope does.
176    ///
177    /// # Examples
178    ///
179    /// ```
180    /// use symbol_lang::SymbolTable;
181    /// use intern_lang::Interner;
182    ///
183    /// let mut names = Interner::new();
184    /// let g = names.intern("g");
185    /// let mut table: SymbolTable<&str> = SymbolTable::new();
186    /// table.define(g, "global");
187    /// table.enter_scope();
188    /// assert_eq!(table.lookup(g), Some(&"global")); // found in the outer scope
189    /// ```
190    #[must_use]
191    pub fn lookup(&self, name: Symbol) -> Option<&T> {
192        self.scopes.iter().rev().find_map(|scope| scope.get(&name))
193    }
194
195    /// Looks `name` up in the current scope only, ignoring enclosing scopes.
196    ///
197    /// Useful for a duplicate-in-this-scope check before defining, when shadowing an
198    /// outer binding is allowed but redefining in the same scope is not.
199    ///
200    /// # Examples
201    ///
202    /// ```
203    /// use symbol_lang::SymbolTable;
204    /// use intern_lang::Interner;
205    ///
206    /// let mut names = Interner::new();
207    /// let v = names.intern("v");
208    /// let mut table: SymbolTable<i32> = SymbolTable::new();
209    /// table.define(v, 1);
210    /// table.enter_scope();
211    /// assert_eq!(table.lookup_local(v), None); // not in *this* scope
212    /// assert_eq!(table.lookup(v), Some(&1)); // but visible through the chain
213    /// ```
214    #[must_use]
215    pub fn lookup_local(&self, name: Symbol) -> Option<&T> {
216        self.scopes.last().and_then(|scope| scope.get(&name))
217    }
218
219    /// Returns `true` if `name` is bound anywhere in the scope chain.
220    ///
221    /// # Examples
222    ///
223    /// ```
224    /// use symbol_lang::SymbolTable;
225    /// use intern_lang::Interner;
226    ///
227    /// let mut names = Interner::new();
228    /// let a = names.intern("a");
229    /// let mut table: SymbolTable<()> = SymbolTable::new();
230    /// assert!(!table.is_defined(a));
231    /// table.define(a, ());
232    /// assert!(table.is_defined(a));
233    /// ```
234    #[must_use]
235    pub fn is_defined(&self, name: Symbol) -> bool {
236        self.lookup(name).is_some()
237    }
238
239    /// Returns the number of active scopes, including the root — always at least 1.
240    ///
241    /// # Examples
242    ///
243    /// ```
244    /// use symbol_lang::SymbolTable;
245    ///
246    /// let mut table: SymbolTable<()> = SymbolTable::new();
247    /// assert_eq!(table.depth(), 1);
248    /// table.enter_scope();
249    /// assert_eq!(table.depth(), 2);
250    /// ```
251    #[must_use]
252    pub fn depth(&self) -> usize {
253        self.scopes.len()
254    }
255}
256
257impl<T> Default for SymbolTable<T> {
258    #[inline]
259    fn default() -> Self {
260        Self::new()
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use intern_lang::Interner;
267
268    use super::*;
269
270    /// Interns three distinct names for tests.
271    fn names() -> (Interner, Symbol, Symbol, Symbol) {
272        let mut interner = Interner::new();
273        let a = interner.intern("a");
274        let b = interner.intern("b");
275        let c = interner.intern("c");
276        (interner, a, b, c)
277    }
278
279    #[test]
280    fn test_new_has_one_root_scope() {
281        let table: SymbolTable<()> = SymbolTable::new();
282        assert_eq!(table.depth(), 1);
283    }
284
285    #[test]
286    fn test_define_then_lookup() {
287        let (_i, a, _b, _c) = names();
288        let mut table: SymbolTable<i32> = SymbolTable::new();
289        assert_eq!(table.define(a, 7), None);
290        assert_eq!(table.lookup(a), Some(&7));
291    }
292
293    #[test]
294    fn test_redefine_returns_previous() {
295        let (_i, a, _b, _c) = names();
296        let mut table: SymbolTable<i32> = SymbolTable::new();
297        table.define(a, 1);
298        assert_eq!(table.define(a, 2), Some(1));
299        assert_eq!(table.lookup(a), Some(&2));
300    }
301
302    #[test]
303    fn test_inner_scope_shadows_then_restores() {
304        let (_i, a, _b, _c) = names();
305        let mut table: SymbolTable<i32> = SymbolTable::new();
306        table.define(a, 1);
307        table.enter_scope();
308        table.define(a, 2);
309        assert_eq!(table.lookup(a), Some(&2));
310        assert_eq!(table.lookup_local(a), Some(&2));
311        table.exit_scope();
312        assert_eq!(table.lookup(a), Some(&1));
313    }
314
315    #[test]
316    fn test_lookup_local_ignores_outer() {
317        let (_i, a, _b, _c) = names();
318        let mut table: SymbolTable<i32> = SymbolTable::new();
319        table.define(a, 1);
320        table.enter_scope();
321        assert_eq!(table.lookup_local(a), None);
322        assert_eq!(table.lookup(a), Some(&1));
323    }
324
325    #[test]
326    fn test_exit_root_is_a_noop() {
327        let mut table: SymbolTable<()> = SymbolTable::new();
328        assert!(!table.exit_scope());
329        assert_eq!(table.depth(), 1);
330    }
331
332    #[test]
333    fn test_define_in_inner_does_not_touch_outer() {
334        let (_i, a, _b, _c) = names();
335        let mut table: SymbolTable<i32> = SymbolTable::new();
336        table.define(a, 1);
337        table.scoped(|table| {
338            table.define(a, 99);
339        });
340        // The inner definition vanished with its scope; the outer stands.
341        assert_eq!(table.lookup(a), Some(&1));
342    }
343
344    #[test]
345    fn test_unbound_lookup_is_none() {
346        let (_i, a, b, _c) = names();
347        let mut table: SymbolTable<i32> = SymbolTable::new();
348        table.define(a, 1);
349        assert_eq!(table.lookup(b), None);
350        assert!(!table.is_defined(b));
351    }
352}