Skip to main content

miden_assembly/linker/resolver/
symbol_resolver.rs

1use alloc::{boxed::Box, collections::BTreeSet, string::ToString, sync::Arc};
2
3use miden_assembly_syntax::{
4    ast::{
5        Alias, AliasTarget, InvocationTarget, InvokeKind, Path, SymbolResolution,
6        SymbolResolutionError,
7    },
8    debuginfo::{SourceManager, SourceSpan, Span, Spanned},
9};
10use miden_core::Word;
11
12use crate::{GlobalItemIndex, LinkerError, ModuleIndex, linker::Linker};
13
14// HELPER STRUCTS
15// ================================================================================================
16
17/// Represents the context in which symbols should be resolved.
18///
19/// A symbol may be resolved in different ways depending on where it is being referenced from, and
20/// how it is being referenced.
21#[derive(Debug, Clone)]
22pub struct SymbolResolutionContext {
23    /// The source span of the caller/referent
24    pub span: SourceSpan,
25    /// The "where", i.e. index of the caller/referent's module node in the [Linker] module graph.
26    pub module: ModuleIndex,
27    /// The "how", i.e. how the symbol is being referenced/invoked.
28    ///
29    /// This is primarily relevant for procedure invocations, particularly syscalls, as "local"
30    /// names resolve in the kernel module, _not_ in the caller's module. Non-procedure symbols are
31    /// always pure references.
32    pub kind: Option<InvokeKind>,
33}
34
35impl SymbolResolutionContext {
36    #[inline]
37    pub fn in_syscall(&self) -> bool {
38        matches!(self.kind, Some(InvokeKind::SysCall))
39    }
40}
41
42// SYMBOL RESOLVER
43// ================================================================================================
44
45/// A [SymbolResolver] is used to resolve a procedure invocation target to its concrete definition.
46///
47/// Because modules can re-export/alias the procedures of modules they import, resolving the name of
48/// a procedure can require multiple steps to reach the original concrete definition of the
49/// procedure.
50///
51/// The [SymbolResolver] encapsulates the tricky details of doing this, so that users of the
52/// resolver need only provide a reference to the [Linker], a name they wish to resolve, and some
53/// information about the caller necessary to determine the context in which the name should be
54/// resolved.
55pub struct SymbolResolver<'a> {
56    /// The graph containing already-compiled and partially-resolved modules.
57    graph: &'a Linker,
58}
59
60impl<'a> SymbolResolver<'a> {
61    /// Create a new [SymbolResolver] for the provided [Linker].
62    pub fn new(graph: &'a Linker) -> Self {
63        Self { graph }
64    }
65
66    #[inline(always)]
67    pub fn source_manager(&self) -> &dyn SourceManager {
68        &self.graph.source_manager
69    }
70
71    #[inline(always)]
72    pub fn source_manager_arc(&self) -> Arc<dyn SourceManager> {
73        self.graph.source_manager.clone()
74    }
75
76    #[inline(always)]
77    pub(crate) fn linker(&self) -> &Linker {
78        self.graph
79    }
80
81    /// Resolve `target`, a possibly-resolved symbol reference, to a [SymbolResolution], using
82    /// `context` as the context.
83    pub fn resolve_invoke_target(
84        &self,
85        context: &SymbolResolutionContext,
86        target: &InvocationTarget,
87    ) -> Result<SymbolResolution, LinkerError> {
88        let resolution = match target {
89            InvocationTarget::MastRoot(mast_root) => {
90                log::debug!(target: "name-resolver::invoke", "resolving {target}");
91                self.validate_syscall_digest(context, *mast_root)?;
92                match self.graph.get_procedure_index_by_digest(mast_root) {
93                    None => Ok(SymbolResolution::MastRoot(*mast_root)),
94                    Some(gid) if context.in_syscall() => {
95                        if self.graph.kernel_index.is_some_and(|k| k == gid.module) {
96                            Ok(SymbolResolution::Exact {
97                                gid,
98                                path: Span::new(mast_root.span(), self.item_path(gid)),
99                            })
100                        } else {
101                            Err(LinkerError::InvalidSysCallTarget {
102                                span: context.span,
103                                source_file: self
104                                    .source_manager()
105                                    .get(context.span.source_id())
106                                    .ok(),
107                                callee: self.item_path(gid),
108                            })
109                        }
110                    },
111                    Some(gid) => Ok(SymbolResolution::Exact {
112                        gid,
113                        path: Span::new(mast_root.span(), self.item_path(gid)),
114                    }),
115                }
116            },
117            InvocationTarget::Symbol(symbol) => {
118                let path = Path::from_ident(symbol);
119                let mut context = context.clone();
120                // Force the resolution context for a syscall target to be the kernel module
121                if context.in_syscall() {
122                    if let Some(kernel) = self.graph.kernel_index {
123                        context.module = kernel;
124                    } else {
125                        return Err(LinkerError::InvalidSysCallTarget {
126                            span: context.span,
127                            source_file: self.source_manager().get(context.span.source_id()).ok(),
128                            callee: Path::from_ident(symbol).into_owned().into(),
129                        });
130                    }
131                }
132                match self.resolve_path(&context, Span::new(symbol.span(), path.as_ref()))? {
133                    SymbolResolution::Module { id: _, path: module_path } => {
134                        Err(LinkerError::InvalidInvokeTarget {
135                            span: symbol.span(),
136                            source_file: self
137                                .graph
138                                .source_manager
139                                .get(symbol.span().source_id())
140                                .ok(),
141                            path: module_path.into_inner(),
142                        })
143                    },
144                    resolution => Ok(resolution),
145                }
146            },
147            InvocationTarget::Path(path) => match self.resolve_path(context, path.as_deref())? {
148                SymbolResolution::Module { id: _, path: module_path } => {
149                    Err(LinkerError::InvalidInvokeTarget {
150                        span: path.span(),
151                        source_file: self.graph.source_manager.get(path.span().source_id()).ok(),
152                        path: module_path.into_inner(),
153                    })
154                },
155                SymbolResolution::Exact { gid, path } if context.in_syscall() => {
156                    if self.graph.kernel_index.is_some_and(|k| k == gid.module) {
157                        Ok(SymbolResolution::Exact { gid, path })
158                    } else {
159                        Err(LinkerError::InvalidSysCallTarget {
160                            span: context.span,
161                            source_file: self.source_manager().get(context.span.source_id()).ok(),
162                            callee: path.into_inner(),
163                        })
164                    }
165                },
166                SymbolResolution::MastRoot(mast_root) => {
167                    self.validate_syscall_digest(context, mast_root)?;
168                    match self.graph.get_procedure_index_by_digest(&mast_root) {
169                        None => Ok(SymbolResolution::MastRoot(mast_root)),
170                        Some(gid) if context.in_syscall() => {
171                            if self.graph.kernel_index.is_some_and(|k| k == gid.module) {
172                                Ok(SymbolResolution::Exact {
173                                    gid,
174                                    path: Span::new(mast_root.span(), self.item_path(gid)),
175                                })
176                            } else {
177                                Err(LinkerError::InvalidSysCallTarget {
178                                    span: context.span,
179                                    source_file: self
180                                        .source_manager()
181                                        .get(context.span.source_id())
182                                        .ok(),
183                                    callee: self.item_path(gid),
184                                })
185                            }
186                        },
187                        Some(gid) => Ok(SymbolResolution::Exact {
188                            gid,
189                            path: Span::new(mast_root.span(), self.item_path(gid)),
190                        }),
191                    }
192                },
193                // NOTE: If we're in a syscall here, we can't validate syscall targets that are not
194                // fully resolved - but such targets will be revisited later at which point they
195                // will be checked
196                resolution => Ok(resolution),
197            },
198        }?;
199        self.enforce_kernel_export_syscall_only(context, target, resolution)
200    }
201
202    fn enforce_kernel_export_syscall_only(
203        &self,
204        context: &SymbolResolutionContext,
205        target: &InvocationTarget,
206        resolution: SymbolResolution,
207    ) -> Result<SymbolResolution, LinkerError> {
208        if matches!(target, InvocationTarget::MastRoot(_)) {
209            return Ok(resolution);
210        }
211        if let SymbolResolution::Exact { gid, ref path } = resolution
212            && context.kind.is_some()
213            && !context.in_syscall()
214        {
215            // Root kernel attached via `with_kernel` is stored as ModuleKind::Library (MAST);
216            // `kernel_index` identifies it. AST kernel modules use ModuleKind::Kernel.
217            let target_is_kernel = self.graph.kernel_index.is_some_and(|ki| ki == gid.module)
218                || self.graph[gid.module].kind().is_kernel();
219            let caller_is_kernel = self.graph.kernel_index.is_some_and(|ki| ki == context.module)
220                || self.graph[context.module].kind().is_kernel();
221            if target_is_kernel && !caller_is_kernel {
222                return Err(LinkerError::KernelProcNotSyscall {
223                    span: context.span,
224                    source_file: self.graph.source_manager.get(context.span.source_id()).ok(),
225                    callee: path.clone().into_inner(),
226                });
227            }
228        }
229        Ok(resolution)
230    }
231
232    fn validate_syscall_digest(
233        &self,
234        context: &SymbolResolutionContext,
235        mast_root: Span<Word>,
236    ) -> Result<(), LinkerError> {
237        if !context.in_syscall() {
238            return Ok(());
239        }
240        // Syscalls must be validated against an attached kernel at assembly time.
241        if !self.graph.has_nonempty_kernel() {
242            return Err(LinkerError::InvalidSysCallTarget {
243                span: context.span,
244                source_file: self.source_manager().get(context.span.source_id()).ok(),
245                callee: Arc::<Path>::from(Path::new("syscall")),
246            });
247        }
248        // Kernel digests only contain exported kernel procedures.
249        if !self.graph.kernel().contains_proc(*mast_root.inner()) {
250            let digest_path = format!("{mast_root}");
251            return Err(LinkerError::InvalidSysCallTarget {
252                span: context.span,
253                source_file: self.source_manager().get(context.span.source_id()).ok(),
254                callee: Arc::<Path>::from(Path::new(&digest_path)),
255            });
256        }
257        Ok(())
258    }
259
260    /// Resolve `target`, a possibly-resolved symbol reference, to a [SymbolResolution], using
261    /// `context` as the context.
262    pub fn resolve_alias_target(
263        &self,
264        context: &SymbolResolutionContext,
265        alias: &Alias,
266    ) -> Result<SymbolResolution, LinkerError> {
267        match alias.target() {
268            target @ AliasTarget::MastRoot(mast_root) => {
269                log::debug!(target: "name-resolver::alias", "resolving alias target {target}");
270                match self.graph.get_procedure_index_by_digest(mast_root) {
271                    None => Ok(SymbolResolution::MastRoot(*mast_root)),
272                    Some(gid) => Ok(SymbolResolution::Exact {
273                        gid,
274                        path: Span::new(mast_root.span(), self.item_path(gid)),
275                    }),
276                }
277            },
278            AliasTarget::Path(path) => {
279                log::debug!(target: "name-resolver::alias", "resolving alias target '{path}'");
280                // We ensure that we do not unintentionally recursively expand an alias target using
281                // its own definition, e.g. with something like `use lib::lib` which without this,
282                // will expand until the stack blows
283                let mut ignored_imports = BTreeSet::from_iter([alias.name().clone().into_inner()]);
284                self.expand_path(context, path.as_deref(), &mut ignored_imports)
285            },
286        }
287    }
288
289    pub fn resolve_path(
290        &self,
291        context: &SymbolResolutionContext,
292        path: Span<&Path>,
293    ) -> Result<SymbolResolution, LinkerError> {
294        let mut ignored_imports = BTreeSet::default();
295        self.expand_path(context, path, &mut ignored_imports)
296    }
297
298    fn expand_path(
299        &self,
300        context: &SymbolResolutionContext,
301        path: Span<&Path>,
302        ignored_imports: &mut BTreeSet<Arc<str>>,
303    ) -> Result<SymbolResolution, LinkerError> {
304        self.expand_path_from(context.module, context, path, ignored_imports)
305    }
306
307    fn expand_path_from(
308        &self,
309        origin_module: ModuleIndex,
310        context: &SymbolResolutionContext,
311        path: Span<&Path>,
312        ignored_imports: &mut BTreeSet<Arc<str>>,
313    ) -> Result<SymbolResolution, LinkerError> {
314        let span = path.span();
315        let mut path = path.into_inner();
316        let mut context = context.clone();
317        loop {
318            log::debug!(target: "name-resolver::expand", "expanding path '{path}' (absolute = {})", path.is_absolute());
319            if path.is_absolute() {
320                // An absolute path does not reference any aliases in the current module, but may
321                // refer to aliases in any of its non-root components.
322                //
323                // However, if the root component of the path is not a known module, then we have to
324                // proceed as if an actual module exists, just one that incorporates more components
325                // of the path than just the root.
326                //
327                // To speed this up, we search for a matching "longest-prefix" of `path` in the
328                // global module list. If we find an exact match, we're done. If we
329                // find a partial match, then we resolve the rest of `path` relative
330                // to that partial match. If we cannot find any match at all, then
331                // the path references an undefined module
332                let mut longest_prefix: Option<(ModuleIndex, Arc<Path>)> = None;
333                for module in self.graph.modules.iter() {
334                    let module_path = module.path().clone();
335                    if path == &*module_path {
336                        log::debug!(target: "name-resolver::expand", "found exact match for '{path}': id={}", module.id());
337                        return Ok(SymbolResolution::Module {
338                            id: module.id(),
339                            path: Span::new(span, module_path),
340                        });
341                    }
342
343                    if path.starts_with_exactly(module_path.as_ref()) {
344                        if let Some((_, prev)) = longest_prefix.as_ref() {
345                            let prev_len = prev.components().count();
346                            let module_len = module_path.components().count();
347                            if prev_len < module_len {
348                                longest_prefix = Some((module.id(), module_path));
349                            } else if prev_len == module_len && prev != &module_path {
350                                return Err(LinkerError::AmbiguousModulePath {
351                                    span,
352                                    source_file: self.source_manager().get(span.source_id()).ok(),
353                                    path: path.to_path_buf().into_boxed_path().into(),
354                                    matches:
355                                        alloc::vec![prev.to_string(), module_path.to_string(),]
356                                            .into_boxed_slice(),
357                                });
358                            }
359                        } else {
360                            longest_prefix = Some((module.id(), module_path));
361                        }
362                    }
363                }
364
365                match longest_prefix {
366                    // We found a module with a common prefix, attempt to expand the subpath of
367                    // `path` relative to that module. If this fails, the path
368                    // is an undefined reference.
369                    Some((module_id, module_path)) => {
370                        log::trace!(target: "name-resolver::expand", "found prefix match for '{path}': id={module_id}, prefix={module_path}");
371                        let subpath = path.strip_prefix(&module_path).unwrap();
372                        context.module = module_id;
373                        ignored_imports.clear();
374                        path = subpath;
375                    },
376                    // No matching module paths found, path is undefined symbol reference
377                    None => {
378                        log::trace!(target: "name-resolver::expand", "no prefix match found for '{path}' - path is undefined symbol reference");
379                        break Err(
380                            SymbolResolutionError::undefined(span, self.source_manager()).into()
381                        );
382                    },
383                }
384            } else if let Some(symbol) = path.as_ident() {
385                // This is a reference to a symbol in the current module, possibly imported.
386                //
387                // We first resolve the symbol in the local module to either a local definition, or
388                // an imported symbol.
389                //
390                // If the symbol is locally-defined, the expansion is the join of the current module
391                // path and the symbol name.
392                //
393                // If the symbol is an imported item, then we expand the imported path recursively.
394                break match self
395                    .resolve_local_with_index(context.module, Span::new(span, symbol.as_str()))?
396                {
397                    SymbolResolution::Local(item) => {
398                        log::trace!(target: "name-resolver::expand", "resolved '{symbol}' to local symbol: {}", context.module + item.into_inner());
399                        let gid = context.module + item.into_inner();
400                        self.ensure_item_visible(origin_module, gid, span)?;
401                        let path = self.module_path(context.module).join(&symbol);
402                        Ok(SymbolResolution::Exact { gid, path: Span::new(span, path.into()) })
403                    },
404                    SymbolResolution::External(path) => {
405                        log::trace!(target: "name-resolver::expand", "expanded '{symbol}' to unresolved external path '{path}'");
406                        self.expand_path_from(
407                            origin_module,
408                            &context,
409                            path.as_deref(),
410                            ignored_imports,
411                        )
412                    },
413                    resolved @ SymbolResolution::MastRoot(_) => Ok(resolved),
414                    SymbolResolution::Exact { gid, path } => {
415                        log::trace!(target: "name-resolver::expand", "resolved '{symbol}' to exact definition");
416                        self.ensure_item_visible(origin_module, gid, span)?;
417                        Ok(SymbolResolution::Exact { gid, path })
418                    },
419                    SymbolResolution::Module { id, path: module_path } => {
420                        log::trace!(target: "name-resolver::expand", "resolved '{symbol}' to module: id={id} path={module_path}");
421                        Ok(SymbolResolution::Module { id, path: module_path })
422                    },
423                };
424            } else {
425                // A relative path can be expressed in four forms:
426                //
427                // 1. A reference to a symbol in the current module (possibly imported)
428                // 2. A reference to a symbol relative to an imported module, e.g. `push.mod::CONST`
429                // 3. A reference to a nested symbol relative to an imported module, e.g.
430                //    `push.mod::submod::CONST`
431                // 4. An absolute path expressed relatively, e.g. `push.root::mod::submod::CONST`,
432                //    which should have been expressed as `push.::root::mod::submod::CONST`, but the
433                //    `::` prefix was omitted/forgotten.
434                //
435                // 1 and 4 are easy to handle (4 is technically a degenerate edge case of 3, but has
436                // an easy fallback path).
437                //
438                // 3 is where all the complexity of relative paths comes in, because it requires us
439                // to recursively expand paths until we cannot do so any longer, and then resolve
440                // the originally referenced symbol relative to that expanded path (which may
441                // require further recursive expansion).
442                //
443                // We start by expecting that a relative path refers to an import in the current
444                // module: if this is not the case, then we fall back to attempting to resolve the
445                // path as if it was absolute. If this fails, the path is considered to refer to an
446                // undefined symbol.
447                //
448                // If the path is relative to an import in the current module, then we proceed by
449                // resolving the subpath relative to the import target. This is the recursive part,
450                // and the result of this recursive expansion is what gets returned from this
451                // function.
452                let (imported_symbol, subpath) = path.split_first().expect("multi-component path");
453                if ignored_imports.contains(imported_symbol) {
454                    log::trace!(target: "name-resolver::expand", "skipping import expansion of '{imported_symbol}': already expanded, resolving as absolute path instead");
455                    let path = path.to_absolute();
456                    break self.expand_path_from(
457                        origin_module,
458                        &context,
459                        Span::new(span, path.as_ref()),
460                        ignored_imports,
461                    );
462                }
463                match self
464                    .resolve_local_with_index(context.module, Span::new(span, imported_symbol))
465                {
466                    Ok(SymbolResolution::Local(item)) => {
467                        log::trace!(target: "name-resolver::expand", "cannot expand '{path}': path is relative to local definition");
468                        // This is a conflicting symbol reference that we would've expected to be
469                        // caught during semantic analysis. Raise a
470                        // diagnostic here telling the user what's wrong
471                        break Err(SymbolResolutionError::invalid_sub_path(
472                            span,
473                            item.span(),
474                            self.source_manager(),
475                        )
476                        .into());
477                    },
478                    Ok(SymbolResolution::Exact { path: item, .. }) => {
479                        log::trace!(target: "name-resolver::expand", "cannot expand '{path}': path is relative to item at '{item}'");
480                        // This is a conflicting symbol reference that we would've expected to be
481                        // caught during semantic analysis. Raise a
482                        // diagnostic here telling the user what's wrong
483                        break Err(SymbolResolutionError::invalid_sub_path(
484                            span,
485                            item.span(),
486                            self.source_manager(),
487                        )
488                        .into());
489                    },
490                    Ok(SymbolResolution::MastRoot(item)) => {
491                        log::trace!(target: "name-resolver::expand", "cannot expand '{path}': path is relative to imported procedure root");
492                        // This is a conflicting symbol reference that we would've expected to be
493                        // caught during semantic analysis. Raise a
494                        // diagnostic here telling the user what's wrong
495                        break Err(SymbolResolutionError::invalid_sub_path(
496                            span,
497                            item.span(),
498                            self.source_manager(),
499                        )
500                        .into());
501                    },
502                    Ok(SymbolResolution::Module { id, path: module_path }) => {
503                        log::trace!(target: "name-resolver::expand", "expanded import '{imported_symbol}' to module: id={id} path={module_path}");
504                        // We've resolved the import to a known module, resolve `subpath` relative
505                        // to it
506                        context.module = id;
507                        ignored_imports.clear();
508                        path = subpath;
509                    },
510                    Ok(SymbolResolution::External(external_path)) => {
511                        // We've resolved the imported symbol to an external path, but we don't know
512                        // if that path is valid or not. Attempt to expand
513                        // the full path produced by joining `subpath` to
514                        // `external_path` and resolving in the context of the
515                        // current module
516                        log::trace!(target: "name-resolver::expand", "expanded import '{imported_symbol}' to unresolved external path '{external_path}'");
517                        let partially_expanded = external_path.join(subpath);
518                        log::trace!(target: "name-resolver::expand", "partially expanded '{path}' to '{partially_expanded}'");
519                        ignored_imports.insert(imported_symbol.to_string().into_boxed_str().into());
520                        break self.expand_path_from(
521                            origin_module,
522                            &context,
523                            Span::new(span, partially_expanded.as_path()),
524                            ignored_imports,
525                        );
526                    },
527                    Err(err)
528                        if matches!(
529                            err.as_ref(),
530                            SymbolResolutionError::UndefinedSymbol { .. }
531                        ) =>
532                    {
533                        // Try to expand the path by treating it as an absolute path
534                        let absolute = path.to_absolute();
535                        log::trace!(target: "name-resolver::expand", "no import found for '{imported_symbol}' in '{path}': attempting to resolve as absolute path instead");
536                        break self.expand_path_from(
537                            origin_module,
538                            &context,
539                            Span::new(span, absolute.as_ref()),
540                            ignored_imports,
541                        );
542                    },
543                    Err(err) => {
544                        log::trace!(target: "name-resolver::expand", "expansion failed due to symbol resolution error");
545                        break Err(err.into());
546                    },
547                }
548            }
549        }
550    }
551
552    fn ensure_item_visible(
553        &self,
554        origin_module: ModuleIndex,
555        item: GlobalItemIndex,
556        span: SourceSpan,
557    ) -> Result<(), LinkerError> {
558        if origin_module == item.module {
559            return Ok(());
560        }
561
562        let symbol = &self.graph[item.module][item.index];
563        if symbol.visibility().is_public() {
564            return Ok(());
565        }
566
567        Err(SymbolResolutionError::private_symbol(
568            span,
569            symbol.name().span(),
570            self.source_manager(),
571        )
572        .into())
573    }
574
575    pub fn resolve_local(
576        &self,
577        context: &SymbolResolutionContext,
578        symbol: &str,
579    ) -> Result<SymbolResolution, Box<SymbolResolutionError>> {
580        let module = if context.in_syscall() {
581            // Resolve local names relative to the kernel
582            match self.graph.kernel_index {
583                Some(kernel) => kernel,
584                None => {
585                    return Err(Box::new(SymbolResolutionError::UndefinedSymbol {
586                        span: context.span,
587                        source_file: self.source_manager().get(context.span.source_id()).ok(),
588                    }));
589                },
590            }
591        } else {
592            context.module
593        };
594        self.resolve_local_with_index(module, Span::new(context.span, symbol))
595    }
596
597    fn resolve_local_with_index(
598        &self,
599        module: ModuleIndex,
600        symbol: Span<&str>,
601    ) -> Result<SymbolResolution, Box<SymbolResolutionError>> {
602        let module = &self.graph[module];
603        log::debug!(target: "name-resolver::local", "resolving '{symbol}' in module {}", module.path());
604        log::debug!(target: "name-resolver::local", "module status: {:?}", module.status());
605        module.resolve(symbol, self)
606    }
607
608    #[inline]
609    pub fn module_path(&self, module: ModuleIndex) -> &Path {
610        self.graph[module].path()
611    }
612
613    pub fn item_path(&self, item: GlobalItemIndex) -> Arc<Path> {
614        let module = &self.graph[item.module];
615        let name = module[item.index].name();
616        module.path().join(name).into()
617    }
618}