Skip to main content

leo_passes/common/symbol_table/
mod.rs

1// Copyright (C) 2019-2026 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17use leo_ast::{Composite, Expression, Function, Interface, Location, NodeBuilder, NodeID, Path, Type};
18use leo_errors::{AstError, Color, Label, LeoError, Result};
19use leo_span::{Span, Symbol};
20
21use indexmap::{IndexMap, IndexSet};
22use itertools::Itertools;
23use std::{cell::RefCell, collections::HashMap, rc::Rc};
24
25mod symbols;
26pub use symbols::*;
27
28/// Maps global and local symbols to information about them.
29///
30/// Scopes are indexed by the NodeID of the function, block, or iteration.
31#[derive(Debug, Default)]
32pub struct SymbolTable {
33    /// Maps a program to the list of programs it imports. This also includes implicit imports, i.e. libraries.
34    imports: IndexMap<Symbol, IndexSet<Symbol>>,
35
36    /// Names of all the external libraries
37    libraries: IndexSet<Symbol>,
38
39    /// Functions indexed by location.
40    functions: IndexMap<Location, FunctionSymbol>,
41
42    /// Records indexed by location.
43    records: IndexMap<Location, Composite>,
44
45    /// Structs indexed by a path.
46    structs: IndexMap<Location, Composite>,
47
48    /// Interfaces indexed by a path.
49    interfaces: IndexMap<Location, Interface>,
50
51    /// Consts that have been successfully evaluated.
52    global_consts: IndexMap<Location, Expression>,
53
54    /// Global variables indexed by location.
55    globals: IndexMap<Location, VariableSymbol>,
56
57    /// Local tables index by the NodeID of the function, iteration, or block they're contained in.
58    all_locals: HashMap<NodeID, LocalTable>,
59
60    /// The current LocalTable we're looking at.
61    local: Option<LocalTable>,
62}
63
64#[derive(Clone, Default, Debug)]
65struct LocalTable {
66    inner: Rc<RefCell<LocalTableInner>>,
67}
68
69#[derive(Clone, Default, Debug)]
70struct LocalTableInner {
71    /// The `NodeID` of the function, iteration, or block this table indexes.
72    id: NodeID,
73
74    /// The parent `NodeID` of this scope, if it exists.
75    parent: Option<NodeID>,
76
77    /// The children of `NodeID` of this scope
78    children: Vec<NodeID>,
79
80    /// The consts we've evaluated in this scope.
81    consts: HashMap<Symbol, Expression>,
82
83    /// Variables in this scope, indexed by name.
84    variables: HashMap<Symbol, VariableSymbol>,
85}
86
87impl LocalTable {
88    fn new(symbol_table: &mut SymbolTable, id: NodeID, parent: Option<NodeID>) -> Self {
89        // If parent exists, register this scope as its child
90        if let Some(parent_id) = parent
91            && let Some(parent_table) = symbol_table.all_locals.get_mut(&parent_id)
92        {
93            parent_table.inner.borrow_mut().children.push(id);
94        }
95
96        LocalTable {
97            inner: Rc::new(RefCell::new(LocalTableInner {
98                id,
99                parent,
100                consts: HashMap::new(),
101                variables: HashMap::new(),
102                children: vec![], // Must still initialize our own children list
103            })),
104        }
105    }
106
107    fn clear_but_consts(&mut self) {
108        self.inner.borrow_mut().variables.clear();
109    }
110
111    /// Recursively duplicates this table and all children.
112    /// `new_parent` is the NodeID of the parent in the new tree (None for root).
113    pub fn dup(
114        &self,
115        node_builder: &NodeBuilder,
116        all_locals: &mut HashMap<NodeID, LocalTable>,
117        new_parent: Option<NodeID>,
118    ) -> LocalTable {
119        let inner = self.inner.borrow();
120
121        // Generate a new ID for this table
122        let new_id = node_builder.next_id();
123
124        // Recursively duplicate children with new_id as their parent
125        let new_children: Vec<NodeID> = inner
126            .children
127            .iter()
128            .map(|child_id| {
129                let child_table = all_locals.get(child_id).expect("Child must exist").clone();
130                let duped_child = child_table.dup(node_builder, all_locals, Some(new_id));
131                let child_new_id = duped_child.inner.borrow().id;
132                all_locals.insert(child_new_id, duped_child);
133                child_new_id
134            })
135            .collect();
136
137        // Duplicate this table with correct parent
138        let new_table = LocalTable {
139            inner: Rc::new(RefCell::new(LocalTableInner {
140                id: new_id,
141                parent: new_parent,
142                children: new_children,
143                consts: inner.consts.clone(),
144                variables: inner.variables.clone(),
145            })),
146        };
147
148        // Register in all_locals
149        all_locals.insert(new_id, new_table.clone());
150
151        new_table
152    }
153}
154
155impl SymbolTable {
156    /// Record that `importer` imports `imported`.
157    pub fn add_import(&mut self, importer: Symbol, imported: Symbol) {
158        self.imports.entry(importer).or_default().insert(imported);
159    }
160
161    /// Record that multiple importers import the same `imported` program.
162    pub fn add_imported_by(&mut self, imported: Symbol, importers: &IndexSet<Symbol>) {
163        for importer in importers {
164            self.add_import(*importer, imported);
165        }
166    }
167
168    /// Returns all programs imported by a given program. This also includes implicit imports, i.e. libraries.
169    pub fn get_imports(&self, program: &Symbol) -> IndexSet<Symbol> {
170        self.imports.get(program).cloned().unwrap_or_default()
171    }
172
173    /// Returns a mutable reference to the set of imports for a given program.
174    pub fn get_imports_mut(&mut self, program: &Symbol) -> Option<&mut IndexSet<Symbol>> {
175        self.imports.get_mut(program)
176    }
177
178    /// Returns an iterator over all import relationships.
179    pub fn iter_imports(&self) -> impl Iterator<Item = (&Symbol, &IndexSet<Symbol>)> {
180        self.imports.iter()
181    }
182
183    /// Adds a library to the symbol table.
184    pub fn add_as_library(&mut self, library: Symbol) {
185        self.libraries.insert(library);
186    }
187
188    /// Checks if a given `Symbol` is the name of an available library.
189    pub fn is_library(&self, name: Symbol) -> bool {
190        self.libraries.contains(&name)
191    }
192
193    /// Check if `target` program is visible from `current` program.
194    fn is_visible(&self, current: Symbol, target: &Symbol) -> bool {
195        current == *target || self.imports.get(&current).map(|imports| imports.contains(target)).unwrap_or(false)
196    }
197
198    /// Returns the transitive closure of imports for a given program.
199    /// Libraries are excluded because they are compile-time only and should not appear in bytecode.
200    pub fn get_transitive_imports(&self, program: &Symbol) -> IndexSet<Symbol> {
201        let mut ordered = IndexSet::new();
202        let mut seen = IndexSet::new();
203
204        if let Some(direct_imports) = self.imports.get(program) {
205            for imported in direct_imports {
206                self.collect_imports_postorder(imported, &mut ordered, &mut seen);
207            }
208        }
209
210        ordered.retain(|sym| !self.libraries.contains(sym));
211        ordered
212    }
213
214    /// Performs a depth-first traversal of the import graph and records programs in dependency order, ensuring that
215    /// each program is added only after all of its transitive imports have been processed.
216    fn collect_imports_postorder(&self, program: &Symbol, ordered: &mut IndexSet<Symbol>, seen: &mut IndexSet<Symbol>) {
217        // Prevent cycles
218        if !seen.insert(*program) {
219            return;
220        }
221
222        if let Some(direct_imports) = self.imports.get(program) {
223            for imported in direct_imports {
224                self.collect_imports_postorder(imported, ordered, seen);
225            }
226        }
227
228        ordered.insert(*program);
229    }
230
231    /// Reset everything except leave consts that have been evaluated.
232    pub fn reset_but_consts(&mut self) {
233        self.functions.clear();
234        self.records.clear();
235        self.structs.clear();
236        self.globals.clear();
237
238        // clear all non-const locals
239        for local_table in self.all_locals.values_mut() {
240            local_table.clear_but_consts();
241        }
242
243        self.local = None;
244    }
245
246    /// Are we currently in the global scope?
247    pub fn global_scope(&self) -> bool {
248        self.local.is_none()
249    }
250
251    /// Iterator over all the structs (not records) in this program.
252    pub fn iter_structs(&self) -> impl Iterator<Item = (&Location, &Composite)> {
253        self.structs.iter()
254    }
255
256    /// Iterator over all the records in this program.
257    pub fn iter_records(&self) -> impl Iterator<Item = (&Location, &Composite)> {
258        self.records.iter()
259    }
260
261    /// Iterator over all the functions in this program.
262    pub fn iter_functions(&self) -> impl Iterator<Item = (&Location, &FunctionSymbol)> {
263        self.functions.iter()
264    }
265
266    /// Iterator over all the interfaces in this program.
267    pub fn iter_interfaces(&self) -> impl Iterator<Item = (&Location, &Interface)> {
268        self.interfaces.iter()
269    }
270
271    /// Access a struct by this location if it exists and is accessible from program named `current_program`.
272    pub fn lookup_struct(&self, current_program: Symbol, loc: &Location) -> Option<&Composite> {
273        if self.is_visible(current_program, &loc.program) { self.structs.get(loc) } else { None }
274    }
275
276    /// Access a record at this location if it exists and is accessible from program named `current_program`.
277    pub fn lookup_record(&self, current_program: Symbol, location: &Location) -> Option<&Composite> {
278        if self.is_visible(current_program, &location.program) { self.records.get(location) } else { None }
279    }
280
281    /// Access a function by this name if it exists and is accessible from program named `current_program`.
282    pub fn lookup_function(&self, current_program: Symbol, location: &Location) -> Option<&FunctionSymbol> {
283        if self.is_visible(current_program, &location.program) { self.functions.get(location) } else { None }
284    }
285
286    /// Access an interface by this name if it exists and is accessible from program named `current_program`.
287    pub fn lookup_interface(&self, current_program: Symbol, location: &Location) -> Option<&Interface> {
288        if self.is_visible(current_program, &location.program) { self.interfaces.get(location) } else { None }
289    }
290
291    /// Attempts to look up a variable by a path from program named `current_program`.
292    ///
293    /// First, it tries to resolve the symbol as a global using the full path under the given program.
294    /// If that fails and the path is non-empty, it falls back to resolving the last component
295    /// of the path as a local symbol.
296    ///
297    /// # Arguments
298    ///
299    /// * `program` - The root symbol representing the program or module context.
300    /// * `path` - A `Path`.
301    ///
302    /// # Returns
303    ///
304    /// An `Option<VariableSymbol>` containing the resolved symbol if found, otherwise `None`.
305    pub fn lookup_path(&self, current_program: Symbol, path: &Path) -> Option<VariableSymbol> {
306        if let Some(loc) = path.try_global_location() {
307            self.lookup_global(current_program, loc).cloned()
308        } else if let Some(sym) = path.try_local_symbol() {
309            self.lookup_local(sym)
310        } else {
311            None
312        }
313    }
314
315    /// Access the variable accessible by this name in the current scope.
316    pub fn lookup_local(&self, name: Symbol) -> Option<VariableSymbol> {
317        let mut current = self.local.as_ref();
318
319        while let Some(table) = current {
320            let borrowed = table.inner.borrow();
321            let value = borrowed.variables.get(&name);
322            if value.is_some() {
323                return value.cloned();
324            }
325
326            current = borrowed.parent.and_then(|id| self.all_locals.get(&id));
327        }
328
329        None
330    }
331
332    /// Enter the scope of this `NodeID`, creating a table if it doesn't exist yet.
333    ///
334    /// Passing `None` means to enter the global scope.
335    pub fn enter_scope(&mut self, id: Option<NodeID>) {
336        self.local = id.map(|id| {
337            let parent = self.local.as_ref().map(|table| table.inner.borrow().id);
338            let new_local_table = if let Some(existing) = self.all_locals.get(&id) {
339                existing.clone()
340            } else {
341                let new_table = LocalTable::new(self, id, parent);
342                self.all_locals.insert(id, new_table.clone());
343                new_table
344            };
345
346            assert_eq!(parent, new_local_table.inner.borrow().parent, "Entered scopes out of order.");
347            new_local_table.clone()
348        });
349    }
350
351    pub fn enter_existing_scope(&mut self, id: Option<NodeID>) {
352        self.local = id.map(|id| {
353            let parent = self.local.as_ref().map(|table| table.inner.borrow().id);
354            let new_local_table = if let Some(existing) = self.all_locals.get(&id) {
355                existing.clone()
356            } else {
357                panic!("local scope must exist");
358            };
359
360            assert_eq!(parent, new_local_table.inner.borrow().parent, "Entered scopes out of order.");
361            new_local_table.clone()
362        });
363    }
364
365    /// Enter a new scope by duplicating the local table at `old_id` recursively.
366    ///
367    /// Each scope in the subtree receives a fresh NodeID from `node_builder`.
368    /// The new root scope's parent is set to the current scope (if any).
369    /// Returns the NodeID of the new duplicated root scope.
370    pub fn enter_scope_duped(&mut self, old_id: NodeID, node_builder: &NodeBuilder) -> usize {
371        let old_local_table = self.all_locals.get(&old_id).expect("Must have an old scope to dup from.").clone();
372
373        // Recursively duplicate the table and all its children
374        let new_local_table =
375            old_local_table.dup(node_builder, &mut self.all_locals, self.local.as_ref().map(|t| t.inner.borrow().id));
376
377        let new_id = new_local_table.inner.borrow().id;
378
379        // Update current scope
380        self.local = Some(new_local_table);
381
382        new_id
383    }
384
385    /// Enter the parent scope of the current scope (or the global scope if there is no local parent scope).
386    pub fn enter_parent(&mut self) {
387        let parent: Option<NodeID> = self.local.as_ref().and_then(|table| table.inner.borrow().parent);
388        self.local = parent.map(|id| self.all_locals.get(&id).expect("Parent should exist.")).cloned();
389    }
390
391    /// Checks if a `symbol` is local to `scope`.
392    pub fn is_local_to(&self, scope: NodeID, symbol: Symbol) -> bool {
393        self.all_locals.get(&scope).map(|locals| locals.inner.borrow().variables.contains_key(&symbol)).unwrap_or(false)
394    }
395
396    /// Checks whether `symbol` is defined in the current scope (self.local) or any of its
397    /// ancestor scopes, up to and including `scope`.
398    ///
399    /// Returns `false` if the current scope is not a descendant of `scope`.
400    pub fn is_defined_in_scope_or_ancestor_until(&self, scope: NodeID, symbol: Symbol) -> bool {
401        let mut current = self.local.as_ref();
402
403        while let Some(table) = current {
404            let inner = table.inner.borrow();
405
406            // Check if symbol is defined in this scope
407            if inner.variables.contains_key(&symbol) {
408                return true;
409            }
410
411            // Stop when we reach the given upper-bound scope
412            if inner.id == scope {
413                break;
414            }
415
416            // Move to parent
417            current = inner.parent.and_then(|parent_id| self.all_locals.get(&parent_id));
418        }
419
420        false
421    }
422
423    /// Checks if a `symbol` is local to `scope` or any of its child scopes.
424    pub fn is_local_to_or_in_child_scope(&self, scope: NodeID, symbol: Symbol) -> bool {
425        let mut stack = vec![scope];
426
427        while let Some(current_id) = stack.pop() {
428            if let Some(table) = self.all_locals.get(&current_id) {
429                let inner = table.inner.borrow();
430
431                if inner.variables.contains_key(&symbol) {
432                    return true;
433                }
434
435                stack.extend(&inner.children);
436            }
437        }
438
439        false
440    }
441
442    /// Insert an evaluated local const into the current scope.
443    /// This function does nothing if we're in a global scope.
444    pub fn insert_local_const(&mut self, name: Symbol, value: Expression) {
445        if let Some(table) = self.local.as_mut() {
446            table.inner.borrow_mut().consts.insert(name, value);
447        }
448    }
449
450    /// Insert an evaluated global const into the global scope
451    /// This function does nothing if we're in a local scope.
452    pub fn insert_global_const(&mut self, location: Location, value: Expression) {
453        if self.global_scope() {
454            self.global_consts.insert(location, value);
455        }
456    }
457
458    /// Find the evaluated const accessible by the given name in the current scope.
459    pub fn lookup_local_const(&self, name: Symbol) -> Option<Expression> {
460        let mut current = self.local.as_ref();
461
462        while let Some(table) = current {
463            let borrowed = table.inner.borrow();
464            let value = borrowed.consts.get(&name);
465            if value.is_some() {
466                return value.cloned();
467            }
468
469            current = borrowed.parent.and_then(|id| self.all_locals.get(&id));
470        }
471
472        None
473    }
474
475    pub fn lookup_global_const(&self, current_program: Symbol, location: &Location) -> Option<Expression> {
476        if self.is_visible(current_program, &location.program) {
477            self.global_consts.get(location).cloned()
478        } else {
479            None
480        }
481    }
482
483    /// Insert a struct at this location.
484    pub fn insert_struct(&mut self, location: Location, r#struct: Composite) -> Result<()> {
485        self.check_shadow_global(&location, r#struct.identifier.span)?;
486        self.structs.insert(location, r#struct);
487        Ok(())
488    }
489
490    /// Insert a record at this location.
491    pub fn insert_record(&mut self, location: Location, record: Composite) -> Result<()> {
492        self.check_shadow_global(&location, record.identifier.span)?;
493        self.records.insert(location, record);
494        Ok(())
495    }
496
497    /// Insert a function at this location.
498    pub fn insert_function(&mut self, location: Location, function: Function) -> Result<()> {
499        self.check_shadow_global(&location, function.identifier.span)?;
500        self.functions.insert(location, FunctionSymbol { function, finalizer: None });
501        Ok(())
502    }
503
504    /// Insert an interface at this location.
505    pub fn insert_interface(&mut self, location: Location, int: Interface) -> Result<()> {
506        self.check_shadow_global(&location, int.identifier.span)?;
507        self.interfaces.insert(location, int);
508        Ok(())
509    }
510
511    /// Insert a global at this location.
512    pub fn insert_global(&mut self, location: Location, var: VariableSymbol) -> Result<()> {
513        self.check_shadow_global(&location, var.span)?;
514        self.globals.insert(location, var);
515        Ok(())
516    }
517
518    /// Access the global variable at this location if it exists and is visible
519    /// from the given `current_program`.
520    pub fn lookup_global(&self, current_program: Symbol, location: &Location) -> Option<&VariableSymbol> {
521        if self.is_visible(current_program, &location.program) { self.globals.get(location) } else { None }
522    }
523
524    /// Sets the type of a local using its name. Returns `false` if the local is not found.
525    pub fn set_local_type(&mut self, name: Symbol, ty: Type) -> bool {
526        let mut current = self.local.as_ref();
527
528        while let Some(table) = current {
529            let mut inner = table.inner.borrow_mut();
530
531            if let Some(sym) = inner.variables.get_mut(&name) {
532                sym.type_ = Some(ty);
533                return true;
534            }
535
536            current = inner.parent.and_then(|id| self.all_locals.get(&id));
537        }
538
539        false
540    }
541
542    /// Sets the type of a global using its location. Returns `false` if the global is not found.
543    pub fn set_global_type(&mut self, location: &Location, ty: Type) -> bool {
544        if let Some(sym) = self.globals.get_mut(location) {
545            sym.type_ = Some(ty);
546            return true;
547        }
548        false
549    }
550
551    pub fn emit_shadow_error(name: Symbol, span: Span, prev_span: Span) -> LeoError {
552        AstError::name_defined_multiple_times(name, span)
553            .with_labels(vec![
554                Label::new(format!("previous definition of `{name}` here"), prev_span).with_color(Color::Blue),
555                Label::new(format!("`{name}` redefined here"), span),
556            ])
557            .into()
558    }
559
560    fn check_shadow_global(&self, location: &Location, span: Span) -> Result<()> {
561        let name = location.path.last().expect("location path must have at least one segment.");
562
563        self.functions
564            .get(location)
565            .map(|f| f.function.identifier.span)
566            .or_else(|| self.records.get(location).map(|r| r.identifier.span))
567            .or_else(|| self.structs.get(location).map(|s| s.identifier.span))
568            .or_else(|| self.globals.get(location).map(|g| g.span))
569            .map_or_else(|| Ok(()), |prev_span| Err(Self::emit_shadow_error(*name, span, prev_span)))
570    }
571
572    fn check_shadow_variable(&self, program: Symbol, path: &[Symbol], span: Span) -> Result<()> {
573        let mut current = self.local.as_ref();
574
575        while let Some(table) = current {
576            if let [name] = &path
577                && let Some(var_symbol) = table.inner.borrow().variables.get(name)
578            {
579                return Err(Self::emit_shadow_error(*name, span, var_symbol.span));
580            }
581            current = table.inner.borrow().parent.map(|id| self.all_locals.get(&id).expect("Parent should exist."));
582        }
583
584        self.check_shadow_global(&Location::new(program, path.to_vec()), span)?;
585
586        Ok(())
587    }
588
589    /// Insert a variable into the current scope.
590    pub fn insert_variable(&mut self, program: Symbol, path: &[Symbol], var: VariableSymbol) -> Result<()> {
591        self.check_shadow_variable(program, path, var.span)?;
592
593        if let Some(table) = self.local.as_mut() {
594            let [name] = &path else { panic!("Local variables cannot have paths with more than 1 segment.") };
595            table.inner.borrow_mut().variables.insert(*name, var);
596        } else {
597            self.globals.insert(Location::new(program, path.to_vec()), var);
598        }
599
600        Ok(())
601    }
602
603    /// Attach a finalizer to a function.
604    pub fn attach_finalizer(
605        &mut self,
606        caller: Location,
607        callee: Location,
608        future_inputs: Vec<Location>,
609        inferred_inputs: Vec<Type>,
610    ) -> Result<()> {
611        let callee_location = Location::new(callee.program, callee.path);
612
613        if let Some(func) = self.functions.get_mut(&caller) {
614            func.finalizer = Some(Finalizer { location: callee_location, future_inputs, inferred_inputs });
615            Ok(())
616        } else {
617            Err(AstError::function_not_found(caller.path.iter().format("::")).into())
618        }
619    }
620}