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}