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, SourceSpan, 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].
18///
19/// Resolver construction uses [Self::checked_symbols], which must either return the full symbol
20/// set or a structured error.
21pub trait SymbolTable {
22    /// The concrete iterator type for the container.
23    type SymbolIter: Iterator<Item = LocalSymbol>;
24
25    /// Get an iterator over the symbols in this symbol table, using the provided [SourceManager]
26    /// to emit errors for symbols which are invalid/unresolvable.
27    fn symbols(&self, source_manager: Arc<dyn SourceManager>) -> Self::SymbolIter;
28
29    /// Get an iterator over the symbols in this symbol table, returning a structured error if the
30    /// full symbol set cannot be represented exactly.
31    ///
32    /// Override this when exact resolver construction needs validation, such as rejecting oversized
33    /// symbol sets.
34    fn checked_symbols(
35        &self,
36        source_manager: Arc<dyn SourceManager>,
37    ) -> Result<Self::SymbolIter, SymbolResolutionError> {
38        Ok(self.symbols(source_manager))
39    }
40}
41
42impl SymbolTable for &crate::library::ModuleInfo {
43    type SymbolIter = alloc::vec::IntoIter<LocalSymbol>;
44
45    fn symbols(&self, _source_manager: Arc<dyn SourceManager>) -> Self::SymbolIter {
46        let mut items = Vec::with_capacity(self.raw_items().len());
47
48        for (i, item) in self.raw_items().iter().enumerate() {
49            let name = item.name().clone();
50            let span = name.span();
51            items.push(LocalSymbol::Item {
52                name,
53                resolved: SymbolResolution::Local(Span::new(span, ItemIndex::new(i))),
54            });
55        }
56
57        items.into_iter()
58    }
59
60    fn checked_symbols(
61        &self,
62        source_manager: Arc<dyn SourceManager>,
63    ) -> Result<Self::SymbolIter, SymbolResolutionError> {
64        if self.raw_items().len() > ItemIndex::MAX_ITEMS {
65            Err(SymbolResolutionError::too_many_items_in_module(
66                SourceSpan::UNKNOWN,
67                &*source_manager,
68            ))
69        } else {
70            Ok(self.symbols(source_manager))
71        }
72    }
73}
74
75impl SymbolTable for &crate::ast::Module {
76    type SymbolIter = alloc::vec::IntoIter<LocalSymbol>;
77
78    fn symbols(&self, source_manager: Arc<dyn SourceManager>) -> Self::SymbolIter {
79        use crate::ast::{AliasTarget, Export};
80
81        let mut items = Vec::with_capacity(self.items.len());
82
83        for (i, item) in self.items.iter().enumerate() {
84            let id = ItemIndex::new(i);
85            let name = item.name().clone();
86            let span = name.span();
87            let name = name.into_inner();
88
89            if let Export::Alias(alias) = item {
90                match alias.target() {
91                    AliasTarget::MastRoot(root) => {
92                        items.push(LocalSymbol::Import {
93                            name: Span::new(span, name),
94                            resolution: Ok(SymbolResolution::MastRoot(*root)),
95                        });
96                    },
97                    AliasTarget::Path(path) => {
98                        let expanded = LocalSymbolTable::expand(
99                            |name| self.get_import(name).map(|alias| alias.target().clone()),
100                            path.as_deref(),
101                            &source_manager,
102                        );
103                        items.push(LocalSymbol::Import {
104                            name: Span::new(span, name),
105                            resolution: expanded,
106                        });
107                    },
108                }
109            } else {
110                items.push(LocalSymbol::Item {
111                    name: Ident::from_raw_parts(Span::new(span, name)),
112                    resolved: SymbolResolution::Local(Span::new(span, id)),
113                });
114            }
115        }
116
117        items.into_iter()
118    }
119
120    fn checked_symbols(
121        &self,
122        source_manager: Arc<dyn SourceManager>,
123    ) -> Result<Self::SymbolIter, SymbolResolutionError> {
124        if self.items.len() > ItemIndex::MAX_ITEMS {
125            Err(SymbolResolutionError::too_many_items_in_module(self.span(), &*source_manager))
126        } else {
127            Ok(self.symbols(source_manager))
128        }
129    }
130}
131
132/// Represents a symbol within the context of a single module
133#[derive(Debug)]
134pub enum LocalSymbol {
135    /// This symbol is a declaration, with the given resolution.
136    Item { name: Ident, resolved: SymbolResolution },
137    /// This symbol is an import of an externally-defined item.
138    Import {
139        name: Span<Arc<str>>,
140        resolution: Result<SymbolResolution, SymbolResolutionError>,
141    },
142}
143
144impl LocalSymbol {
145    pub fn name(&self) -> &str {
146        match self {
147            Self::Item { name, .. } => name.as_str(),
148            Self::Import { name, .. } => name,
149        }
150    }
151}
152
153/// The common local symbol table/registry implementation
154pub(super) struct LocalSymbolTable {
155    source_manager: Arc<dyn SourceManager>,
156    symbols: BTreeMap<Arc<str>, ItemIndex>,
157    items: Vec<LocalSymbol>,
158}
159
160impl core::ops::Index<ItemIndex> for LocalSymbolTable {
161    type Output = LocalSymbol;
162
163    #[inline(always)]
164    fn index(&self, index: ItemIndex) -> &Self::Output {
165        &self.items[index.as_usize()]
166    }
167}
168
169impl LocalSymbolTable {
170    fn build<I>(iter: I, source_manager: Arc<dyn SourceManager>) -> Self
171    where
172        I: Iterator<Item = LocalSymbol>,
173    {
174        let mut symbols = BTreeMap::default();
175        let mut items = Vec::with_capacity(16);
176
177        for (i, symbol) in iter.enumerate() {
178            let id = ItemIndex::try_new(i)
179                .expect("symbol iterators used by LocalSymbolTable::build must be pre-validated");
180            let symbol = match symbol {
181                LocalSymbol::Item {
182                    name,
183                    resolved: SymbolResolution::Local(local),
184                } => LocalSymbol::Item {
185                    name,
186                    resolved: SymbolResolution::Local(Span::new(local.span(), id)),
187                },
188                symbol => symbol,
189            };
190            log::debug!(target: "symbol-table::new", "registering {} symbol: {}", match symbol {
191                LocalSymbol::Item { .. } => "local",
192                LocalSymbol::Import { .. } => "imported",
193            }, symbol.name());
194            let name = match &symbol {
195                LocalSymbol::Item { name, .. } => name.clone().into_inner(),
196                LocalSymbol::Import { name, .. } => name.clone().into_inner(),
197            };
198
199            if let Some(prev) = symbols.get(&name).copied() {
200                debug_assert!(
201                    false,
202                    "duplicate symbol '{name}' reached local resolver construction (previous={prev:?}, current={id:?})"
203                );
204            } else {
205                symbols.insert(name.clone(), id);
206            }
207            items.push(symbol);
208        }
209
210        Self { source_manager, symbols, items }
211    }
212
213    pub fn new<S>(
214        iter: S,
215        source_manager: Arc<dyn SourceManager>,
216    ) -> Result<Self, SymbolResolutionError>
217    where
218        S: SymbolTable,
219    {
220        let symbols = iter.checked_symbols(source_manager.clone())?;
221        Ok(Self::build(symbols, source_manager))
222    }
223}
224
225impl LocalSymbolTable {
226    /// Get the symbol `name` from this table, if present.
227    ///
228    /// Returns `Ok(None)` if the symbol is undefined in this table.
229    ///
230    /// Returns `Ok(Some)` if the symbol is defined, and we were able to resolve it to either a
231    /// local or external item without encountering any issues.
232    ///
233    /// Returns `Err` if the symbol cannot possibly be resolved, e.g. the expanded path refers to
234    /// a child of an item that cannot have children, such as a procedure.
235    pub fn get(&self, name: Span<&str>) -> Result<SymbolResolution, SymbolResolutionError> {
236        log::debug!(target: "symbol-table", "attempting to resolve '{name}'");
237        let (span, name) = name.into_parts();
238        let Some(item) = self.symbols.get(name).copied() else {
239            return Err(SymbolResolutionError::undefined(span, &self.source_manager));
240        };
241        match &self.items[item.as_usize()] {
242            LocalSymbol::Item { resolved, .. } => {
243                log::debug!(target: "symbol-table", "resolved '{name}' to {resolved:?}");
244                Ok(resolved.clone())
245            },
246            LocalSymbol::Import { name, resolution } => {
247                log::debug!(target: "symbol-table", "'{name}' refers to an import");
248                match resolution {
249                    Ok(resolved) => {
250                        log::debug!(target: "symbol-table", "resolved '{name}' to {resolved:?}");
251                        Ok(resolved.clone())
252                    },
253                    Err(err) => {
254                        log::error!(target: "symbol-table", "resolution of '{name}' failed: {err}");
255                        Err(err.clone())
256                    },
257                }
258            },
259        }
260    }
261
262    /// Expand `path` in the context of `module`.
263    ///
264    /// Our aim here is to replace any leading import-relative path component with the corresponding
265    /// target path, recursively.
266    ///
267    /// Doing so ensures that code like the following works as expected:
268    ///
269    /// ```masm,ignore
270    /// use mylib::foo
271    /// use foo::bar->baz
272    ///
273    /// begin
274    ///     exec.baz::p
275    /// end
276    /// ```
277    ///
278    /// In the scenario above, calling `expand` on `baz::p` would proceed as follows:
279    ///
280    /// 1. `path` is `baz::p` a. We split `path` into `baz` and `p` (i.e. `module_name` and `rest`)
281    ///    b. We look for an import of the symbol `baz`, and find `use foo::bar->baz` c. The target
282    ///    of the import is `foo::bar`, which we recursively call `expand` on
283    /// 2. `path` is now `foo::bar` a. We split `path` into `foo` and `bar` b. We look for an import
284    ///    of `foo`, and find `use mylib::foo` c. The target of the import is `mylib::foo`, which we
285    ///    recursively call `expand` on
286    /// 3. `path` is now `mylib::foo` a. We split `path` into `mylib` and `foo` b. We look for an
287    ///    import of `mylib`, and do not find one. c. Since there is no import, we consider
288    ///    `mylib::foo` to be fully expanded and return it
289    /// 4. We've now expanded `foo` into `mylib::foo`, and so expansion of `foo::bar` is completed
290    ///    by joining `bar` to `mylib::foo`, and returning `mylib::foo::bar`.
291    /// 5. We've now expanded `baz` into `mylib::foo::bar`, and so the expansion of `baz::p` is
292    ///    completed by joining `p` to `mylib::foo::bar` and returning `mylib::foo::bar::p`.
293    /// 6. We're done, having successfully resolved `baz::p` to its full expansion
294    ///    `mylib::foo::bar::p`
295    pub fn expand<F>(
296        get_import: F,
297        path: Span<&Path>,
298        source_manager: &dyn SourceManager,
299    ) -> Result<SymbolResolution, SymbolResolutionError>
300    where
301        F: Fn(&str) -> Option<AliasTarget>,
302    {
303        let mut expansion_stack = Vec::new();
304        Self::expand_with_guard(get_import, path, source_manager, &mut expansion_stack)
305    }
306
307    fn expand_with_guard<F>(
308        get_import: F,
309        path: Span<&Path>,
310        source_manager: &dyn SourceManager,
311        expansion_stack: &mut Vec<Arc<Path>>,
312    ) -> Result<SymbolResolution, SymbolResolutionError>
313    where
314        F: Fn(&str) -> Option<AliasTarget>,
315    {
316        if expansion_stack.len() > MAX_ALIAS_EXPANSION_DEPTH {
317            return Err(SymbolResolutionError::alias_expansion_depth_exceeded(
318                path.span(),
319                MAX_ALIAS_EXPANSION_DEPTH,
320                source_manager,
321            ));
322        }
323
324        let path_ref: &Path = *path;
325        if expansion_stack.iter().any(|entry| entry.as_ref() == path_ref) {
326            return Err(SymbolResolutionError::alias_expansion_cycle(path.span(), source_manager));
327        }
328
329        expansion_stack.push(Arc::from(path_ref));
330
331        let result = {
332            let (module_name, rest) = path.split_first().unwrap();
333            if let Some(target) = get_import(module_name) {
334                match target {
335                    AliasTarget::MastRoot(digest) if rest.is_empty() => {
336                        Ok(SymbolResolution::MastRoot(digest))
337                    },
338                    AliasTarget::MastRoot(digest) => {
339                        Err(SymbolResolutionError::invalid_alias_target(
340                            digest.span(),
341                            path.span(),
342                            source_manager,
343                        ))
344                    },
345                    // If we have an import like `use lib::lib`, we cannot refer to the base `lib`
346                    // any longer, as it has been shadowed; any attempt to
347                    // further expand the path will recurse infinitely.
348                    //
349                    // For now, we handle this by simply stopping further expansion. In the future,
350                    // we may want to refine module.get_import to allow passing
351                    // an exclusion list, so that we can avoid recursing on the
352                    // same import in an infinite loop.
353                    AliasTarget::Path(shadowed) if shadowed.as_deref() == path => {
354                        Ok(SymbolResolution::External(shadowed))
355                    },
356                    AliasTarget::Path(path) => {
357                        let resolved = Self::expand_with_guard(
358                            get_import,
359                            path.as_deref(),
360                            source_manager,
361                            expansion_stack,
362                        )?;
363                        match resolved {
364                            SymbolResolution::Module { id, path } => {
365                                // We can consider this path fully-resolved, and mark it absolute,
366                                // if it is not already
367                                if rest.is_empty() {
368                                    Ok(SymbolResolution::Module { id, path })
369                                } else {
370                                    Ok(SymbolResolution::External(
371                                        path.map(|p| p.join(rest).into()),
372                                    ))
373                                }
374                            },
375                            SymbolResolution::External(resolved) => {
376                                // We can consider this path fully-resolved, and mark it absolute,
377                                // if it is not already
378                                Ok(SymbolResolution::External(
379                                    resolved.map(|p| p.to_absolute().join(rest).into()),
380                                ))
381                            },
382                            res @ (SymbolResolution::MastRoot(_)
383                            | SymbolResolution::Local(_)
384                            | SymbolResolution::Exact { .. })
385                                if rest.is_empty() =>
386                            {
387                                Ok(res)
388                            },
389                            SymbolResolution::MastRoot(digest) => {
390                                Err(SymbolResolutionError::invalid_alias_target(
391                                    digest.span(),
392                                    path.span(),
393                                    source_manager,
394                                ))
395                            },
396                            SymbolResolution::Exact { path: item_path, .. } => {
397                                Err(SymbolResolutionError::invalid_alias_target(
398                                    item_path.span(),
399                                    path.span(),
400                                    source_manager,
401                                ))
402                            },
403                            SymbolResolution::Local(item) => {
404                                Err(SymbolResolutionError::invalid_alias_target(
405                                    item.span(),
406                                    path.span(),
407                                    source_manager,
408                                ))
409                            },
410                        }
411                    },
412                }
413            } else {
414                // We can consider this path fully-resolved, and mark it absolute, if it is not
415                // already
416                Ok(SymbolResolution::External(path.map(|p| p.to_absolute().into_owned().into())))
417            }
418        };
419
420        expansion_stack.pop();
421        result
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use alloc::{
428        collections::BTreeMap,
429        string::{String, ToString},
430        sync::Arc,
431    };
432    use core::str::FromStr;
433
434    use miden_debug_types::DefaultSourceManager;
435
436    use super::*;
437    use crate::PathBuf;
438
439    fn path_arc(path: &str) -> Arc<Path> {
440        let path = PathBuf::from_str(path).expect("valid path");
441        Arc::from(path.as_path())
442    }
443
444    #[test]
445    fn alias_expansion_detects_cycle() {
446        let source_manager = DefaultSourceManager::default();
447        let mut imports = BTreeMap::<String, AliasTarget>::new();
448        imports.insert("a".to_string(), AliasTarget::Path(Span::unknown(path_arc("b"))));
449        imports.insert("b".to_string(), AliasTarget::Path(Span::unknown(path_arc("a"))));
450
451        let path = PathBuf::from_str("a").expect("valid path");
452        let result = LocalSymbolTable::expand(
453            |name| imports.get(name).cloned(),
454            Span::unknown(path.as_path()),
455            &source_manager,
456        );
457
458        assert!(matches!(result, Err(SymbolResolutionError::AliasExpansionCycle { .. })));
459    }
460
461    #[test]
462    fn alias_expansion_depth_boundary() {
463        let source_manager = DefaultSourceManager::default();
464        let mut imports = BTreeMap::<String, AliasTarget>::new();
465        let max_depth = MAX_ALIAS_EXPANSION_DEPTH;
466        for i in 0..max_depth {
467            let current = format!("a{i}");
468            let next = format!("a{}", i + 1);
469            imports.insert(current, AliasTarget::Path(Span::unknown(path_arc(&next))));
470        }
471
472        let path = PathBuf::from_str("a0").expect("valid path");
473        let result = LocalSymbolTable::expand(
474            |name| imports.get(name).cloned(),
475            Span::unknown(path.as_path()),
476            &source_manager,
477        )
478        .expect("expected depth boundary to resolve");
479
480        match result {
481            SymbolResolution::External(resolved) => {
482                let expected = format!("a{max_depth}");
483                let expected = PathBuf::from_str(&expected).expect("valid path");
484                let expected = expected.as_path().to_absolute().into_owned();
485                assert_eq!(resolved.as_deref(), expected.as_path());
486            },
487            other => panic!("expected external resolution, got {other:?}"),
488        }
489    }
490
491    #[test]
492    fn alias_expansion_depth_exceeded() {
493        let source_manager = DefaultSourceManager::default();
494        let mut imports = BTreeMap::<String, AliasTarget>::new();
495        for i in 0..=MAX_ALIAS_EXPANSION_DEPTH {
496            let current = format!("a{i}");
497            let next = format!("a{}", i + 1);
498            imports.insert(current, AliasTarget::Path(Span::unknown(path_arc(&next))));
499        }
500
501        let path = PathBuf::from_str("a0").expect("valid path");
502        let result = LocalSymbolTable::expand(
503            |name| imports.get(name).cloned(),
504            Span::unknown(path.as_path()),
505            &source_manager,
506        );
507
508        assert!(matches!(
509            result,
510            Err(SymbolResolutionError::AliasExpansionDepthExceeded { max_depth, .. })
511                if max_depth == MAX_ALIAS_EXPANSION_DEPTH
512        ));
513    }
514
515    #[test]
516    fn alias_expansion_handles_shadowed_import() {
517        let source_manager = DefaultSourceManager::default();
518        let mut imports = BTreeMap::<String, AliasTarget>::new();
519        imports.insert("lib".to_string(), AliasTarget::Path(Span::unknown(path_arc("lib"))));
520
521        let path = PathBuf::from_str("lib").expect("valid path");
522        let result = LocalSymbolTable::expand(
523            |name| imports.get(name).cloned(),
524            Span::unknown(path.as_path()),
525            &source_manager,
526        )
527        .expect("shadowed import should resolve");
528
529        match result {
530            SymbolResolution::External(resolved) => {
531                assert_eq!(resolved.as_deref(), path.as_path());
532            },
533            other => panic!("expected external resolution, got {other:?}"),
534        }
535    }
536
537    #[test]
538    fn checked_symbols_rejects_oversized_module() {
539        let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
540        let mut module =
541            crate::ast::Module::new(crate::ast::ModuleKind::Library, Path::new("::m::huge"));
542
543        for i in 0..=ItemIndex::MAX_ITEMS {
544            module.items.push(crate::ast::Export::Constant(crate::ast::Constant::new(
545                SourceSpan::UNKNOWN,
546                crate::ast::Visibility::Private,
547                Ident::new(format!("A{i}")).expect("valid identifier"),
548                crate::ast::ConstantExpr::Int(Span::unknown(crate::parser::IntValue::from(0u8))),
549            )));
550        }
551
552        let result = (&module).checked_symbols(source_manager);
553
554        assert!(matches!(result, Err(SymbolResolutionError::TooManyItemsInModule { .. })));
555    }
556
557    #[test]
558    fn checked_symbols_guard_custom_symbol_table_exact() {
559        struct ExactTooManySymbols;
560
561        impl SymbolTable for ExactTooManySymbols {
562            type SymbolIter = alloc::vec::IntoIter<LocalSymbol>;
563
564            fn symbols(&self, _source_manager: Arc<dyn SourceManager>) -> Self::SymbolIter {
565                panic!("exact construction must not request unchecked symbols")
566            }
567
568            fn checked_symbols(
569                &self,
570                source_manager: Arc<dyn SourceManager>,
571            ) -> Result<Self::SymbolIter, SymbolResolutionError> {
572                Err(SymbolResolutionError::too_many_items_in_module(
573                    SourceSpan::UNKNOWN,
574                    &*source_manager,
575                ))
576            }
577        }
578
579        let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
580        let result = LocalSymbolTable::new(ExactTooManySymbols, source_manager);
581
582        assert!(matches!(result, Err(SymbolResolutionError::TooManyItemsInModule { .. })));
583    }
584
585    #[cfg(test)]
586    struct DuplicateSymbolsForInvariantTest;
587
588    #[cfg(test)]
589    impl SymbolTable for DuplicateSymbolsForInvariantTest {
590        type SymbolIter = alloc::vec::IntoIter<LocalSymbol>;
591
592        fn symbols(&self, _source_manager: Arc<dyn SourceManager>) -> Self::SymbolIter {
593            let first = LocalSymbol::Item {
594                name: Ident::new("dup").expect("valid identifier"),
595                resolved: SymbolResolution::Local(Span::unknown(ItemIndex::new(0))),
596            };
597            let second = LocalSymbol::Item {
598                name: Ident::new("dup").expect("valid identifier"),
599                resolved: SymbolResolution::Local(Span::unknown(ItemIndex::new(1))),
600            };
601            alloc::vec![first, second].into_iter()
602        }
603    }
604
605    #[cfg(debug_assertions)]
606    #[test]
607    #[should_panic(expected = "duplicate symbol 'dup' reached local resolver construction")]
608    fn local_symbol_table_rejects_duplicate_symbols() {
609        let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
610        let _table = LocalSymbolTable::new(DuplicateSymbolsForInvariantTest, source_manager);
611    }
612
613    #[test]
614    fn local_symbol_table_duplicate_symbols_have_explicit_behavior() {
615        use std::panic::{AssertUnwindSafe, catch_unwind};
616
617        let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
618        let result = catch_unwind(AssertUnwindSafe(|| {
619            LocalSymbolTable::new(DuplicateSymbolsForInvariantTest, source_manager)
620        }));
621
622        if cfg!(debug_assertions) {
623            assert!(
624                result.is_err(),
625                "debug builds should panic when duplicates reach local resolver construction"
626            );
627        } else {
628            let table = result
629                .expect("release builds should not panic on duplicate symbols")
630                .expect("release builds should still construct a table");
631            let resolved = table
632                .get(Span::unknown("dup"))
633                .expect("release behavior should keep a deterministic symbol mapping");
634            match resolved {
635                SymbolResolution::Local(id) => assert_eq!(id.into_inner(), ItemIndex::new(0)),
636                other => panic!("expected local symbol resolution, got {other:?}"),
637            }
638        }
639    }
640}