miden_assembly_syntax/ast/item/resolver/
symbol_table.rs

1use alloc::{collections::BTreeMap, sync::Arc, vec::Vec};
2
3use miden_debug_types::{SourceManager, Span, Spanned};
4
5use super::{SymbolResolution, SymbolResolutionError};
6use crate::{
7    Path,
8    ast::{AliasTarget, Ident, ItemIndex},
9};
10
11/// This trait abstracts over any type which acts as a symbol table, e.g. a [crate::ast::Module].
12pub trait SymbolTable {
13    /// The concrete iterator type for the container.
14    type SymbolIter: Iterator<Item = LocalSymbol>;
15
16    /// Get an iterator over the symbols in this symbol table, using the provided [SourceManager]
17    /// to emit errors for symbols which are invalid/unresolvable.
18    fn symbols(&self, source_manager: Arc<dyn SourceManager>) -> Self::SymbolIter;
19}
20
21impl SymbolTable for &crate::library::ModuleInfo {
22    type SymbolIter = alloc::vec::IntoIter<LocalSymbol>;
23
24    fn symbols(&self, _source_manager: Arc<dyn SourceManager>) -> Self::SymbolIter {
25        let module_items = self.items();
26        let mut items = Vec::with_capacity(module_items.len());
27
28        for (index, item) in module_items {
29            let name = item.name().clone();
30            let span = name.span();
31
32            assert_eq!(index.as_usize(), items.len());
33            items.push(LocalSymbol::Item {
34                name,
35                resolved: SymbolResolution::Local(Span::new(span, index)),
36            });
37        }
38
39        items.into_iter()
40    }
41}
42
43impl SymbolTable for &crate::ast::Module {
44    type SymbolIter = alloc::vec::IntoIter<LocalSymbol>;
45
46    fn symbols(&self, source_manager: Arc<dyn SourceManager>) -> Self::SymbolIter {
47        use crate::ast::{AliasTarget, Export};
48
49        let mut items = Vec::with_capacity(self.items.len());
50
51        for (i, item) in self.items.iter().enumerate() {
52            let id = ItemIndex::new(i);
53            let name = item.name().clone();
54            let span = name.span();
55            let name = name.into_inner();
56
57            if let Export::Alias(alias) = item {
58                match alias.target() {
59                    AliasTarget::MastRoot(root) => {
60                        items.push(LocalSymbol::Import {
61                            name: Span::new(span, name),
62                            resolution: Ok(SymbolResolution::MastRoot(*root)),
63                        });
64                    },
65                    AliasTarget::Path(path) => {
66                        let expanded = LocalSymbolTable::expand(
67                            |name| self.get_import(name).map(|alias| alias.target().clone()),
68                            path.as_deref(),
69                            &source_manager,
70                        );
71                        items.push(LocalSymbol::Import {
72                            name: Span::new(span, name),
73                            resolution: expanded,
74                        });
75                    },
76                }
77            } else {
78                items.push(LocalSymbol::Item {
79                    name: Ident::from_raw_parts(Span::new(span, name)),
80                    resolved: SymbolResolution::Local(Span::new(span, id)),
81                });
82            }
83        }
84
85        items.into_iter()
86    }
87}
88
89/// Represents a symbol within the context of a single module
90#[derive(Debug)]
91pub enum LocalSymbol {
92    /// This symbol is a declaration, with the given resolution.
93    Item { name: Ident, resolved: SymbolResolution },
94    /// This symbol is an import of an externally-defined item.
95    Import {
96        name: Span<Arc<str>>,
97        resolution: Result<SymbolResolution, SymbolResolutionError>,
98    },
99}
100
101impl LocalSymbol {
102    pub fn name(&self) -> &str {
103        match self {
104            Self::Item { name, .. } => name.as_str(),
105            Self::Import { name, .. } => name,
106        }
107    }
108}
109
110/// The common local symbol table/registry implementation
111pub(super) struct LocalSymbolTable {
112    source_manager: Arc<dyn SourceManager>,
113    symbols: BTreeMap<Arc<str>, ItemIndex>,
114    items: Vec<LocalSymbol>,
115}
116
117impl core::ops::Index<ItemIndex> for LocalSymbolTable {
118    type Output = LocalSymbol;
119
120    #[inline(always)]
121    fn index(&self, index: ItemIndex) -> &Self::Output {
122        &self.items[index.as_usize()]
123    }
124}
125
126impl LocalSymbolTable {
127    pub fn new<S>(iter: S, source_manager: Arc<dyn SourceManager>) -> Self
128    where
129        S: SymbolTable,
130    {
131        let mut symbols = BTreeMap::default();
132        let mut items = Vec::with_capacity(16);
133
134        for (i, symbol) in iter.symbols(source_manager.clone()).enumerate() {
135            log::debug!(target: "symbol-table::new", "registering {} symbol: {}", match symbol {
136                LocalSymbol::Item { .. } => "local",
137                LocalSymbol::Import { .. } => "imported",
138            }, symbol.name());
139
140            let id = ItemIndex::new(i);
141            let name = match &symbol {
142                LocalSymbol::Item { name, .. } => name.clone().into_inner(),
143                LocalSymbol::Import { name, .. } => name.clone().into_inner(),
144            };
145
146            symbols.insert(name, id);
147            items.push(symbol);
148        }
149
150        Self { source_manager, symbols, items }
151    }
152
153    #[inline(always)]
154    pub fn source_manager(&self) -> &dyn SourceManager {
155        &self.source_manager
156    }
157
158    #[inline(always)]
159    pub fn source_manager_arc(&self) -> Arc<dyn SourceManager> {
160        self.source_manager.clone()
161    }
162}
163
164impl LocalSymbolTable {
165    /// Get the symbol `name` from this table, if present.
166    ///
167    /// Returns `Ok(None)` if the symbol is undefined in this table.
168    ///
169    /// Returns `Ok(Some)` if the symbol is defined, and we were able to resolve it to either a
170    /// local or external item without encountering any issues.
171    ///
172    /// Returns `Err` if the symbol cannot possibly be resolved, e.g. the expanded path refers to
173    /// a child of an item that cannot have children, such as a procedure.
174    pub fn get(&self, name: Span<&str>) -> Result<SymbolResolution, SymbolResolutionError> {
175        log::debug!(target: "symbol-table", "attempting to resolve '{name}'");
176        let (span, name) = name.into_parts();
177        let Some(item) = self.symbols.get(name).copied() else {
178            return Err(SymbolResolutionError::undefined(span, &self.source_manager));
179        };
180        match &self.items[item.as_usize()] {
181            LocalSymbol::Item { resolved, .. } => {
182                log::debug!(target: "symbol-table", "resolved '{name}' to {resolved:?}");
183                Ok(resolved.clone())
184            },
185            LocalSymbol::Import { name, resolution } => {
186                log::debug!(target: "symbol-table", "'{name}' refers to an import");
187                match resolution {
188                    Ok(resolved) => {
189                        log::debug!(target: "symbol-table", "resolved '{name}' to {resolved:?}");
190                        Ok(resolved.clone())
191                    },
192                    Err(err) => {
193                        log::error!(target: "symbol-table", "resolution of '{name}' failed: {err}");
194                        Err(err.clone())
195                    },
196                }
197            },
198        }
199    }
200
201    /// Expand `path` in the context of `module`.
202    ///
203    /// Our aim here is to replace any leading import-relative path component with the corresponding
204    /// target path, recursively.
205    ///
206    /// Doing so ensures that code like the following works as expected:
207    ///
208    /// ```masm,ignore
209    /// use mylib::foo
210    /// use foo::bar->baz
211    ///
212    /// begin
213    ///     exec.baz::p
214    /// end
215    /// ```
216    ///
217    /// In the scenario above, calling `expand` on `baz::p` would proceed as follows:
218    ///
219    /// 1. `path` is `baz::p` a. We split `path` into `baz` and `p` (i.e. `module_name` and `rest`)
220    ///    b. We look for an import of the symbol `baz`, and find `use foo::bar->baz` c. The target
221    ///    of the import is `foo::bar`, which we recursively call `expand` on
222    /// 2. `path` is now `foo::bar` a. We split `path` into `foo` and `bar` b. We look for an import
223    ///    of `foo`, and find `use mylib::foo` c. The target of the import is `mylib::foo`, which we
224    ///    recursively call `expand` on
225    /// 3. `path` is now `mylib::foo` a. We split `path` into `mylib` and `foo` b. We look for an
226    ///    import of `mylib`, and do not find one. c. Since there is no import, we consider
227    ///    `mylib::foo` to be fully expanded and return it
228    /// 4. We've now expanded `foo` into `mylib::foo`, and so expansion of `foo::bar` is completed
229    ///    by joining `bar` to `mylib::foo`, and returning `mylib::foo::bar`.
230    /// 5. We've now expanded `baz` into `mylib::foo::bar`, and so the expansion of `baz::p` is
231    ///    completed by joining `p` to `mylib::foo::bar` and returning `mylib::foo::bar::p`.
232    /// 6. We're done, having successfully resolved `baz::p` to its full expansion
233    ///    `mylib::foo::bar::p`
234    pub fn expand<F>(
235        get_import: F,
236        path: Span<&Path>,
237        source_manager: &dyn SourceManager,
238    ) -> Result<SymbolResolution, SymbolResolutionError>
239    where
240        F: Fn(&str) -> Option<AliasTarget>,
241    {
242        let (module_name, rest) = path.split_first().unwrap();
243        if let Some(target) = get_import(module_name) {
244            match target {
245                AliasTarget::MastRoot(digest) if rest.is_empty() => {
246                    Ok(SymbolResolution::MastRoot(digest))
247                },
248                AliasTarget::MastRoot(digest) => Err(SymbolResolutionError::invalid_alias_target(
249                    digest.span(),
250                    path.span(),
251                    source_manager,
252                )),
253                // If we have an import like `use lib::lib`, we cannot refer to the base `lib` any
254                // longer, as it has been shadowed; any attempt to further expand the path will
255                // recurse infinitely.
256                //
257                // For now, we handle this by simply stopping further expansion. In the future, we
258                // may want to refine module.get_import to allow passing an exclusion list, so that
259                // we can avoid recursing on the same import in an infinite loop.
260                AliasTarget::Path(shadowed) if shadowed.as_deref() == path => {
261                    Ok(SymbolResolution::External(shadowed.clone()))
262                },
263                AliasTarget::Path(path) => {
264                    let path = path.clone();
265                    let resolved = Self::expand(get_import, path.as_deref(), source_manager)?;
266                    match resolved {
267                        SymbolResolution::Module { id, path } => {
268                            // We can consider this path fully-resolved, and mark it absolute, if it
269                            // is not already
270                            if rest.is_empty() {
271                                Ok(SymbolResolution::Module { id, path })
272                            } else {
273                                Ok(SymbolResolution::External(path.map(|p| p.join(rest).into())))
274                            }
275                        },
276                        SymbolResolution::External(resolved) => {
277                            // We can consider this path fully-resolved, and mark it absolute, if it
278                            // is not already
279                            Ok(SymbolResolution::External(
280                                resolved.map(|p| p.to_absolute().join(rest).into()),
281                            ))
282                        },
283                        res @ (SymbolResolution::MastRoot(_)
284                        | SymbolResolution::Local(_)
285                        | SymbolResolution::Exact { .. })
286                            if rest.is_empty() =>
287                        {
288                            Ok(res)
289                        },
290                        SymbolResolution::MastRoot(digest) => {
291                            Err(SymbolResolutionError::invalid_alias_target(
292                                digest.span(),
293                                path.span(),
294                                source_manager,
295                            ))
296                        },
297                        SymbolResolution::Exact { path: item_path, .. } => {
298                            Err(SymbolResolutionError::invalid_alias_target(
299                                item_path.span(),
300                                path.span(),
301                                source_manager,
302                            ))
303                        },
304                        SymbolResolution::Local(item) => {
305                            Err(SymbolResolutionError::invalid_alias_target(
306                                item.span(),
307                                path.span(),
308                                source_manager,
309                            ))
310                        },
311                    }
312                },
313            }
314        } else {
315            // We can consider this path fully-resolved, and mark it absolute, if it is not already
316            Ok(SymbolResolution::External(path.map(|p| p.to_absolute().into_owned().into())))
317        }
318    }
319}