Skip to main content

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/// Maximum number of alias expansion steps permitted during symbol resolution.
12///
13/// This limit is intended to prevent stack overflows from maliciously deep or cyclic
14/// alias graphs while remaining far above normal usage patterns.
15const MAX_ALIAS_EXPANSION_DEPTH: usize = 128;
16
17/// This trait abstracts over any type which acts as a symbol table, e.g. a [crate::ast::Module].
18pub trait SymbolTable {
19    /// The concrete iterator type for the container.
20    type SymbolIter: Iterator<Item = LocalSymbol>;
21
22    /// Get an iterator over the symbols in this symbol table, using the provided [SourceManager]
23    /// to emit errors for symbols which are invalid/unresolvable.
24    fn symbols(&self, source_manager: Arc<dyn SourceManager>) -> Self::SymbolIter;
25}
26
27impl SymbolTable for &crate::library::ModuleInfo {
28    type SymbolIter = alloc::vec::IntoIter<LocalSymbol>;
29
30    fn symbols(&self, _source_manager: Arc<dyn SourceManager>) -> Self::SymbolIter {
31        let module_items = self.items();
32        let mut items = Vec::with_capacity(module_items.len());
33
34        for (index, item) in module_items {
35            let name = item.name().clone();
36            let span = name.span();
37
38            assert_eq!(index.as_usize(), items.len());
39            items.push(LocalSymbol::Item {
40                name,
41                resolved: SymbolResolution::Local(Span::new(span, index)),
42            });
43        }
44
45        items.into_iter()
46    }
47}
48
49impl SymbolTable for &crate::ast::Module {
50    type SymbolIter = alloc::vec::IntoIter<LocalSymbol>;
51
52    fn symbols(&self, source_manager: Arc<dyn SourceManager>) -> Self::SymbolIter {
53        use crate::ast::{AliasTarget, Export};
54
55        let mut items = Vec::with_capacity(self.items.len());
56
57        for (i, item) in self.items.iter().enumerate() {
58            let id = ItemIndex::new(i);
59            let name = item.name().clone();
60            let span = name.span();
61            let name = name.into_inner();
62
63            if let Export::Alias(alias) = item {
64                match alias.target() {
65                    AliasTarget::MastRoot(root) => {
66                        items.push(LocalSymbol::Import {
67                            name: Span::new(span, name),
68                            resolution: Ok(SymbolResolution::MastRoot(*root)),
69                        });
70                    },
71                    AliasTarget::Path(path) => {
72                        let expanded = LocalSymbolTable::expand(
73                            |name| self.get_import(name).map(|alias| alias.target().clone()),
74                            path.as_deref(),
75                            &source_manager,
76                        );
77                        items.push(LocalSymbol::Import {
78                            name: Span::new(span, name),
79                            resolution: expanded,
80                        });
81                    },
82                }
83            } else {
84                items.push(LocalSymbol::Item {
85                    name: Ident::from_raw_parts(Span::new(span, name)),
86                    resolved: SymbolResolution::Local(Span::new(span, id)),
87                });
88            }
89        }
90
91        items.into_iter()
92    }
93}
94
95/// Represents a symbol within the context of a single module
96#[derive(Debug)]
97pub enum LocalSymbol {
98    /// This symbol is a declaration, with the given resolution.
99    Item { name: Ident, resolved: SymbolResolution },
100    /// This symbol is an import of an externally-defined item.
101    Import {
102        name: Span<Arc<str>>,
103        resolution: Result<SymbolResolution, SymbolResolutionError>,
104    },
105}
106
107impl LocalSymbol {
108    pub fn name(&self) -> &str {
109        match self {
110            Self::Item { name, .. } => name.as_str(),
111            Self::Import { name, .. } => name,
112        }
113    }
114}
115
116/// The common local symbol table/registry implementation
117pub(super) struct LocalSymbolTable {
118    source_manager: Arc<dyn SourceManager>,
119    symbols: BTreeMap<Arc<str>, ItemIndex>,
120    items: Vec<LocalSymbol>,
121}
122
123impl core::ops::Index<ItemIndex> for LocalSymbolTable {
124    type Output = LocalSymbol;
125
126    #[inline(always)]
127    fn index(&self, index: ItemIndex) -> &Self::Output {
128        &self.items[index.as_usize()]
129    }
130}
131
132impl LocalSymbolTable {
133    pub fn new<S>(iter: S, source_manager: Arc<dyn SourceManager>) -> Self
134    where
135        S: SymbolTable,
136    {
137        let mut symbols = BTreeMap::default();
138        let mut items = Vec::with_capacity(16);
139
140        for (i, symbol) in iter.symbols(source_manager.clone()).enumerate() {
141            log::debug!(target: "symbol-table::new", "registering {} symbol: {}", match symbol {
142                LocalSymbol::Item { .. } => "local",
143                LocalSymbol::Import { .. } => "imported",
144            }, symbol.name());
145
146            let id = ItemIndex::new(i);
147            let name = match &symbol {
148                LocalSymbol::Item { name, .. } => name.clone().into_inner(),
149                LocalSymbol::Import { name, .. } => name.clone().into_inner(),
150            };
151
152            symbols.insert(name, id);
153            items.push(symbol);
154        }
155
156        Self { source_manager, symbols, items }
157    }
158
159    #[inline(always)]
160    pub fn source_manager(&self) -> &dyn SourceManager {
161        &self.source_manager
162    }
163
164    #[inline(always)]
165    pub fn source_manager_arc(&self) -> Arc<dyn SourceManager> {
166        self.source_manager.clone()
167    }
168}
169
170impl LocalSymbolTable {
171    /// Get the symbol `name` from this table, if present.
172    ///
173    /// Returns `Ok(None)` if the symbol is undefined in this table.
174    ///
175    /// Returns `Ok(Some)` if the symbol is defined, and we were able to resolve it to either a
176    /// local or external item without encountering any issues.
177    ///
178    /// Returns `Err` if the symbol cannot possibly be resolved, e.g. the expanded path refers to
179    /// a child of an item that cannot have children, such as a procedure.
180    pub fn get(&self, name: Span<&str>) -> Result<SymbolResolution, SymbolResolutionError> {
181        log::debug!(target: "symbol-table", "attempting to resolve '{name}'");
182        let (span, name) = name.into_parts();
183        let Some(item) = self.symbols.get(name).copied() else {
184            return Err(SymbolResolutionError::undefined(span, &self.source_manager));
185        };
186        match &self.items[item.as_usize()] {
187            LocalSymbol::Item { resolved, .. } => {
188                log::debug!(target: "symbol-table", "resolved '{name}' to {resolved:?}");
189                Ok(resolved.clone())
190            },
191            LocalSymbol::Import { name, resolution } => {
192                log::debug!(target: "symbol-table", "'{name}' refers to an import");
193                match resolution {
194                    Ok(resolved) => {
195                        log::debug!(target: "symbol-table", "resolved '{name}' to {resolved:?}");
196                        Ok(resolved.clone())
197                    },
198                    Err(err) => {
199                        log::error!(target: "symbol-table", "resolution of '{name}' failed: {err}");
200                        Err(err.clone())
201                    },
202                }
203            },
204        }
205    }
206
207    /// Expand `path` in the context of `module`.
208    ///
209    /// Our aim here is to replace any leading import-relative path component with the corresponding
210    /// target path, recursively.
211    ///
212    /// Doing so ensures that code like the following works as expected:
213    ///
214    /// ```masm,ignore
215    /// use mylib::foo
216    /// use foo::bar->baz
217    ///
218    /// begin
219    ///     exec.baz::p
220    /// end
221    /// ```
222    ///
223    /// In the scenario above, calling `expand` on `baz::p` would proceed as follows:
224    ///
225    /// 1. `path` is `baz::p` a. We split `path` into `baz` and `p` (i.e. `module_name` and `rest`)
226    ///    b. We look for an import of the symbol `baz`, and find `use foo::bar->baz` c. The target
227    ///    of the import is `foo::bar`, which we recursively call `expand` on
228    /// 2. `path` is now `foo::bar` a. We split `path` into `foo` and `bar` b. We look for an import
229    ///    of `foo`, and find `use mylib::foo` c. The target of the import is `mylib::foo`, which we
230    ///    recursively call `expand` on
231    /// 3. `path` is now `mylib::foo` a. We split `path` into `mylib` and `foo` b. We look for an
232    ///    import of `mylib`, and do not find one. c. Since there is no import, we consider
233    ///    `mylib::foo` to be fully expanded and return it
234    /// 4. We've now expanded `foo` into `mylib::foo`, and so expansion of `foo::bar` is completed
235    ///    by joining `bar` to `mylib::foo`, and returning `mylib::foo::bar`.
236    /// 5. We've now expanded `baz` into `mylib::foo::bar`, and so the expansion of `baz::p` is
237    ///    completed by joining `p` to `mylib::foo::bar` and returning `mylib::foo::bar::p`.
238    /// 6. We're done, having successfully resolved `baz::p` to its full expansion
239    ///    `mylib::foo::bar::p`
240    pub fn expand<F>(
241        get_import: F,
242        path: Span<&Path>,
243        source_manager: &dyn SourceManager,
244    ) -> Result<SymbolResolution, SymbolResolutionError>
245    where
246        F: Fn(&str) -> Option<AliasTarget>,
247    {
248        let mut expansion_stack = Vec::new();
249        Self::expand_with_guard(get_import, path, source_manager, &mut expansion_stack)
250    }
251
252    fn expand_with_guard<F>(
253        get_import: F,
254        path: Span<&Path>,
255        source_manager: &dyn SourceManager,
256        expansion_stack: &mut Vec<Arc<Path>>,
257    ) -> Result<SymbolResolution, SymbolResolutionError>
258    where
259        F: Fn(&str) -> Option<AliasTarget>,
260    {
261        if expansion_stack.len() > MAX_ALIAS_EXPANSION_DEPTH {
262            return Err(SymbolResolutionError::alias_expansion_depth_exceeded(
263                path.span(),
264                MAX_ALIAS_EXPANSION_DEPTH,
265                source_manager,
266            ));
267        }
268
269        let path_ref: &Path = *path;
270        if expansion_stack.iter().any(|entry| entry.as_ref() == path_ref) {
271            return Err(SymbolResolutionError::alias_expansion_cycle(path.span(), source_manager));
272        }
273
274        expansion_stack.push(Arc::from(path_ref));
275
276        let result = {
277            let (module_name, rest) = path.split_first().unwrap();
278            if let Some(target) = get_import(module_name) {
279                match target {
280                    AliasTarget::MastRoot(digest) if rest.is_empty() => {
281                        Ok(SymbolResolution::MastRoot(digest))
282                    },
283                    AliasTarget::MastRoot(digest) => {
284                        Err(SymbolResolutionError::invalid_alias_target(
285                            digest.span(),
286                            path.span(),
287                            source_manager,
288                        ))
289                    },
290                    // If we have an import like `use lib::lib`, we cannot refer to the base `lib`
291                    // any longer, as it has been shadowed; any attempt to
292                    // further expand the path will recurse infinitely.
293                    //
294                    // For now, we handle this by simply stopping further expansion. In the future,
295                    // we may want to refine module.get_import to allow passing
296                    // an exclusion list, so that we can avoid recursing on the
297                    // same import in an infinite loop.
298                    AliasTarget::Path(shadowed) if shadowed.as_deref() == path => {
299                        Ok(SymbolResolution::External(shadowed))
300                    },
301                    AliasTarget::Path(path) => {
302                        let resolved = Self::expand_with_guard(
303                            get_import,
304                            path.as_deref(),
305                            source_manager,
306                            expansion_stack,
307                        )?;
308                        match resolved {
309                            SymbolResolution::Module { id, path } => {
310                                // We can consider this path fully-resolved, and mark it absolute,
311                                // if it is not already
312                                if rest.is_empty() {
313                                    Ok(SymbolResolution::Module { id, path })
314                                } else {
315                                    Ok(SymbolResolution::External(
316                                        path.map(|p| p.join(rest).into()),
317                                    ))
318                                }
319                            },
320                            SymbolResolution::External(resolved) => {
321                                // We can consider this path fully-resolved, and mark it absolute,
322                                // if it is not already
323                                Ok(SymbolResolution::External(
324                                    resolved.map(|p| p.to_absolute().join(rest).into()),
325                                ))
326                            },
327                            res @ (SymbolResolution::MastRoot(_)
328                            | SymbolResolution::Local(_)
329                            | SymbolResolution::Exact { .. })
330                                if rest.is_empty() =>
331                            {
332                                Ok(res)
333                            },
334                            SymbolResolution::MastRoot(digest) => {
335                                Err(SymbolResolutionError::invalid_alias_target(
336                                    digest.span(),
337                                    path.span(),
338                                    source_manager,
339                                ))
340                            },
341                            SymbolResolution::Exact { path: item_path, .. } => {
342                                Err(SymbolResolutionError::invalid_alias_target(
343                                    item_path.span(),
344                                    path.span(),
345                                    source_manager,
346                                ))
347                            },
348                            SymbolResolution::Local(item) => {
349                                Err(SymbolResolutionError::invalid_alias_target(
350                                    item.span(),
351                                    path.span(),
352                                    source_manager,
353                                ))
354                            },
355                        }
356                    },
357                }
358            } else {
359                // We can consider this path fully-resolved, and mark it absolute, if it is not
360                // already
361                Ok(SymbolResolution::External(path.map(|p| p.to_absolute().into_owned().into())))
362            }
363        };
364
365        expansion_stack.pop();
366        result
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use alloc::{
373        collections::BTreeMap,
374        string::{String, ToString},
375        sync::Arc,
376    };
377    use core::str::FromStr;
378
379    use miden_debug_types::DefaultSourceManager;
380
381    use super::*;
382    use crate::PathBuf;
383
384    fn path_arc(path: &str) -> Arc<Path> {
385        let path = PathBuf::from_str(path).expect("valid path");
386        Arc::from(path.as_path())
387    }
388
389    #[test]
390    fn alias_expansion_detects_cycle() {
391        let source_manager = DefaultSourceManager::default();
392        let mut imports = BTreeMap::<String, AliasTarget>::new();
393        imports.insert("a".to_string(), AliasTarget::Path(Span::unknown(path_arc("b"))));
394        imports.insert("b".to_string(), AliasTarget::Path(Span::unknown(path_arc("a"))));
395
396        let path = PathBuf::from_str("a").expect("valid path");
397        let result = LocalSymbolTable::expand(
398            |name| imports.get(name).cloned(),
399            Span::unknown(path.as_path()),
400            &source_manager,
401        );
402
403        assert!(matches!(result, Err(SymbolResolutionError::AliasExpansionCycle { .. })));
404    }
405
406    #[test]
407    fn alias_expansion_depth_boundary() {
408        let source_manager = DefaultSourceManager::default();
409        let mut imports = BTreeMap::<String, AliasTarget>::new();
410        let max_depth = MAX_ALIAS_EXPANSION_DEPTH;
411        for i in 0..max_depth {
412            let current = format!("a{i}");
413            let next = format!("a{}", i + 1);
414            imports.insert(current, AliasTarget::Path(Span::unknown(path_arc(&next))));
415        }
416
417        let path = PathBuf::from_str("a0").expect("valid path");
418        let result = LocalSymbolTable::expand(
419            |name| imports.get(name).cloned(),
420            Span::unknown(path.as_path()),
421            &source_manager,
422        )
423        .expect("expected depth boundary to resolve");
424
425        match result {
426            SymbolResolution::External(resolved) => {
427                let expected = format!("a{max_depth}");
428                let expected = PathBuf::from_str(&expected).expect("valid path");
429                let expected = expected.as_path().to_absolute().into_owned();
430                assert_eq!(resolved.as_deref(), expected.as_path());
431            },
432            other => panic!("expected external resolution, got {other:?}"),
433        }
434    }
435
436    #[test]
437    fn alias_expansion_depth_exceeded() {
438        let source_manager = DefaultSourceManager::default();
439        let mut imports = BTreeMap::<String, AliasTarget>::new();
440        for i in 0..=MAX_ALIAS_EXPANSION_DEPTH {
441            let current = format!("a{i}");
442            let next = format!("a{}", i + 1);
443            imports.insert(current, AliasTarget::Path(Span::unknown(path_arc(&next))));
444        }
445
446        let path = PathBuf::from_str("a0").expect("valid path");
447        let result = LocalSymbolTable::expand(
448            |name| imports.get(name).cloned(),
449            Span::unknown(path.as_path()),
450            &source_manager,
451        );
452
453        assert!(matches!(
454            result,
455            Err(SymbolResolutionError::AliasExpansionDepthExceeded { max_depth, .. })
456                if max_depth == MAX_ALIAS_EXPANSION_DEPTH
457        ));
458    }
459
460    #[test]
461    fn alias_expansion_handles_shadowed_import() {
462        let source_manager = DefaultSourceManager::default();
463        let mut imports = BTreeMap::<String, AliasTarget>::new();
464        imports.insert("lib".to_string(), AliasTarget::Path(Span::unknown(path_arc("lib"))));
465
466        let path = PathBuf::from_str("lib").expect("valid path");
467        let result = LocalSymbolTable::expand(
468            |name| imports.get(name).cloned(),
469            Span::unknown(path.as_path()),
470            &source_manager,
471        )
472        .expect("shadowed import should resolve");
473
474        match result {
475            SymbolResolution::External(resolved) => {
476                assert_eq!(resolved.as_deref(), path.as_path());
477            },
478            other => panic!("expected external resolution, got {other:?}"),
479        }
480    }
481}