cas_compiler/
sym_table.rs

1use crate::item::{Item, Symbol, SymbolDecl};
2use std::collections::{HashMap, HashSet};
3
4/// An identifier used to distinguish between different scopes where variables are defined.
5///
6/// A new [`ScopeId`] is created for each new scope that is entered. A new scope is created in one
7/// of the following situations:
8///
9/// - A curly brace `{}` block is entered.
10/// - A function definition `f(x) = ...` is entered.
11/// - A `loop`, `while`, `for`, `sum`, or `product` loop is entered.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub struct ScopeId(usize);
14
15/// A scope that holds a set of symbols.
16#[derive(Debug, Clone)]
17pub struct Scope {
18    /// The unique identifier for this scope.
19    id: ScopeId,
20
21    /// The table of symbol declarations.
22    symbols: HashMap<String, Item>,
23
24    /// Set of symbols used in this scope that were declared in a parent scope.
25    ///
26    /// This is important for functions. Functions can still be used long after they are declared;
27    /// however, the variables they use may no longer be in scope. To ensure that the function can
28    /// still access these variables, the symbols they use are captured and stored in this set to
29    /// be used when the function is called.
30    captures: HashSet<Symbol>,
31}
32
33impl Scope {
34    /// Creates a new scope with the given unique identifier.
35    pub fn new(id: ScopeId) -> Self {
36        Self {
37            id,
38            symbols: HashMap::new(),
39            captures: HashSet::new(),
40        }
41    }
42
43    /// Returns the unique identifier for this scope.
44    pub fn id(&self) -> ScopeId {
45        self.id
46    }
47
48    /// Returns the set of symbols captured by this scope.
49    pub fn captures(&self) -> &HashSet<Symbol> {
50        &self.captures
51    }
52
53    /// Inserts a symbol into this scope.
54    pub fn insert(&mut self, name: String, item: Item) {
55        self.symbols.insert(name, item);
56    }
57
58    /// Resolves the item with the given name if it exists in this scope.
59    pub fn resolve_item(&self, name: &str) -> Option<&Item> {
60        self.symbols.get(name)
61    }
62
63    /// Adds a symbol to the set of captures for this scope.
64    pub fn add_capture(&mut self, symbol: Symbol) {
65        self.captures.insert(symbol);
66    }
67}
68
69/// A symbol table that maps identifiers to information about the values they represent.
70///
71/// This is used to store information about variables and functions that are defined in the
72/// program.
73#[derive(Debug, Clone)]
74pub struct SymbolTable {
75    /// The next unique identifier to assign to a symbol.
76    next_id: usize,
77
78    /// The table of symbol declarations.
79    symbols: HashMap<ScopeId, Scope>,
80
81    /// A stack of scopes that are currently active.
82    ///
83    /// When traversing the AST, scopes are pushed onto this stack upon entering a new scope, and
84    /// popped off when leaving the scope. Once popped, the scope is added to the `symbols` table.
85    active_scopes: Vec<Scope>,
86}
87
88impl Default for SymbolTable {
89    fn default() -> Self {
90        Self {
91            next_id: 1,
92            symbols: HashMap::new(),
93            active_scopes: vec![Scope::new(ScopeId(0))], // global scope
94        }
95    }
96}
97
98impl SymbolTable {
99    /// Creates a new symbol table with an initial global scope.
100    pub fn new() -> Self {
101        Self::default()
102    }
103
104    /// Returns true if the symbol table is in the global scope.
105    pub fn is_global_scope(&self) -> bool {
106        self.active_scopes.len() == 1
107    }
108
109    /// Returns the next unique identifier to assign to a symbol.
110    pub fn next_id(&self) -> ScopeId {
111        ScopeId(self.next_id)
112    }
113
114    /// Creates a new scope and makes it the active scope.
115    pub fn enter_scope(&mut self) {
116        let id = ScopeId(self.next_id);
117        self.next_id += 1;
118
119        let scope = Scope::new(id);
120        self.active_scopes.push(scope);
121    }
122
123    /// Exits the current scope and makes the parent scope the active scope.
124    ///
125    /// If the current scope has no symbols declared in it when it is exited, it will not be added
126    /// to the symbol table.
127    pub fn exit_scope(&mut self) {
128        let scope = self.active_scopes.pop().expect("no scope to exit");
129        if !scope.symbols.is_empty() {
130            self.symbols.insert(scope.id(), scope);
131        }
132    }
133
134    /// Exits the current scope, makes the parent scope the active scope, then returns a reference
135    /// to the current scope that was exited. This is useful for finding the scope's captured
136    /// symbols.
137    ///
138    /// The scope is always added to the symbol table, even if it has no symbols declared in it.
139    pub(crate) fn exit_scope_get(&mut self) -> &Scope {
140        let scope = self.active_scopes.pop().expect("no scope to exit");
141        self.symbols.entry(scope.id()).or_insert(scope)
142    }
143
144    /// Returns a reference to the active scope.
145    pub fn active_scope(&self) -> &Scope {
146        self.active_scopes
147            .last()
148            .expect("no active scope")
149    }
150
151    /// Returns a mutable reference to the active scope.
152    pub fn active_scope_mut(&mut self) -> &mut Scope {
153        self.active_scopes
154            .last_mut()
155            .expect("no active scope")
156    }
157
158    /// Inserts a symbol into the current scope.
159    pub fn insert(&mut self, name: String, item: Item) {
160        self.active_scope_mut().insert(name, item);
161    }
162
163    /// Resolves the item with the given name if it exists in the current scope.
164    pub fn resolve_item(&self, name: &str) -> Option<&Item> {
165        self.active_scopes
166            .iter()
167            .rev()
168            .find_map(|scope| scope.resolve_item(name))
169    }
170
171    /// Resolves the item with the given name if it exists in the current scope.
172    ///
173    /// If the item was declared in a parent scope, it is added to the set of captures for the
174    /// current scope.
175    pub fn resolve_item_mark_capture(&mut self, name: &str) -> Option<Symbol> {
176        let (last, rest) = self.active_scopes.split_last_mut()?;
177        if let Some(item) = last.resolve_item(name) {
178            return Some(Symbol::User(item.id()));
179        }
180
181        // go in reverse order to find the nearest parent scope that contains the item
182        rest.iter_mut().rev().find_map(|scope| {
183            scope.resolve_item(name).map(|item| {
184                last.add_capture(Symbol::User(item.id()));
185                Symbol::User(item.id())
186            })
187        })
188    }
189
190    /// Resolves an [`Item::Symbol`] with the given name if it exists in the current scope.
191    pub fn resolve_symbol(&self, name: &str) -> Option<SymbolDecl> {
192        self.resolve_item(name)
193            .and_then(|item| match item {
194                Item::Symbol(decl) => Some(*decl),
195                _ => None,
196            })
197    }
198}