Skip to main content

miden_assembly/linker/
mod.rs

1//! Assembly of a Miden Assembly project is comprised of four phases:
2//!
3//! 1. _Parsing_, where MASM sources are parsed into the AST data structure. Some light validation
4//!    is done in this phase, to catch invalid syntax, invalid immediate values (e.g. overflow), and
5//!    other simple checks that require little to no reasoning about surrounding context.
6//! 2. _Semantic analysis_, where initial validation of the AST is performed. This step catches
7//!    unused imports, references to undefined local symbols, orphaned doc comments, and other
8//!    checks that only require minimal module-local context. Initial symbol resolution is performed
9//!    here based on module-local context, as well as constant folding of expressions that can be
10//!    resolved locally. Symbols which refer to external items are unable to be fully processed as
11//!    part of this phase, and is instead left to the linking phase.
12//! 3. _Linking_, the most critical phase of compilation. During this phase, the assembler has the
13//!    full compilation graph available to it, and so this is where inter-module symbol references
14//!    are finally able to be resolved (or not, in which case appropriate errors are raised). This
15//!    is the phase where we catch cyclic references, references to undefined symbols, references to
16//!    non-public symbols from other modules, etc. Once all symbols are linked, the assembler is
17//!    free to compile all of the procedures to MAST, and generate a [crate::package::Package].
18//! 4. _Assembly_, the final phase, where all of the linked items provided to the assembler are
19//!    lowered to MAST, or to their final representations in the [crate::package::Package] produced
20//!    as the output of assembly. During this phase, it is expected that the compilation graph has
21//!    been validated by the linker, and we're simply processing the conversion to MAST.
22//!
23//! This module provides the implementation of the linker and its associated data structures. There
24//! are three primary parts:
25//!
26//! 1. The _call graph_, this is what tracks dependencies between procedures in the compilation
27//!    graph, and is used to ensure that all procedure references can be resolved to a MAST root
28//!    during final assembly.
29//! 2. The _symbol resolver_, this is what is responsible for computing symbol resolutions using
30//!    context-sensitive details about how a symbol is referenced. This context sensitivity is how
31//!    we are able to provide better diagnostics when invalid references are found. The resolver
32//!    shares part of it's implementation with the same infrastructure used for symbol resolution
33//!    that is performed during semantic analysis - the difference is that at link-time, we are
34//!    stricter about what happens when a symbol cannot be resolved correctly.
35//! 3. A set of _rewrites_, applied to symbols/modules at link-time, which rewrite the AST so that
36//!    all symbol references and constant expressions are fully resolved/folded. This is where any
37//!    final issues are discovered, and the AST is prepared for lowering to MAST.
38mod callgraph;
39mod debug;
40mod errors;
41mod library;
42mod module;
43pub mod namespaces;
44mod resolver;
45mod rewrites;
46mod symbols;
47
48use alloc::{
49    boxed::Box,
50    collections::{BTreeMap, BTreeSet},
51    string::{String, ToString},
52    sync::Arc,
53    vec::Vec,
54};
55use core::{
56    cell::RefCell,
57    ops::{ControlFlow, Index},
58};
59
60use miden_assembly_syntax::{
61    Report,
62    ast::{
63        self, AttributeSet, GlobalItemIndex, InvocationTarget, ItemIndex, Module, ModuleIndex,
64        Path, SymbolResolution, Visibility, types,
65    },
66    debuginfo::{SourceManager, SourceSpan, Span, Spanned},
67    module::{ItemInfo, ModuleDescriptor},
68};
69use miden_core::{Word, advice::AdviceMap, program::KernelDescriptor};
70use miden_mast_package::Package as MastPackage;
71use smallvec::{SmallVec, smallvec};
72
73pub use self::{
74    callgraph::{CallGraph, CycleError},
75    errors::LinkerError,
76    library::{LinkLibrary, Linkage},
77    namespaces::NamespaceGraph,
78    resolver::{ResolverCache, SymbolResolutionContext, SymbolResolver},
79    symbols::{Import, Symbol, SymbolItem},
80};
81use self::{
82    module::{LinkModule, ModuleSource},
83    namespaces::ResolvedImports,
84    resolver::*,
85};
86
87/// Selects how [`Linker::link`] handles a static cycle in the call graph.
88///
89/// MAST procedures must be built from callees to callers, so a static cycle (recursion that does
90/// not go through a `dynexec`) prevents MAST from being generated. Assembly therefore rejects
91/// static cycles by default (see [`Self::Strict`]). Lint analysis does not need MAST and can
92/// instead skip the cycle and its callers, so it links in [`Self::Analysis`] mode: the resolved
93/// modules and call edges are committed and the cycle is reported as a nonfatal diagnostic.
94#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
95pub enum LinkMode {
96    /// Reject any static cycle in the call graph, rolling back all pending changes.
97    ///
98    /// This is the default and is required before MAST can be built.
99    #[default]
100    Strict,
101    /// Commit resolved modules and call edges even when a static cycle is found, and report the
102    /// cycle as a nonfatal diagnostic.
103    ///
104    /// Unresolved imports, unresolved calls, and failed rewrites remain fatal errors in this mode;
105    /// only the final cycle check is nonfatal.
106    Analysis,
107}
108
109/// The nonfatal outcome of a [`LinkMode::Analysis`] link.
110///
111/// Unlike [`Linker::link`], analysis linking does not reject a static recursion cycle. Instead it
112/// commits the resolved modules and call edges, and reports the cycle so the caller can skip the
113/// cycle (and every caller that depends on it) and continue analyzing the rest of the project.
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct LinkAnalysis {
116    /// The module indices that comprise the public interface of the assembled artifact,
117    /// determined by tracing the modules reachable from the roots via their public submodules.
118    pub module_indices: Vec<ModuleIndex>,
119    /// The procedures that participate in a static recursion cycle in the call graph, reported as
120    /// fully-qualified procedure paths (`module::proc`). Empty when the call graph is acyclic.
121    pub cycle: Box<[String]>,
122}
123
124impl LinkAnalysis {
125    /// Returns `true` when the analysis link found a static recursion cycle.
126    pub fn has_cycle(&self) -> bool {
127        !self.cycle.is_empty()
128    }
129}
130
131/// Represents the current status of a symbol in the state of the [Linker]
132#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
133pub enum LinkStatus {
134    /// The module or item has not been visited by the linker
135    #[default]
136    Unlinked,
137    /// The module or item has been visited by the linker, but still refers to one or more
138    /// unresolved symbols.
139    PartiallyLinked,
140    /// The module or item has been visited by the linker, and is fully linked and resolved
141    Linked,
142}
143
144// LINKER
145// ================================================================================================
146
147/// The [`Linker`] is responsible for analyzing the input modules and libraries provided to the
148/// assembler, and _linking_ them together.
149///
150/// The core conceptual data structure of the linker is the _module graph_, which is implemented
151/// by a vector of module nodes, and a _call graph_, which is implemented as an adjacency matrix
152/// of item nodes and the outgoing edges from those nodes, representing references from that item
153/// to another symbol (typically as the result of procedure invocation, hence "call" graph).
154///
155/// Each item/symbol known to the linker is given a _global item index_, which is actually a pair
156/// of indices: a _module index_ (which indexes into the vector of module nodes), and an _item
157/// index_ (which indexes into the items defined by a module). These global item indices function
158/// as a unique identifier within the linker, to a specific item, and can be resolved to either the
159/// original syntax tree of the item, or to metadata about the item retrieved from previously-
160/// assembled MAST.
161///
162/// The process of linking involves two phases:
163///
164/// 1. Setting up the linker context, by providing the set of inputs to link together
165/// 2. Analyzing and rewriting the symbols known to the linker, as needed, to ensure that all symbol
166///    references are resolved to concrete definitions.
167///
168/// The assembler will call [`Self::link`] once it has provided all inputs that it wants to link,
169/// which will, when successful, return the set of module indices corresponding to the modules that
170/// comprise the public interface of the assembled artifact. The assembler then constructs the MAST
171/// starting from the exported procedures of those modules, recursively tracing the call graph
172/// based on whether or not the callee is statically or dynamically linked. In the static linking
173/// case, any procedures referenced in a statically-linked library or module will be included in
174/// the assembled artifact. In the dynamic linking case, referenced procedures are instead
175/// referenced in the assembled artifact only by their MAST root.
176#[derive(Clone)]
177pub struct Linker {
178    /// The set of libraries to link against.
179    libraries: BTreeMap<Word, LinkLibrary>,
180    /// The statically linked libraries to pass to MAST forest construction.
181    ///
182    /// This index is keyed by full MAST forest commitment, not package commitment, so static
183    /// libraries with the same exported procedure roots but different stored advice are
184    /// retained.
185    static_libraries: BTreeMap<Word, LinkLibrary>,
186    /// The global set of items known to the linker
187    modules: Vec<LinkModule>,
188    /// The global call graph of calls, not counting those that are performed directly via MAST
189    /// root.
190    callgraph: CallGraph,
191    /// The set of MAST roots which have procedure definitions in this graph. There can be
192    /// multiple procedures bound to the same root due to having identical code.
193    procedures_by_mast_root: BTreeMap<Word, SmallVec<[GlobalItemIndex; 1]>>,
194    /// The index of the kernel module in `modules`, if present
195    kernel_index: Option<ModuleIndex>,
196    /// The kernel library being linked against.
197    ///
198    /// This is always provided, with an empty kernel being the default.
199    kernel: KernelDescriptor,
200    kernel_package: Option<Arc<MastPackage>>,
201    /// The source manager to use when emitting diagnostics.
202    source_manager: Arc<dyn SourceManager>,
203}
204
205// ------------------------------------------------------------------------------------------------
206/// Constructors
207impl Linker {
208    /// Instantiate a new [Linker], using the provided [SourceManager] to resolve source info.
209    pub fn new(source_manager: Arc<dyn SourceManager>) -> Self {
210        Self {
211            libraries: Default::default(),
212            static_libraries: Default::default(),
213            modules: Default::default(),
214            callgraph: Default::default(),
215            procedures_by_mast_root: Default::default(),
216            kernel_index: None,
217            kernel: Default::default(),
218            kernel_package: None,
219            source_manager,
220        }
221    }
222
223    /// Registers `library` and all of its modules with the linker, according to its linkage
224    pub fn link_library(&mut self, library: LinkLibrary) -> Result<(), LinkerError> {
225        use alloc::collections::btree_map::Entry;
226
227        let module_descriptors = library.module_descriptors().map_err(|err| {
228            LinkerError::InvalidPackageModuleSurface {
229                package: library.package.name.to_string(),
230                reason: err.to_string(),
231            }
232        })?;
233        let library_interface_digest = library.interface_commitment().map_err(|err| {
234            LinkerError::InvalidPackageModuleSurface {
235                package: library.package.name.to_string(),
236                reason: err.to_string(),
237            }
238        })?;
239
240        let static_library = matches!(library.linkage, Linkage::Static).then(|| library.clone());
241        let result = match self.libraries.entry(library_interface_digest) {
242            Entry::Vacant(entry) => {
243                entry.insert(library);
244                self.link_assembled_modules(module_descriptors)
245            },
246            Entry::Occupied(mut entry) => {
247                let prev = entry.get_mut();
248
249                // If the same library is linked both dynamically and statically, prefer static
250                // linking always.
251                if matches!(prev.linkage, Linkage::Dynamic) {
252                    prev.linkage = library.linkage;
253                }
254
255                Ok(())
256            },
257        };
258
259        if result.is_ok()
260            && let Some(static_library) = static_library
261        {
262            self.static_libraries
263                .entry(static_library.commitment())
264                .or_insert(static_library);
265        }
266
267        result
268    }
269
270    /// Registers a set of MAST modules with the linker.
271    ///
272    /// If called directly, the modules will default to being dynamically linked. You must use
273    /// [`Self::link_library`] if you wish to statically link a set of assembled modules.
274    pub fn link_assembled_modules(
275        &mut self,
276        modules: impl IntoIterator<Item = ModuleDescriptor>,
277    ) -> Result<(), LinkerError> {
278        for module in modules {
279            self.link_assembled_module(module)?;
280        }
281
282        Ok(())
283    }
284
285    /// Registers a MAST module with the linker.
286    ///
287    /// If called directly, the module will default to being dynamically linked. You must use
288    /// [`Self::link_library`] if you wish to statically link `module`.
289    pub fn link_assembled_module(
290        &mut self,
291        module: ModuleDescriptor,
292    ) -> Result<ModuleIndex, LinkerError> {
293        log::debug!(target: "linker", "adding pre-assembled module {} to module graph", module.path());
294
295        let module_path = module.path();
296        let is_duplicate = self.find_module_index(module_path).is_some();
297        if is_duplicate {
298            return Err(LinkerError::DuplicateModule {
299                path: module_path.to_path_buf().into_boxed_path().into(),
300            });
301        }
302
303        let module_index = self.next_module_id();
304        let submodules = module.submodules().to_vec();
305        let items = module.items();
306        let mut symbols = Vec::with_capacity(items.len());
307        for (idx, item) in items {
308            let gid = module_index + idx;
309            self.callgraph.get_or_insert_node(gid);
310            match &item {
311                ItemInfo::Procedure(item) => {
312                    self.register_procedure_root(gid, item.digest);
313                },
314                ItemInfo::Constant(_) | ItemInfo::Type(_) => (),
315            }
316            symbols.push(Symbol::new(
317                item.name().clone(),
318                Visibility::Public,
319                LinkStatus::Linked,
320                SymbolItem::Compiled(item.clone()),
321            ));
322        }
323
324        let link_module = LinkModule::new(
325            module_index,
326            ast::ModuleKind::Library,
327            LinkStatus::Linked,
328            ModuleSource::Mast,
329            module_path.into(),
330        )
331        .with_submodules(submodules)
332        .with_symbols(symbols);
333
334        self.modules.push(link_module);
335        Ok(module_index)
336    }
337
338    /// Registers a set of AST modules with the linker.
339    ///
340    /// See [`Self::link_module`] for more details.
341    pub fn link_modules(
342        &mut self,
343        modules: impl IntoIterator<Item = Box<Module>>,
344    ) -> Result<Vec<ModuleIndex>, LinkerError> {
345        modules.into_iter().map(|mut m| self.link_module(&mut m)).collect()
346    }
347
348    /// Registers an AST module with the linker.
349    ///
350    /// A module provided to this method is presumed to be dynamically linked, unless specifically
351    /// handled otherwise by the assembler. In particular, the assembler will only statically link
352    /// the set of AST modules provided to [`Self::link`], as they are expected to comprise the
353    /// public interface of the assembled artifact.
354    ///
355    /// # Errors
356    ///
357    /// This operation can fail for the following reasons:
358    ///
359    /// * Module with same [Path] is in the graph already
360    /// * Too many modules in the graph
361    ///
362    /// # Panics
363    ///
364    /// This function will panic if the number of modules exceeds the maximum representable
365    /// [ModuleIndex] value, `u16::MAX`.
366    pub fn link_module(&mut self, module: &mut Module) -> Result<ModuleIndex, LinkerError> {
367        log::debug!(target: "linker", "adding unprocessed module {}", module.path());
368
369        let is_duplicate = self.find_module_index(module.path()).is_some();
370        if is_duplicate {
371            return Err(LinkerError::DuplicateModule { path: module.path().into() });
372        }
373
374        let module_index = self.next_module_id();
375        let submodules = module.submodules().to_vec();
376        let mut symbols = Vec::new();
377        let imports = module.take_imports().into_iter().map(Import::new).collect::<Vec<_>>();
378        for item in module.take_items() {
379            match item {
380                ast::Item::Type(item) => {
381                    let gid = module_index + ItemIndex::new(symbols.len());
382                    self.callgraph.get_or_insert_node(gid);
383                    symbols.push(Symbol::new(
384                        item.name().clone(),
385                        item.visibility(),
386                        LinkStatus::Unlinked,
387                        SymbolItem::Type(item),
388                    ));
389                },
390                ast::Item::Constant(item) => {
391                    let gid = module_index + ItemIndex::new(symbols.len());
392                    self.callgraph.get_or_insert_node(gid);
393                    symbols.push(Symbol::new(
394                        item.name().clone(),
395                        item.visibility,
396                        LinkStatus::Unlinked,
397                        SymbolItem::Constant(item),
398                    ));
399                },
400                ast::Item::Procedure(item) => {
401                    let gid = module_index + ItemIndex::new(symbols.len());
402                    self.callgraph.get_or_insert_node(gid);
403                    symbols.push(Symbol::new(
404                        item.name().clone().into(),
405                        item.visibility(),
406                        LinkStatus::Unlinked,
407                        SymbolItem::Procedure(RefCell::new(Box::new(item))),
408                    ));
409                },
410            }
411        }
412        let link_module = LinkModule::new(
413            module_index,
414            module.kind(),
415            LinkStatus::Unlinked,
416            ModuleSource::Ast,
417            module.path().into(),
418        )
419        .with_advice_map(module.advice_map().clone())
420        .with_submodules(submodules)
421        .with_imports(imports)
422        .with_symbols(symbols);
423
424        self.modules.push(link_module);
425        Ok(module_index)
426    }
427
428    #[inline]
429    fn next_module_id(&self) -> ModuleIndex {
430        ModuleIndex::new(self.modules.len())
431    }
432}
433
434// ------------------------------------------------------------------------------------------------
435/// Kernels
436impl Linker {
437    /// Returns a new [Linker] instantiated from the provided kernel and kernel info module.
438    ///
439    /// Note: it is assumed that kernel and kernel_module are consistent, but this is not checked.
440    pub fn with_kernel(
441        source_manager: Arc<dyn SourceManager>,
442        kernel_package: Arc<MastPackage>,
443    ) -> Result<Self, Report> {
444        log::debug!(target: "linker", "instantiating linker with kernel package {}@{}", kernel_package.name, kernel_package.version);
445
446        let mut linker = Self::new(source_manager);
447        linker.link_with_kernel(kernel_package)?;
448
449        Ok(linker)
450    }
451
452    /// Add a kernel to the linker after the linker is initially constructed.
453    ///
454    /// This cannot cause any issues with modules already added to the linker (if any), as they
455    /// cannot have directly depended on the kernel, or an error would have been raised.
456    ///
457    /// This will panic if the kernel is empty, or the provided kernel module info is not valid for
458    /// a kernel.
459    pub fn link_with_kernel(&mut self, kernel_package: Arc<MastPackage>) -> Result<(), Report> {
460        if !kernel_package.is_kernel() {
461            return Err(Report::msg("invalid kernel package: not a kernel"));
462        }
463        let kernel = kernel_package.to_kernel_descriptor()?;
464        if kernel.is_empty() {
465            return Err(Report::msg("invalid kernel package: kernel cannot be empty"));
466        }
467        assert!(self.kernel.is_empty());
468        assert!(self.kernel_package.is_none());
469
470        log::debug!(target: "linker", "modifying linker with kernel package {}@{}", kernel_package.name, kernel_package.version);
471
472        let mut kernel_index = None;
473        let module_descriptors = kernel_package.try_module_descriptors().map_err(|err| {
474            LinkerError::InvalidPackageModuleSurface {
475                package: kernel_package.name.to_string(),
476                reason: err.to_string(),
477            }
478        })?;
479        for module_descriptor in module_descriptors {
480            let is_kernel_module = module_descriptor.path().is_kernel_path();
481            let module_index = self.link_assembled_module(module_descriptor)?;
482            if is_kernel_module {
483                kernel_index = Some(module_index);
484            }
485        }
486        assert!(kernel_index.is_some());
487
488        self.kernel_index = kernel_index;
489        self.kernel = kernel;
490        self.kernel_package = Some(kernel_package);
491
492        Ok(())
493    }
494
495    pub fn kernel(&self) -> &KernelDescriptor {
496        &self.kernel
497    }
498
499    pub fn kernel_package(&self) -> Option<Arc<MastPackage>> {
500        self.kernel_package.clone()
501    }
502
503    pub fn has_nonempty_kernel(&self) -> bool {
504        self.kernel_index.is_some() || !self.kernel.is_empty()
505    }
506}
507
508// ------------------------------------------------------------------------------------------------
509/// Analysis
510impl Linker {
511    fn cycle_error(&self, cycle: CycleError) -> LinkerError {
512        LinkerError::Cycle { nodes: self.cycle_procedure_paths(cycle) }
513    }
514
515    /// Formats the procedures participating in `cycle` as fully-qualified paths (`module::proc`).
516    fn cycle_procedure_paths(&self, cycle: CycleError) -> Box<[String]> {
517        cycle
518            .into_node_ids()
519            .map(|node| {
520                let module = self[node.module].path();
521                let item = self[node].name();
522                module.join(item).to_string()
523            })
524            .collect::<Vec<String>>()
525            .into_boxed_slice()
526    }
527
528    /// Links the modules in `roots` and `support` using the current state of the linker.
529    ///
530    /// Returns the module indices corresponding to the public interface of the final assembled
531    /// artifact. This is determined by tracing the modules reachable from `roots` via their public
532    /// submodules. Any module in the graph reachable this way is returned as part of the public
533    /// interface.
534    ///
535    /// This links in [`LinkMode::Strict`]: any static cycle in the call graph is a fatal error. Use
536    /// [`Self::link_analysis`] to keep the resolved graph and report a cycle as a nonfatal
537    /// diagnostic instead.
538    pub fn link(
539        &mut self,
540        roots: impl IntoIterator<Item = Box<Module>>,
541        support: impl IntoIterator<Item = Box<Module>>,
542    ) -> Result<Vec<ModuleIndex>, LinkerError> {
543        use alloc::collections::BTreeSet;
544
545        let root_indices = self.link_modules(roots)?;
546        let _support_indices = self.link_modules(support)?;
547        let namespaces = NamespaceGraph::build(self)?;
548        let imports = namespaces.resolve_imports(self)?;
549
550        self.link_and_rewrite(&namespaces, &imports, LinkMode::Strict)?;
551
552        let mut reachable = BTreeSet::new();
553
554        for root in root_indices {
555            reachable.extend(namespaces.reachable_from_root(root));
556        }
557
558        Ok(reachable.into_iter().collect())
559    }
560
561    /// Links the modules in `roots` and `support` in [`LinkMode::Analysis`].
562    ///
563    /// Unlike [`Self::link`], this does not reject a static recursion cycle. Instead it commits the
564    /// resolved modules and call edges, and returns the cycle as a nonfatal diagnostic so the
565    /// caller can skip the cycle (and every caller that depends on it) and continue analyzing the
566    /// rest of the project.
567    ///
568    /// Unresolved imports, unresolved calls, and failed rewrites remain fatal errors and are
569    /// returned as `Err`.
570    pub fn link_analysis(
571        &mut self,
572        roots: impl IntoIterator<Item = Box<Module>>,
573        support: impl IntoIterator<Item = Box<Module>>,
574    ) -> Result<LinkAnalysis, LinkerError> {
575        use alloc::collections::BTreeSet;
576
577        let root_indices = self.link_modules(roots)?;
578        let _support_indices = self.link_modules(support)?;
579        let namespaces = NamespaceGraph::build(self)?;
580        let imports = namespaces.resolve_imports(self)?;
581
582        let cycle = self.link_and_rewrite(&namespaces, &imports, LinkMode::Analysis)?;
583
584        let module_indices = {
585            let mut reachable = BTreeSet::new();
586            for root in root_indices {
587                reachable.extend(namespaces.reachable_from_root(root));
588            }
589            reachable.into_iter().collect::<Vec<_>>()
590        };
591
592        let cycle = match cycle {
593            Some(cycle) => self.cycle_procedure_paths(cycle),
594            None => Box::new([]),
595        };
596
597        Ok(LinkAnalysis { module_indices, cycle })
598    }
599
600    /// Links `kernel` using the current state of the linker.
601    ///
602    /// Returns the module index of the kernel module, which is expected to provide the public
603    /// interface of the final assembled kernel.
604    ///
605    /// This differs from `link` in that we allow all AST modules in the module graph access to
606    /// kernel features, e.g. `caller`, as if they are defined by the kernel module itself.
607    pub fn link_kernel(
608        &mut self,
609        mut kernel: Box<Module>,
610        support: impl IntoIterator<Item = Box<Module>>,
611    ) -> Result<Vec<ModuleIndex>, LinkerError> {
612        self.link_modules(support)?;
613        let original_module_len = self.modules.len();
614        let original_callgraph = self.callgraph.clone();
615        let module_index = self.link_module(&mut kernel)?;
616        let original_kernel_index = self.kernel_index;
617        let original_module_kinds = self
618            .modules
619            .iter()
620            .enumerate()
621            .take(module_index.as_usize())
622            .filter(|(_, module)| matches!(module.source(), ModuleSource::Ast))
623            .map(|(module_index, module)| (module_index, module.kind()))
624            .collect::<Vec<_>>();
625
626        // Set the module kind of all pending AST modules to Kernel, as we are linking a kernel
627        for module in self.modules.iter_mut().take(module_index.as_usize()) {
628            if matches!(module.source(), ModuleSource::Ast) {
629                module.set_kind(ast::ModuleKind::Kernel);
630            }
631        }
632
633        self.kernel_index = Some(module_index);
634
635        let result = (|| {
636            let namespaces = NamespaceGraph::build(self)?;
637            let imports = namespaces.resolve_imports(self)?;
638            self.link_and_rewrite(&namespaces, &imports, LinkMode::Strict)?;
639
640            Ok(namespaces.reachable_from_root(module_index))
641        })();
642
643        match result {
644            ok @ Ok(_) => ok,
645            err => {
646                self.kernel_index = original_kernel_index;
647                self.callgraph = original_callgraph;
648                self.modules.truncate(original_module_len);
649                for (module_index, module_kind) in original_module_kinds {
650                    self.modules[module_index].set_kind(module_kind);
651                }
652
653                err
654            },
655        }
656    }
657
658    /// Compute the module graph from the set of pending modules, and link it, rewriting any AST
659    /// modules with unresolved, or partially-resolved, symbol references.
660    ///
661    /// This should be called any time you add more libraries or modules to the module graph, to
662    /// ensure that the graph is valid, and that there are no unresolved references. In general,
663    /// you will only instantiate the linker, build up the graph, and link a single time; but you
664    /// can re-use the linker to build multiple artifacts as well.
665    ///
666    /// When this function is called, some initial information is calculated about the AST modules
667    /// which are to be added to the graph, and then each module is visited to perform a deeper
668    /// analysis than can be done by the `sema` module, as we now have the full set of modules
669    /// available to do import resolution, and to rewrite invoke targets with their absolute paths
670    /// and/or MAST roots. A variety of issues are caught at this stage.
671    ///
672    /// Once each module is validated, the various analysis results stored as part of the graph
673    /// structure are updated to reflect that module being added to the graph. Once part of the
674    /// graph, the module becomes immutable/clone-on-write, so as to allow the graph to be
675    /// cheaply cloned.
676    ///
677    /// The final, and most important, analysis done by this function is the topological sort of
678    /// the global call graph, which contains the inter-procedural dependencies of every procedure
679    /// in the module graph. We use this sort order to do two things:
680    ///
681    /// 1. Verify that there are no static cycles in the graph that would prevent us from being able
682    ///    to hash the generated MAST of the program. NOTE: dynamic cycles, e.g. those induced by
683    ///    `dynexec`, are perfectly fine, we are only interested in preventing cycles that interfere
684    ///    with the ability to generate MAST roots.
685    ///
686    /// 2. Visit the call graph bottom-up, so that we can fully compile a procedure before any of
687    ///    its callers, and thus rewrite those callers to reference that procedure by MAST root,
688    ///    rather than by name. As a result, a compiled MAST program is like an immutable snapshot
689    ///    of the entire call graph at the time of compilation. Later, if we choose to recompile a
690    ///    subset of modules (currently we do not have support for this in the assembler API), we
691    ///    can re-analyze/re-compile only those parts of the graph which have actually changed.
692    ///
693    /// NOTE: This will return `Err` if we detect a validation error, an operation not supported by
694    /// the current configuration, or, in [`LinkMode::Strict`], a cycle in the graph. In
695    /// [`LinkMode::Analysis`] a static cycle is not fatal: the resolved modules and call edges are
696    /// committed, and the cycle is returned as a nonfatal diagnostic instead of an error.
697    fn link_and_rewrite(
698        &mut self,
699        namespaces: &NamespaceGraph,
700        imports: &ResolvedImports,
701        mode: LinkMode,
702    ) -> Result<Option<CycleError>, LinkerError> {
703        log::debug!(
704            target: "linker",
705            "processing {} unlinked/partially-linked modules, and recomputing module graph",
706            self.modules.iter().filter(|m| !m.is_linked()).count()
707        );
708
709        // It is acceptable for there to be no changes, but if the graph is empty and no changes
710        // are being made, we treat that as an error
711        if self.modules.is_empty() {
712            return Err(LinkerError::Empty);
713        }
714
715        // If no changes are being made, report or reject any cycle already committed by analysis
716        // mode.
717        if self.modules.iter().all(LinkModule::is_linked) {
718            return match self.callgraph.toposort() {
719                Err(cycle) if mode == LinkMode::Strict => Err(self.cycle_error(cycle)),
720                Err(cycle) => Ok(Some(cycle)),
721                Ok(_) => Ok(None),
722            };
723        }
724
725        // Obtain a set of resolvers for the pending modules so that we can do name resolution
726        // before they are added to the graph
727        let pending_modules = self
728            .modules
729            .iter()
730            .enumerate()
731            .filter(|(_, module)| module.is_unlinked())
732            .map(|(module_index, module)| (module_index, module.clone()))
733            .collect::<Vec<_>>();
734        let original_callgraph = self.callgraph.clone();
735
736        let result = (|| {
737            let resolver = SymbolResolver::with_namespaces(self, namespaces, imports);
738            let mut edges = Vec::new();
739            let mut cache = ResolverCache::default();
740            let mut linked_modules = Vec::new();
741
742            for (module_index, module) in self.modules.iter().enumerate() {
743                if !module.is_unlinked() {
744                    continue;
745                }
746
747                let module_index = ModuleIndex::new(module_index);
748
749                for import in module.imports() {
750                    if let Some(namespaces::ResolvedUse::Item(gid)) =
751                        imports.get(module_index, import.local_name().as_str())
752                    {
753                        import.set_resolved(gid);
754                    }
755                }
756
757                for (symbol_idx, symbol) in module.symbols().enumerate() {
758                    let gid = module_index + ItemIndex::new(symbol_idx);
759
760                    // Perform any applicable rewrites to this item
761                    rewrites::rewrite_symbol(gid, symbol, &resolver, &mut cache)?;
762
763                    // Update the linker graph
764                    match symbol.item() {
765                        SymbolItem::Compiled(_) | SymbolItem::Type(_) | SymbolItem::Constant(_) => {
766                        },
767                        SymbolItem::Procedure(proc) => {
768                            // Add edges to all transitive dependencies of this item due to
769                            // calls/symbol refs
770                            let proc = proc.borrow();
771                            for invoke in proc.invoked() {
772                                log::debug!(target: "linker", "  | recording {} dependency on {}", invoke.kind, invoke.target);
773
774                                let context = SymbolResolutionContext {
775                                    span: invoke.span(),
776                                    module: module_index,
777                                    kind: Some(invoke.kind),
778                                };
779                                if let Some(callee) = resolver
780                                    .resolve_invoke_target(&context, &invoke.target)?
781                                    .into_global_id()
782                                {
783                                    log::debug!(
784                                        target: "linker",
785                                        "  | resolved dependency to gid {}:{}",
786                                        callee.module.as_usize(),
787                                        callee.index.as_usize()
788                                    );
789                                    edges.push((gid, callee));
790                                }
791                            }
792                        },
793                    }
794                }
795
796                linked_modules.push(module_index);
797            }
798
799            let mut callgraph = self.callgraph.clone();
800            // A static cycle may be introduced either by a self-edge (a procedure that calls
801            // itself) or by a longer cycle that is only visible once all edges are in place. We
802            // accumulate every procedure that participates in such a cycle, then let `mode` decide
803            // whether it is fatal.
804            let mut cycle_nodes: BTreeSet<GlobalItemIndex> = BTreeSet::new();
805            for (caller, callee) in edges {
806                match callgraph.add_edge(caller, callee) {
807                    Ok(()) => (),
808                    // A self-edge: the callee _is_ the caller. `add_edge` rejects it without
809                    // recording it, so insert the edge manually to keep the call graph complete
810                    // for analysis, and remember the cycle.
811                    Err(cycle) => {
812                        callgraph.get_or_insert_node(callee);
813                        callgraph.get_or_insert_node(caller).push(callee);
814                        cycle_nodes.extend(cycle.into_node_ids());
815                    },
816                }
817            }
818
819            // Detect any remaining static cycles now that every edge has been recorded.
820            if let Err(cycle) = callgraph.toposort() {
821                cycle_nodes.extend(cycle.into_node_ids());
822            }
823
824            let cycle = if cycle_nodes.is_empty() {
825                None
826            } else {
827                Some(CycleError::new(cycle_nodes))
828            };
829
830            // Strict linking rejects any static cycle before MAST is ever built; analysis mode
831            // keeps the committed graph and returns the cycle as a nonfatal diagnostic.
832            if mode == LinkMode::Strict
833                && let Some(cycle) = cycle
834            {
835                Err(self.cycle_error(cycle))
836            } else {
837                Ok::<_, LinkerError>((linked_modules, callgraph, cycle))
838            }
839        })();
840
841        match result {
842            Ok((linked_modules, callgraph, cycle)) => {
843                self.callgraph = callgraph;
844                for module_index in linked_modules {
845                    self.modules[module_index.as_usize()].set_status(LinkStatus::Linked);
846                }
847                Ok(cycle)
848            },
849            Err(err) => {
850                self.callgraph = original_callgraph;
851                for (module_index, module) in pending_modules {
852                    self.modules[module_index] = module;
853                }
854                Err(err)
855            },
856        }
857    }
858}
859
860// ------------------------------------------------------------------------------------------------
861/// Accessors/Queries
862impl Linker {
863    /// Get access to all module information maintained by the linker
864    pub fn modules(&self) -> &[LinkModule] {
865        self.modules.as_slice()
866    }
867
868    /// Get an iterator over the external libraries the linker has linked against
869    pub fn libraries(&self) -> impl Iterator<Item = &LinkLibrary> {
870        self.libraries.values()
871    }
872
873    /// Get an iterator over the static libraries used to build the final MAST forest.
874    pub fn static_libraries(&self) -> impl Iterator<Item = &LinkLibrary> {
875        self.static_libraries.values()
876    }
877
878    /// Compute the topological sort of the callgraph rooted at `caller`
879    pub fn topological_sort_from_root(
880        &self,
881        caller: GlobalItemIndex,
882    ) -> Result<Vec<GlobalItemIndex>, CycleError> {
883        self.callgraph.toposort_caller(caller)
884    }
885
886    /// Returns a procedure index which corresponds to the provided procedure digest.
887    ///
888    /// Note that there can be many procedures with the same digest. This method returns an
889    /// arbitrary one.
890    pub fn get_procedure_index_by_digest(
891        &self,
892        procedure_digest: &Word,
893    ) -> Option<GlobalItemIndex> {
894        self.procedures_by_mast_root.get(procedure_digest).map(|indices| indices[0])
895    }
896
897    /// Resolves `target` from the perspective of `caller`.
898    pub fn resolve_invoke_target(
899        &self,
900        caller: &SymbolResolutionContext,
901        target: &InvocationTarget,
902    ) -> Result<SymbolResolution, LinkerError> {
903        let namespaces = NamespaceGraph::build(self)?;
904        let imports = namespaces.resolve_imports(self)?;
905        let resolver = SymbolResolver::with_namespaces(self, &namespaces, &imports);
906        resolver.resolve_invoke_target(caller, target)
907    }
908
909    /// Resolves `path` from the perspective of `caller`.
910    pub fn resolve_path(
911        &self,
912        caller: &SymbolResolutionContext,
913        path: &Path,
914    ) -> Result<SymbolResolution, LinkerError> {
915        let namespaces = NamespaceGraph::build(self)?;
916        let imports = namespaces.resolve_imports(self)?;
917        let resolver = SymbolResolver::with_namespaces(self, &namespaces, &imports);
918        resolver.resolve_path(caller, Span::new(caller.span, path))
919    }
920
921    /// Resolves the user-defined type signature of the given procedure to the HIR type signature
922    pub fn resolve_signature(
923        &self,
924        gid: GlobalItemIndex,
925    ) -> Result<Option<Arc<types::FunctionType>>, LinkerError> {
926        match self[gid].item() {
927            SymbolItem::Compiled(ItemInfo::Procedure(proc)) => Ok(proc.signature.clone()),
928            SymbolItem::Procedure(proc) => {
929                let proc = proc.borrow();
930                match proc.signature() {
931                    Some(ty) => self.translate_function_type(gid.module, ty).map(Some),
932                    None => Ok(None),
933                }
934            },
935            SymbolItem::Compiled(_) | SymbolItem::Constant(_) | SymbolItem::Type(_) => {
936                panic!("procedure index unexpectedly refers to non-procedure item")
937            },
938        }
939    }
940
941    fn translate_function_type(
942        &self,
943        module_index: ModuleIndex,
944        ty: &ast::FunctionType,
945    ) -> Result<Arc<types::FunctionType>, LinkerError> {
946        use miden_assembly_syntax::ast::TypeResolver;
947
948        let cc = ty.cc.clone();
949        let mut args = Vec::with_capacity(ty.args.len());
950
951        let symbol_resolver = SymbolResolver::new(self);
952        let mut cache = ResolverCache::default();
953        let mut resolver = Resolver {
954            resolver: &symbol_resolver,
955            cache: &mut cache,
956            current_module: module_index,
957        };
958        for arg in ty.args.iter() {
959            if let Some(arg) = resolver.resolve(arg)? {
960                args.push(arg);
961            } else {
962                let span = arg.span();
963                return Err(LinkerError::UndefinedType {
964                    span,
965                    source_file: self.source_manager.get(span.source_id()).ok(),
966                });
967            }
968        }
969        let mut results = Vec::with_capacity(ty.results.len());
970        for result in ty.results.iter() {
971            if let Some(result) = resolver.resolve(result)? {
972                results.push(result);
973            } else {
974                let span = result.span();
975                return Err(LinkerError::UndefinedType {
976                    span,
977                    source_file: self.source_manager.get(span.source_id()).ok(),
978                });
979            }
980        }
981        Ok(Arc::new(types::FunctionType::new(cc, args, results)))
982    }
983
984    /// Resolves a [GlobalItemIndex] to the known attributes of that procedure
985    pub fn resolve_attributes(&self, gid: GlobalItemIndex) -> AttributeSet {
986        match self[gid].item() {
987            SymbolItem::Compiled(ItemInfo::Procedure(proc)) => proc.attributes.clone(),
988            SymbolItem::Procedure(proc) => {
989                let proc = proc.borrow();
990                proc.attributes().clone()
991            },
992            SymbolItem::Compiled(_) | SymbolItem::Constant(_) | SymbolItem::Type(_) => {
993                panic!("procedure index unexpectedly refers to non-procedure item")
994            },
995        }
996    }
997
998    /// Resolves a [GlobalItemIndex] to a concrete [ast::types::Type]
999    pub fn resolve_type(
1000        &self,
1001        span: SourceSpan,
1002        gid: GlobalItemIndex,
1003    ) -> Result<types::Type, LinkerError> {
1004        use miden_assembly_syntax::ast::{TypeResolver, constants::ConstEnvironment};
1005
1006        let symbol_resolver = SymbolResolver::new(self);
1007        let mut cache = ResolverCache::default();
1008        let mut resolver = Resolver {
1009            cache: &mut cache,
1010            resolver: &symbol_resolver,
1011            current_module: gid.module,
1012        };
1013
1014        let template = resolver.get_type(span, gid)?.ok_or_else(|| LinkerError::UndefinedType {
1015            span,
1016            source_file: resolver.get_source_file_for(span),
1017        })?;
1018        resolver.finalize(span, template)
1019    }
1020
1021    /// Registers a [MastNodeId] as corresponding to a given [GlobalProcedureIndex].
1022    ///
1023    /// # SAFETY
1024    ///
1025    /// It is essential that the caller _guarantee_ that the given digest belongs to the specified
1026    /// procedure. It is fine if there are multiple procedures with the same digest, but it _must_
1027    /// be the case that if a given digest is specified, it can be used as if it was the definition
1028    /// of the referenced procedure, i.e. they are referentially transparent.
1029    pub(crate) fn register_procedure_root(
1030        &mut self,
1031        id: GlobalItemIndex,
1032        procedure_mast_root: Word,
1033    ) {
1034        use alloc::collections::btree_map::Entry;
1035        match self.procedures_by_mast_root.entry(procedure_mast_root) {
1036            Entry::Occupied(ref mut entry) => {
1037                let prev_id = entry.get()[0];
1038                if prev_id != id {
1039                    // Multiple procedures with the same root, but compatible
1040                    entry.get_mut().push(id);
1041                }
1042            },
1043            Entry::Vacant(entry) => {
1044                entry.insert(smallvec![id]);
1045            },
1046        }
1047    }
1048
1049    /// Resolve a [Path] to a [ModuleIndex] in this graph
1050    pub fn find_module_index(&self, path: &Path) -> Option<ModuleIndex> {
1051        self.modules.iter().position(|m| path == m.path()).map(ModuleIndex::new)
1052    }
1053
1054    /// Resolve a [Path] to a [Module] in this graph
1055    pub fn find_module(&self, path: &Path) -> Option<&LinkModule> {
1056        self.modules.iter().find(|m| path == m.path())
1057    }
1058}
1059
1060/// Const evaluation
1061impl Linker {
1062    /// Evaluate `expr` to a concrete constant value, in the context of the given item.
1063    pub fn const_eval(
1064        &self,
1065        gid: GlobalItemIndex,
1066        expr: &ast::ConstantExpr,
1067        cache: &mut ResolverCache,
1068    ) -> Result<ast::ConstantValue, LinkerError> {
1069        let symbol_resolver = SymbolResolver::new(self);
1070        let mut resolver = Resolver {
1071            resolver: &symbol_resolver,
1072            cache,
1073            current_module: gid.module,
1074        };
1075
1076        ast::constants::eval::expr(expr, &mut resolver).map(|expr| expr.expect_value())
1077    }
1078}
1079
1080impl Index<ModuleIndex> for Linker {
1081    type Output = LinkModule;
1082
1083    fn index(&self, index: ModuleIndex) -> &Self::Output {
1084        &self.modules[index.as_usize()]
1085    }
1086}
1087
1088impl Index<GlobalItemIndex> for Linker {
1089    type Output = Symbol;
1090
1091    fn index(&self, index: GlobalItemIndex) -> &Self::Output {
1092        &self.modules[index.module.as_usize()][index.index]
1093    }
1094}
1095
1096#[cfg(test)]
1097mod tests {
1098    use std::{
1099        collections::BTreeSet,
1100        panic::{AssertUnwindSafe, catch_unwind},
1101        string::String,
1102        sync::Arc,
1103    };
1104
1105    use miden_assembly_syntax::{
1106        ast::{
1107            Ident, InvocationTarget, InvokeKind, ItemIndex, Path, SymbolResolutionError,
1108            Visibility, types,
1109        },
1110        debuginfo::{SourceSpan, Span},
1111        module::{ItemInfo, TypeInfo},
1112    };
1113    use miden_core::Felt;
1114
1115    use super::*;
1116    use crate::{
1117        Assembler,
1118        ast::Module,
1119        testing::{TestContext, source_file},
1120    };
1121
1122    #[test]
1123    fn failed_kernel_link_restores_kernel_state() {
1124        let context = TestContext::default();
1125        let source_manager = context.source_manager();
1126        let kernel_source = r#"
1127                pub proc a
1128                    call.b
1129                end
1130
1131                proc b
1132                    call.a
1133                end
1134                "#;
1135
1136        let userspace = context
1137            .parse_module(source_file!(
1138                &context,
1139                r#"
1140                    namespace userspace
1141
1142                    pub proc helper
1143                        push.1
1144                    end
1145                    "#
1146            ))
1147            .expect("userspace module parsing must succeed");
1148
1149        let mut linker = Linker::new(source_manager);
1150        let userspace_index = linker
1151            .link([userspace], None)
1152            .expect("userspace module must link successfully")
1153            .into_iter()
1154            .next()
1155            .expect("linked module index must be returned");
1156
1157        let first_err = linker
1158            .link_kernel(
1159                context
1160                    .parse_kernel(source_file!(&context, kernel_source))
1161                    .expect("kernel parsing must succeed"),
1162                None,
1163            )
1164            .expect_err("expected cyclic kernel to be rejected");
1165
1166        assert!(first_err.to_string().contains("found a cycle in the call graph"));
1167        assert!(!linker.has_nonempty_kernel(), "failed kernel link must not leave a kernel set");
1168        assert_eq!(linker[userspace_index].kind(), ast::ModuleKind::Library);
1169
1170        let second_err = linker
1171            .link_kernel(
1172                context
1173                    .parse_kernel(source_file!(&context, kernel_source))
1174                    .expect("kernel parsing must succeed"),
1175                None,
1176            )
1177            .expect_err("expected cyclic kernel retry to be rejected");
1178        assert!(second_err.to_string().contains("found a cycle in the call graph"));
1179        assert!(!second_err.to_string().contains("duplicate module"));
1180
1181        let syscall_context = SymbolResolutionContext {
1182            span: SourceSpan::UNKNOWN,
1183            module: userspace_index,
1184            kind: Some(InvokeKind::SysCall),
1185        };
1186        let err = linker
1187            .resolve_invoke_target(
1188                &syscall_context,
1189                &InvocationTarget::Symbol(Ident::new("a").expect("valid identifier")),
1190            )
1191            .expect_err("expected syscall without a linked kernel to be rejected");
1192        assert!(matches!(err, LinkerError::InvalidSysCallTarget { .. }));
1193    }
1194
1195    #[test]
1196    fn link_library_keeps_same_interface_libraries_with_distinct_forest_commitments() {
1197        let context = TestContext::default();
1198        let module = context
1199            .parse_module(source_file!(
1200                &context,
1201                r#"
1202                namespace lib
1203
1204                pub proc foo
1205                    push.1
1206                end
1207                "#
1208            ))
1209            .expect("library module should parse");
1210        let package: Arc<MastPackage> = Assembler::new(context.source_manager())
1211            .assemble_library("lib", module, None::<Box<Module>>)
1212            .expect("library should assemble")
1213            .into();
1214        let with_advice = Arc::new(package.as_ref().clone().with_advice_map(AdviceMap::from_iter(
1215            [(Word::from([1_u32, 2, 3, 4]), vec![Felt::from_u32(5)])],
1216        )));
1217
1218        assert_ne!(package.commitment(), with_advice.commitment());
1219        assert_eq!(
1220            package.interface_commitment().unwrap(),
1221            with_advice.interface_commitment().unwrap()
1222        );
1223        assert_ne!(package.mast_forest().commitment(), with_advice.mast_forest().commitment());
1224
1225        let mut linker = Linker::new(context.source_manager());
1226        linker
1227            .link_library(LinkLibrary::from_package(package).with_linkage(Linkage::Static))
1228            .expect("first library should link");
1229        linker
1230            .link_library(LinkLibrary::from_package(with_advice).with_linkage(Linkage::Static))
1231            .expect("same public interface with distinct forest commitment should link");
1232
1233        assert_eq!(linker.libraries().count(), 1);
1234        assert_eq!(linker.static_libraries().count(), 2);
1235    }
1236
1237    #[test]
1238    fn oversized_link_module_resolution_returns_structured_error() {
1239        let context = TestContext::default();
1240        let mut linker = Linker::new(context.source_manager());
1241        let module_id = ModuleIndex::new(0);
1242        let path = Arc::<Path>::from(Path::new("::m::huge"));
1243        let mut symbols = Vec::with_capacity(ItemIndex::MAX_ITEMS + 1);
1244
1245        for i in 0..=ItemIndex::MAX_ITEMS {
1246            let name = Ident::new(format!("a{i}")).expect("valid identifier");
1247            symbols.push(Symbol::new(
1248                name.clone(),
1249                Visibility::Private,
1250                LinkStatus::Unlinked,
1251                SymbolItem::Compiled(ItemInfo::Type(TypeInfo { name, ty: types::Type::Felt })),
1252            ));
1253        }
1254
1255        linker.modules.push(
1256            LinkModule::new(
1257                module_id,
1258                ast::ModuleKind::Library,
1259                LinkStatus::Unlinked,
1260                ModuleSource::Mast,
1261                path,
1262            )
1263            .with_symbols(symbols),
1264        );
1265
1266        let result = catch_unwind(AssertUnwindSafe(|| {
1267            linker[module_id].resolve(Span::unknown("a0"), &SymbolResolver::new(&linker))
1268        }));
1269
1270        let result = match result {
1271            Ok(result) => result,
1272            Err(panic) => {
1273                let message = panic
1274                    .downcast_ref::<&str>()
1275                    .copied()
1276                    .or_else(|| panic.downcast_ref::<String>().map(String::as_str))
1277                    .expect("panic payload should be a string");
1278                panic!("expected graceful error, got panic: {message}");
1279            },
1280        };
1281
1282        assert!(matches!(
1283            result,
1284            Err(err) if matches!(*err, SymbolResolutionError::TooManyItemsInModule { .. })
1285        ));
1286    }
1287
1288    /// Resolve `name` within `module_index` to its [GlobalItemIndex] in the linker graph.
1289    fn proc_gid(linker: &Linker, module_index: ModuleIndex, name: &str) -> GlobalItemIndex {
1290        let index = ItemIndex::new(
1291            linker[module_index]
1292                .symbols()
1293                .position(|symbol| symbol.name().as_str() == name)
1294                .expect("procedure should be present in the module"),
1295        );
1296        GlobalItemIndex { module: module_index, index }
1297    }
1298
1299    #[test]
1300    fn analysis_mode_commits_cycle_and_reports_procedure_paths() {
1301        let context = TestContext::default();
1302        let module = context
1303            .parse_module(source_file!(
1304                &context,
1305                r#"
1306                namespace proj
1307
1308                pub proc a
1309                    call.b
1310                    call.leaf
1311                end
1312
1313                pub proc b
1314                    call.a
1315                end
1316
1317                pub proc caller
1318                    call.a
1319                end
1320
1321                pub proc leaf
1322                    push.1
1323                end
1324
1325                pub proc independent
1326                    push.1
1327                end
1328                "#
1329            ))
1330            .expect("cyclic module must parse");
1331
1332        let mut linker = Linker::new(context.source_manager());
1333        let analysis = linker
1334            .link_analysis([module], None)
1335            .expect("analysis link must not reject a static cycle");
1336
1337        // Analysis mode returns linked module indices and a cycle diagnostic.
1338        assert!(
1339            !analysis.module_indices.is_empty(),
1340            "analysis must return linked module indices"
1341        );
1342        assert!(analysis.has_cycle(), "analysis must report the static recursion cycle");
1343        let cycle: BTreeSet<String> = analysis.cycle.iter().cloned().collect();
1344        assert_eq!(
1345            cycle,
1346            BTreeSet::from(["::proj::a".to_string(), "::proj::b".to_string()]),
1347            "cycle diagnostic must exclude acyclic callees"
1348        );
1349
1350        let module_index = analysis.module_indices[0];
1351
1352        // The cycle, and every caller that depends on it, cannot be lifted.
1353        let a = proc_gid(&linker, module_index, "a");
1354        let caller = proc_gid(&linker, module_index, "caller");
1355        assert!(
1356            linker.topological_sort_from_root(a).is_err(),
1357            "a procedure in the cycle must be detected as part of a cycle"
1358        );
1359        assert!(
1360            linker.topological_sort_from_root(caller).is_err(),
1361            "a caller that depends on the cycle must be detected alongside it"
1362        );
1363
1364        // Procedures outside the skipped set can still be lifted.
1365        let independent = proc_gid(&linker, module_index, "independent");
1366        let sorted = linker
1367            .topological_sort_from_root(independent)
1368            .expect("a procedure outside the cycle must still be liftable");
1369        assert_eq!(sorted, vec![independent]);
1370
1371        // Once analysis mode commits the linked graph, later calls must still observe its cycle.
1372        let repeated = linker
1373            .link_analysis([], [])
1374            .expect("repeated analysis must preserve the cycle diagnostic");
1375        assert_eq!(repeated.cycle, analysis.cycle);
1376
1377        let err = linker
1378            .link([], [])
1379            .expect_err("strict linking must reject a cycle committed by analysis mode");
1380        assert!(
1381            err.to_string().contains("found a cycle in the call graph"),
1382            "strict link should report the committed cycle, got: {err}"
1383        );
1384    }
1385
1386    #[test]
1387    fn analysis_mode_reports_no_cycle_for_acyclic_graph() {
1388        let context = TestContext::default();
1389        let module = context
1390            .parse_module(source_file!(
1391                &context,
1392                r#"
1393                namespace proj
1394
1395                pub proc a
1396                    push.1
1397                end
1398
1399                pub proc b
1400                    call.a
1401                end
1402                "#
1403            ))
1404            .expect("acyclic module must parse");
1405
1406        let mut linker = Linker::new(context.source_manager());
1407        let analysis = linker
1408            .link_analysis([module], None)
1409            .expect("analysis link must succeed for an acyclic graph");
1410
1411        assert!(!analysis.has_cycle(), "an acyclic graph must not report a cycle");
1412        assert!(analysis.cycle.is_empty());
1413        assert!(!analysis.module_indices.is_empty());
1414
1415        let module_index = analysis.module_indices[0];
1416        let a = proc_gid(&linker, module_index, "a");
1417        let b = proc_gid(&linker, module_index, "b");
1418        let sorted = linker
1419            .topological_sort_from_root(b)
1420            .expect("an acyclic graph must be fully liftable");
1421        assert_eq!(sorted, vec![b, a]);
1422    }
1423
1424    #[test]
1425    fn strict_mode_rejects_cycle_and_rolls_back() {
1426        let context = TestContext::default();
1427        let module = context
1428            .parse_module(source_file!(
1429                &context,
1430                r#"
1431                namespace proj
1432
1433                pub proc a
1434                    call.b
1435                end
1436
1437                pub proc b
1438                    call.a
1439                end
1440                "#
1441            ))
1442            .expect("cyclic module must parse");
1443
1444        let mut linker = Linker::new(context.source_manager());
1445        let err = linker
1446            .link([module], None)
1447            .expect_err("strict linking must reject a static cycle before MAST is built");
1448        assert!(
1449            err.to_string().contains("found a cycle in the call graph"),
1450            "strict link should report the cycle, got: {err}"
1451        );
1452
1453        // A failed strict link rolls back all changes: the module stays in the graph but is
1454        // unlinked, and no call edges are committed.
1455        let module_index = linker
1456            .modules()
1457            .iter()
1458            .position(|module| module.path().as_str().ends_with("::proj"))
1459            .map(ModuleIndex::new)
1460            .expect("module should still be present after a rolled-back link");
1461        assert!(
1462            linker[module_index].is_unlinked(),
1463            "a failed strict link must not mark modules as linked"
1464        );
1465        let a = proc_gid(&linker, module_index, "a");
1466        assert_eq!(
1467            linker
1468                .topological_sort_from_root(a)
1469                .expect("a failed strict link must not commit any call edges"),
1470            vec![a],
1471        );
1472    }
1473
1474    #[test]
1475    fn fatal_link_error_rolls_back_symbol_status() {
1476        for mode in [LinkMode::Strict, LinkMode::Analysis] {
1477            let context = TestContext::default();
1478            let module = context
1479                .parse_module(source_file!(
1480                    &context,
1481                    r#"
1482                    namespace proj
1483
1484                    pub proc linked
1485                        push.1
1486                    end
1487
1488                    pub proc unresolved
1489                        call.::support::missing
1490                    end
1491                    "#
1492                ))
1493                .expect("module with an unresolved call must parse");
1494            let support = context
1495                .parse_module(source_file!(
1496                    &context,
1497                    r#"
1498                    namespace support
1499
1500                    pub proc present
1501                        push.1
1502                    end
1503                    "#
1504                ))
1505                .expect("support module must parse");
1506
1507            let mut linker = Linker::new(context.source_manager());
1508            let err = match mode {
1509                LinkMode::Strict => linker.link([module], [support]).map(drop),
1510                LinkMode::Analysis => linker.link_analysis([module], [support]).map(drop),
1511            }
1512            .expect_err("an unresolved call must be fatal");
1513
1514            assert!(
1515                !err.to_string().contains("found a cycle in the call graph"),
1516                "an unresolved call must not be reported as a cycle, got: {err}"
1517            );
1518            assert!(linker[ModuleIndex::new(0)].is_unlinked());
1519            assert!(linker[ModuleIndex::new(0)].symbols().all(Symbol::is_unlinked));
1520        }
1521    }
1522}