Skip to main content

miden_assembly/
assembler.rs

1pub(super) mod debuginfo;
2mod error;
3mod product;
4
5use alloc::{boxed::Box, collections::BTreeMap, string::ToString, sync::Arc, vec::Vec};
6
7use debuginfo::DebugInfoSections;
8use miden_assembly_syntax::{
9    KernelLibrary, Library, MAX_REPEAT_COUNT, Parse, ParseOptions, SemanticAnalysisError,
10    ast::{
11        self, Ident, InvocationTarget, InvokeKind, ItemIndex, ModuleKind, SymbolResolution,
12        Visibility, types::FunctionType,
13    },
14    debuginfo::{DefaultSourceManager, SourceManager, SourceSpan, Spanned},
15    diagnostics::{IntoDiagnostic, RelatedLabel, Report},
16    library::{ConstantExport, ItemInfo, LibraryExport, ProcedureExport, TypeExport},
17};
18use miden_core::{
19    Word,
20    mast::{
21        DecoratorId, LoopNodeBuilder, MastForestContributor, MastNodeExt, MastNodeId,
22        SplitNodeBuilder,
23    },
24    operations::{AssemblyOp, Operation},
25    program::{Kernel, Program},
26};
27use miden_mast_package::PackageManifest;
28use miden_project::{Linkage, TargetType};
29
30use self::{error::AssemblerError, product::AssemblyProduct};
31use crate::{
32    GlobalItemIndex, ModuleIndex, Procedure, ProcedureContext,
33    ast::Path,
34    basic_block_builder::{BasicBlockBuilder, BasicBlockOrDecorators},
35    fmp::{fmp_end_frame_sequence, fmp_initialization_sequence, fmp_start_frame_sequence},
36    linker::{LinkLibrary, Linker, LinkerError, SymbolItem, SymbolResolutionContext},
37    mast_forest_builder::MastForestBuilder,
38};
39
40/// Maximum allowed nesting of control-flow blocks during compilation.
41///
42/// This limit is intended to prevent stack overflows from maliciously deep block nesting while
43/// remaining far above typical program structure depth.
44pub(crate) const MAX_CONTROL_FLOW_NESTING: usize = 256;
45
46// ASSEMBLER
47// ================================================================================================
48
49/// The [Assembler] produces a _Merkelized Abstract Syntax Tree (MAST)_ from Miden Assembly sources,
50/// as an artifact of one of three types:
51///
52/// * A kernel library (see [`KernelLibrary`])
53/// * A library (see [`Library`])
54/// * A program (see [`Program`])
55///
56/// Assembled artifacts can additionally reference or include code from previously assembled
57/// libraries.
58///
59/// # Usage
60///
61/// Depending on your needs, there are multiple ways of using the assembler, starting with the
62/// type of artifact you want to produce:
63///
64/// * If you wish to produce an executable program, you will call [`Self::assemble_program`] with
65///   the source module which contains the program entrypoint.
66/// * If you wish to produce a library for use in other executables, you will call
67///   [`Self::assemble_library`] with the source module(s) whose exports form the public API of the
68///   library.
69/// * If you wish to produce a kernel library, you will call [`Self::assemble_kernel`] with the
70///   source module(s) whose exports form the public API of the kernel.
71///
72/// In the case where you are assembling a library or program, you also need to determine if you
73/// need to specify a kernel. You will need to do so if any of your code needs to call into the
74/// kernel directly.
75///
76/// * If a kernel is needed, you should construct an `Assembler` using [`Assembler::with_kernel`]
77/// * Otherwise, you should construct an `Assembler` using [`Assembler::new`]
78///
79/// <div class="warning">
80/// Programs compiled with an empty kernel cannot use the `syscall` instruction.
81/// </div>
82///
83/// Lastly, you need to provide inputs to the assembler which it will use at link time to resolve
84/// references to procedures which are externally-defined (i.e. not defined in any of the modules
85/// provided to the `assemble_*` function you called). There are a few different ways to do this:
86///
87/// * If you have source code, or a [`ast::Module`], see [`Self::compile_and_statically_link`]
88/// * If you need to reference procedures from a previously assembled [`Library`], but do not want
89///   to include the MAST of those procedures in the assembled artifact, you want to _dynamically
90///   link_ that library, see [`Self::link_dynamic_library`] for more.
91/// * If you want to incorporate referenced procedures from a previously assembled [`Library`] into
92///   the assembled artifact, you want to _statically link_ that library, see
93///   [`Self::link_static_library`] for more.
94#[derive(Clone)]
95pub struct Assembler {
96    /// The source manager to use for compilation and source location information
97    source_manager: Arc<dyn SourceManager>,
98    /// The linker instance used internally to link assembler inputs
99    linker: Box<Linker>,
100    /// The debug information gathered during assembly
101    pub(super) debug_info: DebugInfoSections,
102    /// Whether to treat warning diagnostics as errors
103    warnings_as_errors: bool,
104    /// Whether to preserve debug information in the assembled artifact.
105    pub(super) emit_debug_info: bool,
106    /// Whether to trim source file paths in debug information.
107    pub(super) trim_paths: bool,
108}
109
110impl Default for Assembler {
111    fn default() -> Self {
112        let source_manager = Arc::new(DefaultSourceManager::default());
113        let linker = Box::new(Linker::new(source_manager.clone()));
114        Self {
115            source_manager,
116            linker,
117            debug_info: Default::default(),
118            warnings_as_errors: false,
119            emit_debug_info: true,
120            trim_paths: false,
121        }
122    }
123}
124
125// ------------------------------------------------------------------------------------------------
126/// Constructors
127impl Assembler {
128    /// Start building an [Assembler]
129    pub fn new(source_manager: Arc<dyn SourceManager>) -> Self {
130        let linker = Box::new(Linker::new(source_manager.clone()));
131        Self {
132            source_manager,
133            linker,
134            debug_info: Default::default(),
135            warnings_as_errors: false,
136            emit_debug_info: true,
137            trim_paths: false,
138        }
139    }
140
141    /// Start building an [`Assembler`] with a kernel defined by the provided [KernelLibrary].
142    pub fn with_kernel(source_manager: Arc<dyn SourceManager>, kernel_lib: KernelLibrary) -> Self {
143        let (kernel, kernel_module, _) = kernel_lib.into_parts();
144        let linker = Box::new(Linker::with_kernel(source_manager.clone(), kernel, kernel_module));
145        Self {
146            source_manager,
147            linker,
148            ..Default::default()
149        }
150    }
151
152    /// Sets the default behavior of this assembler with regard to warning diagnostics.
153    ///
154    /// When true, any warning diagnostics that are emitted will be promoted to errors.
155    pub fn with_warnings_as_errors(mut self, yes: bool) -> Self {
156        self.warnings_as_errors = yes;
157        self
158    }
159
160    #[cfg(feature = "std")]
161    pub(crate) fn with_emit_debug_info(mut self, yes: bool) -> Self {
162        self.emit_debug_info = yes;
163        self
164    }
165
166    #[cfg(feature = "std")]
167    pub(crate) fn with_trim_paths(mut self, yes: bool) -> Self {
168        self.trim_paths = yes;
169        self
170    }
171
172    pub(crate) fn invalid_invoke_target_report(
173        &self,
174        kind: InvokeKind,
175        callee: &InvocationTarget,
176        caller: GlobalItemIndex,
177    ) -> Report {
178        let span = callee.span();
179        let source_file = self.source_manager.get(span.source_id()).ok();
180        let context = SymbolResolutionContext {
181            span,
182            module: caller.module,
183            kind: Some(kind),
184        };
185
186        let path = match self.linker.resolve_invoke_target(&context, callee) {
187            Ok(
188                SymbolResolution::Exact { path, .. }
189                | SymbolResolution::Module { path, .. }
190                | SymbolResolution::External(path),
191            ) => Some(path.into_inner()),
192            Ok(SymbolResolution::Local(_) | SymbolResolution::MastRoot(_)) => None,
193            Err(err) => return Report::new(err),
194        }
195        .or_else(|| match callee {
196            InvocationTarget::Symbol(symbol) => Some(Path::from_ident(symbol).into_owned().into()),
197            InvocationTarget::Path(path) => Some(path.clone().into_inner()),
198            InvocationTarget::MastRoot(_) => None,
199        });
200
201        match path {
202            Some(path) => Report::new(LinkerError::InvalidInvokeTarget { span, source_file, path }),
203            None => Report::msg("invalid procedure reference: target is not a procedure"),
204        }
205    }
206}
207
208// ------------------------------------------------------------------------------------------------
209/// Dependency Management
210impl Assembler {
211    /// Ensures `module` is compiled, and then statically links it into the final artifact.
212    ///
213    /// The given module must be a library module, or an error will be returned.
214    #[inline]
215    pub fn compile_and_statically_link(&mut self, module: impl Parse) -> Result<&mut Self, Report> {
216        self.compile_and_statically_link_all([module])
217    }
218
219    /// Ensures every module in `modules` is compiled, and then statically links them into the final
220    /// artifact.
221    ///
222    /// All of the given modules must be library modules, or an error will be returned.
223    pub fn compile_and_statically_link_all(
224        &mut self,
225        modules: impl IntoIterator<Item = impl Parse>,
226    ) -> Result<&mut Self, Report> {
227        let modules = modules
228            .into_iter()
229            .map(|module| {
230                module.parse_with_options(
231                    self.source_manager.clone(),
232                    ParseOptions {
233                        warnings_as_errors: self.warnings_as_errors,
234                        ..ParseOptions::for_library()
235                    },
236                )
237            })
238            .collect::<Result<Vec<_>, Report>>()?;
239
240        self.linker.link_modules(modules)?;
241
242        Ok(self)
243    }
244
245    /// Compiles and statically links all Miden Assembly modules in the provided directory, using
246    /// the provided [Path] as the root namespace for the compiled modules.
247    ///
248    /// When compiling each module, its Miden Assembly path is derived by appending path components
249    /// corresponding to the relative path of the module in `dir`, to `namespace`. If a source file
250    /// named `mod.masm` is found, the resulting module will derive its path using the path
251    /// components of the parent directory, rather than the file name.
252    ///
253    /// The `namespace` can be any valid Miden Assembly path, e.g. `std` is a valid path, as is
254    /// `std::math::u64` - there is no requirement that the namespace be a single identifier. This
255    /// allows defining multiple projects relative to a common root namespace without conflict.
256    ///
257    /// This function recursively parses the entire directory structure under `dir`, ignoring
258    /// any files which do not have the `.masm` extension.
259    ///
260    /// For example, let's say I call this function like so:
261    ///
262    /// ```rust
263    /// use miden_assembly::{Assembler, Path};
264    ///
265    /// let mut assembler = Assembler::default();
266    /// assembler.compile_and_statically_link_from_dir("~/masm/core", "miden::core::foo");
267    /// ```
268    ///
269    /// Here's how we would handle various files under this path:
270    ///
271    /// - ~/masm/core/sys.masm            -> Parsed as "miden::core::foo::sys"
272    /// - ~/masm/core/crypto/hash.masm    -> Parsed as "miden::core::foo::crypto::hash"
273    /// - ~/masm/core/math/u32.masm       -> Parsed as "miden::core::foo::math::u32"
274    /// - ~/masm/core/math/u64.masm       -> Parsed as "miden::core::foo::math::u64"
275    /// - ~/masm/core/math/README.md      -> Ignored
276    #[cfg(feature = "std")]
277    pub fn compile_and_statically_link_from_dir(
278        &mut self,
279        dir: impl AsRef<std::path::Path>,
280        namespace: impl AsRef<Path>,
281    ) -> Result<(), Report> {
282        use miden_assembly_syntax::parser;
283
284        let namespace = namespace.as_ref();
285        let modules = parser::read_modules_from_dir(
286            dir,
287            namespace,
288            self.source_manager.clone(),
289            self.warnings_as_errors,
290        )?;
291        self.linker.link_modules(modules)?;
292        Ok(())
293    }
294
295    /// Links the final artifact against `library`.
296    ///
297    /// The way in which procedures referenced in `library` will be linked by the final artifact is
298    /// determined by `kind`:
299    ///
300    /// * [`Linkage::Dynamic`] inserts a reference to the procedure in the assembled MAST, but not
301    ///   the MAST of the procedure itself. Consequently, it is necessary to provide both the
302    ///   assembled artifact _and_ `library` to the VM when executing the program, otherwise the
303    ///   procedure reference will not be resolvable at runtime.
304    /// * [`Linkage::Static`] includes the MAST of the referenced procedure in the final artifact,
305    ///   including any code reachable from that procedure contained in `library`. The resulting
306    ///   artifact does not require `library` to be provided to the VM when executing it, as all
307    ///   procedure references were resolved ahead of time.
308    pub fn link_library(
309        &mut self,
310        library: impl AsRef<Library>,
311        linkage: Linkage,
312    ) -> Result<(), Report> {
313        self.linker
314            .link_library(LinkLibrary::from_library(library.as_ref()).with_linkage(linkage))
315            .map_err(Report::from)
316    }
317
318    /// Dynamically link against `library` during assembly.
319    ///
320    /// This makes it possible to resolve references to procedures exported by the library during
321    /// assembly, without including code from the library into the assembled artifact.
322    ///
323    /// Dynamic linking produces smaller binaries, but requires you to provide `library` to the VM
324    /// at runtime when executing the assembled artifact.
325    ///
326    /// Internally, calls to procedures exported from `library` will be lowered to a
327    /// [`miden_core::mast::ExternalNode`] in the resulting MAST. These nodes represent an indirect
328    /// reference to the root MAST node of the referenced procedure. These indirect references
329    /// are resolved at runtime by the processor when executed.
330    ///
331    /// One consequence of these types of references, is that in the case where multiple procedures
332    /// have the same MAST root, but different decorators, it is not (currently) possible for the
333    /// processor to distinguish between which specific procedure (and its resulting decorators) the
334    /// caller intended to reference, and so any of them might be chosen.
335    ///
336    /// In order to reduce the chance of this producing confusing diagnostics or debugger output,
337    /// it is not recommended to export multiple procedures with the same MAST root, but differing
338    /// decorators, from a library. There are scenarios where this might be necessary, such as when
339    /// renaming a procedure, or moving it between modules, while keeping the original definition
340    /// around during a deprecation period. It is just something to be aware of if you notice, for
341    /// example, unexpected procedure paths or source locations in diagnostics - it could be due
342    /// to this edge case.
343    pub fn link_dynamic_library(&mut self, library: impl AsRef<Library>) -> Result<(), Report> {
344        let library = LinkLibrary::from_library(library.as_ref()).with_linkage(Linkage::Dynamic);
345        self.linker.link_library(library).map_err(Report::from)
346    }
347
348    /// Dynamically link against `library` during assembly.
349    ///
350    /// See [`Self::link_dynamic_library`] for more details.
351    pub fn with_dynamic_library(mut self, library: impl AsRef<Library>) -> Result<Self, Report> {
352        self.link_dynamic_library(library)?;
353        Ok(self)
354    }
355
356    /// Statically link against `library` during assembly.
357    ///
358    /// This makes it possible to resolve references to procedures exported by the library during
359    /// assembly, and ensure that the referenced procedure and any code reachable from it in that
360    /// library, are included in the assembled artifact.
361    ///
362    /// Static linking produces larger binaries, but allows you to produce self-contained artifacts
363    /// that avoid the requirement that you provide `library` to the VM at runtime.
364    pub fn link_static_library(&mut self, library: impl AsRef<Library>) -> Result<(), Report> {
365        let library = LinkLibrary::from_library(library.as_ref()).with_linkage(Linkage::Static);
366        self.linker.link_library(library).map_err(Report::from)
367    }
368
369    /// Statically link against `library` during assembly.
370    ///
371    /// See [`Self::link_static_library`]
372    pub fn with_static_library(mut self, library: impl AsRef<Library>) -> Result<Self, Report> {
373        self.link_static_library(library)?;
374        Ok(self)
375    }
376
377    /// Link against `package` with the specified linkage mode during assembly.
378    pub fn link_package(
379        &mut self,
380        package: Arc<miden_mast_package::Package>,
381        linkage: Linkage,
382    ) -> Result<(), Report> {
383        match package.kind {
384            TargetType::Kernel => {
385                if !self.kernel().is_empty() {
386                    return Err(Report::msg(format!(
387                        "duplicate kernels present in the dependency graph: '{}@{}' conflicts with another kernel we've already linked",
388                        &package.name, &package.version
389                    )));
390                }
391
392                let kernel_module = package.kernel_module_info()?;
393                let kernel = package.to_kernel()?;
394                self.linker.link_with_kernel(kernel, kernel_module)?;
395                Ok(())
396            },
397            TargetType::Executable => {
398                Err(Report::msg("cannot add executable packages to an assembler"))
399            },
400            _ => {
401                self.linker
402                    .link_library(LinkLibrary::from_package(package).with_linkage(linkage))?;
403                Ok(())
404            },
405        }
406    }
407}
408
409// ------------------------------------------------------------------------------------------------
410/// Public Accessors
411impl Assembler {
412    /// Returns true if this assembler promotes warning diagnostics as errors by default.
413    pub fn warnings_as_errors(&self) -> bool {
414        self.warnings_as_errors
415    }
416
417    /// Returns a reference to the kernel for this assembler.
418    ///
419    /// If the assembler was instantiated without a kernel, the internal kernel will be empty.
420    pub fn kernel(&self) -> &Kernel {
421        self.linker.kernel()
422    }
423
424    #[cfg(any(feature = "std", all(test, feature = "std")))]
425    pub(crate) fn source_manager(&self) -> Arc<dyn SourceManager> {
426        self.source_manager.clone()
427    }
428
429    #[cfg(any(test, feature = "testing"))]
430    #[doc(hidden)]
431    pub fn linker(&self) -> &Linker {
432        &self.linker
433    }
434}
435
436// ------------------------------------------------------------------------------------------------
437/// Compilation/Assembly
438impl Assembler {
439    /// Assembles a set of modules into a [Library].
440    ///
441    /// # Errors
442    ///
443    /// Returns an error if parsing or compilation of the specified modules fails.
444    pub fn assemble_library(
445        self,
446        modules: impl IntoIterator<Item = impl Parse>,
447    ) -> Result<Arc<Library>, Report> {
448        let modules = modules
449            .into_iter()
450            .map(|module| {
451                module.parse_with_options(
452                    self.source_manager.clone(),
453                    ParseOptions {
454                        warnings_as_errors: self.warnings_as_errors,
455                        ..ParseOptions::for_library()
456                    },
457                )
458            })
459            .collect::<Result<Vec<_>, Report>>()?;
460
461        Ok(self.assemble_library_modules(modules, TargetType::Library)?.into_artifact())
462    }
463
464    /// Assemble a [Library] from a standard Miden Assembly project layout, using the provided
465    /// [Path] as the root under which the project is rooted.
466    ///
467    /// The standard layout assumes that the given filesystem path corresponds to the root of
468    /// `namespace`. Modules will be parsed with their path made relative to `namespace` according
469    /// to their location in the directory structure with respect to `path`. See below for an
470    /// example of what this looks like in practice.
471    ///
472    /// The `namespace` can be any valid Miden Assembly path, e.g. `std` is a valid path, as is
473    /// `std::math::u64` - there is no requirement that the namespace be a single identifier. This
474    /// allows defining multiple projects relative to a common root namespace without conflict.
475    ///
476    /// NOTE: You must ensure there is no conflict in namespace between projects, e.g. two projects
477    /// both assembled with `namespace` set to `std::math` would conflict with each other in a way
478    /// that would prevent them from being used at the same time.
479    ///
480    /// This function recursively parses the entire directory structure under `path`, ignoring
481    /// any files which do not have the `.masm` extension.
482    ///
483    /// For example, let's say I call this function like so:
484    ///
485    /// ```rust
486    /// use miden_assembly::{Assembler, Path};
487    ///
488    /// Assembler::default().assemble_library_from_dir("~/masm/core", "miden::core::foo");
489    /// ```
490    ///
491    /// Here's how we would handle various files under this path:
492    ///
493    /// - ~/masm/core/sys.masm            -> Parsed as "miden::core::foo::sys"
494    /// - ~/masm/core/crypto/hash.masm    -> Parsed as "miden::core::foo::crypto::hash"
495    /// - ~/masm/core/math/u32.masm       -> Parsed as "miden::core::foo::math::u32"
496    /// - ~/masm/core/math/u64.masm       -> Parsed as "miden::core::foo::math::u64"
497    /// - ~/masm/core/math/README.md      -> Ignored
498    #[cfg(feature = "std")]
499    pub fn assemble_library_from_dir(
500        self,
501        dir: impl AsRef<std::path::Path>,
502        namespace: impl AsRef<Path>,
503    ) -> Result<Arc<Library>, Report> {
504        use miden_assembly_syntax::parser;
505
506        let dir = dir.as_ref();
507        let namespace = namespace.as_ref();
508
509        let source_manager = self.source_manager.clone();
510        let modules =
511            parser::read_modules_from_dir(dir, namespace, source_manager, self.warnings_as_errors)?;
512        self.assemble_library(modules)
513    }
514
515    /// Assembles the provided module into a [KernelLibrary] intended to be used as a Kernel.
516    ///
517    /// # Errors
518    ///
519    /// Returns an error if parsing or compilation of the specified modules fails.
520    pub fn assemble_kernel(self, module: impl Parse) -> Result<KernelLibrary, Report> {
521        let module = module.parse_with_options(
522            self.source_manager.clone(),
523            ParseOptions {
524                path: Some(Path::kernel_path().into()),
525                warnings_as_errors: self.warnings_as_errors,
526                ..ParseOptions::for_kernel()
527            },
528        )?;
529
530        self.assemble_kernel_module(module)?.into_kernel_library()
531    }
532
533    /// Assemble a [KernelLibrary] from a standard Miden Assembly kernel project layout.
534    ///
535    /// The kernel library will export procedures defined by the module at `sys_module_path`.
536    ///
537    /// If the optional `lib_dir` is provided, all modules under this directory will be available
538    /// from the kernel module under the `$kernel` namespace. For example, if `lib_dir` is set to
539    /// "~/masm/lib", the files will be accessible in the kernel module as follows:
540    ///
541    /// - ~/masm/lib/foo.masm        -> Can be imported as "$kernel::foo"
542    /// - ~/masm/lib/bar/baz.masm    -> Can be imported as "$kernel::bar::baz"
543    ///
544    /// Note: this is a temporary structure which will likely change once
545    /// <https://github.com/0xMiden/miden-vm/issues/1436> is implemented.
546    #[cfg(feature = "std")]
547    pub fn assemble_kernel_from_dir(
548        mut self,
549        sys_module_path: impl AsRef<std::path::Path>,
550        lib_dir: Option<impl AsRef<std::path::Path>>,
551    ) -> Result<KernelLibrary, Report> {
552        // if library directory is provided, add modules from this directory to the assembler
553        if let Some(lib_dir) = lib_dir {
554            self.compile_and_statically_link_from_dir(lib_dir, Path::kernel_path())?;
555        }
556
557        self.assemble_kernel(sys_module_path.as_ref())
558    }
559
560    /// Shared code used by both [`Self::assemble_library`] and [`Self::assemble_kernel`].
561    fn assemble_library_product(
562        mut self,
563        module_indices: &[ModuleIndex],
564        kind: TargetType,
565    ) -> Result<AssemblyProduct, Report> {
566        let staticlibs = self.linker.libraries().filter_map(|lib| {
567            if matches!(lib.linkage, Linkage::Static) {
568                Some(lib.mast.as_ref())
569            } else {
570                None
571            }
572        });
573        let mut mast_forest_builder = MastForestBuilder::new(staticlibs)?;
574        mast_forest_builder.set_emit_debug_info(self.emit_debug_info);
575        let mut exports = {
576            let mut exports = BTreeMap::new();
577
578            for module_idx in module_indices.iter().copied() {
579                let module = &self.linker[module_idx];
580
581                if let Some(advice_map) = module.advice_map() {
582                    mast_forest_builder.merge_advice_map(advice_map)?;
583                }
584
585                let module_kind = module.kind();
586                let module_path = module.path().clone();
587                for index in 0..module.symbols().len() {
588                    let index = ItemIndex::new(index);
589                    let gid = module_idx + index;
590
591                    let path: Arc<Path> = {
592                        let symbol = &self.linker[gid];
593                        if !symbol.visibility().is_public() {
594                            continue;
595                        }
596                        module_path
597                            .join(symbol.name())
598                            .canonicalize()
599                            .into_diagnostic()?
600                            .into_boxed_path()
601                            .into()
602                    };
603                    let export = self.export_symbol(
604                        gid,
605                        module_kind,
606                        path.clone(),
607                        &mut mast_forest_builder,
608                    )?;
609                    if exports.insert(path.clone(), export).is_some() {
610                        return Err(Report::new(AssemblerError::DuplicateExportPath { path }));
611                    }
612                }
613            }
614
615            exports
616        };
617
618        let (mast_forest, id_remappings) = mast_forest_builder.build();
619        for (_proc_name, export) in exports.iter_mut() {
620            match export {
621                LibraryExport::Procedure(export) => {
622                    if let Some(&new_node_id) = id_remappings.get(&export.node) {
623                        export.node = new_node_id;
624                    }
625                },
626                LibraryExport::Constant(_) | LibraryExport::Type(_) => (),
627            }
628        }
629
630        self.finish_library_product(mast_forest, exports, kind)
631    }
632
633    /// The purpose of this function is, for any given symbol in the set of modules being compiled
634    /// to a [Library], to generate a corresponding [LibraryExport] for that symbol.
635    ///
636    /// For procedures, this function is also responsible for compiling the procedure, and updating
637    /// the provided [MastForestBuilder] accordingly.
638    fn export_symbol(
639        &mut self,
640        gid: GlobalItemIndex,
641        module_kind: ModuleKind,
642        symbol_path: Arc<Path>,
643        mast_forest_builder: &mut MastForestBuilder,
644    ) -> Result<LibraryExport, Report> {
645        log::trace!(target: "assembler::export_symbol", "exporting {} {symbol_path}", match self.linker[gid].item() {
646            SymbolItem::Compiled(ItemInfo::Procedure(_)) => "compiled procedure",
647            SymbolItem::Compiled(ItemInfo::Constant(_)) => "compiled constant",
648            SymbolItem::Compiled(ItemInfo::Type(_)) => "compiled type",
649            SymbolItem::Procedure(_) => "procedure",
650            SymbolItem::Constant(_) => "constant",
651            SymbolItem::Type(_) => "type",
652            SymbolItem::Alias { .. } => "alias",
653        });
654        let mut cache = crate::linker::ResolverCache::default();
655        let export = match self.linker[gid].item() {
656            SymbolItem::Compiled(ItemInfo::Procedure(item)) => {
657                let resolved = match mast_forest_builder.get_procedure(gid) {
658                    Some(proc) => ResolvedProcedure {
659                        node: proc.body_node_id(),
660                        signature: proc.signature(),
661                    },
662                    // We didn't find the procedure in our current MAST forest. We still need to
663                    // check if it exists in one of a library dependency.
664                    None => {
665                        let node = self.ensure_valid_procedure_mast_root(
666                            InvokeKind::ProcRef,
667                            SourceSpan::UNKNOWN,
668                            item.digest,
669                            mast_forest_builder,
670                        )?;
671                        ResolvedProcedure { node, signature: item.signature.clone() }
672                    },
673                };
674                let digest = item.digest;
675                let ResolvedProcedure { node, signature } = resolved;
676                let attributes = item.attributes.clone();
677                let pctx = ProcedureContext::new(
678                    gid,
679                    /* is_program_entrypoint= */ false,
680                    symbol_path.clone(),
681                    Visibility::Public,
682                    signature.clone(),
683                    module_kind.is_kernel(),
684                    self.source_manager.clone(),
685                );
686
687                let procedure = pctx.into_procedure(digest, node);
688                self.linker.register_procedure_root(gid, digest)?;
689                mast_forest_builder.insert_procedure(gid, procedure)?;
690                LibraryExport::Procedure(ProcedureExport {
691                    node,
692                    path: symbol_path,
693                    signature: signature.map(|sig| (*sig).clone()),
694                    attributes,
695                })
696            },
697            SymbolItem::Compiled(ItemInfo::Constant(item)) => {
698                LibraryExport::Constant(ConstantExport {
699                    path: symbol_path,
700                    value: item.value.clone(),
701                })
702            },
703            SymbolItem::Compiled(ItemInfo::Type(item)) => {
704                LibraryExport::Type(TypeExport { path: symbol_path, ty: item.ty.clone() })
705            },
706            SymbolItem::Procedure(_) => {
707                self.compile_subgraph(SubgraphRoot::not_as_entrypoint(gid), mast_forest_builder)?;
708                let node = mast_forest_builder
709                    .get_procedure(gid)
710                    .expect("compilation succeeded but root not found in cache")
711                    .body_node_id();
712                let signature = self.linker.resolve_signature(gid)?;
713                let attributes = self.linker.resolve_attributes(gid)?;
714                LibraryExport::Procedure(ProcedureExport {
715                    node,
716                    path: symbol_path,
717                    signature: signature.map(Arc::unwrap_or_clone),
718                    attributes,
719                })
720            },
721            SymbolItem::Constant(item) => {
722                // Evaluate constant to a concrete value for export
723                let value = self.linker.const_eval(gid, &item.value, &mut cache)?;
724
725                LibraryExport::Constant(ConstantExport { path: symbol_path, value })
726            },
727            SymbolItem::Type(item) => {
728                let ty = self.linker.resolve_type(item.span(), gid)?;
729                LibraryExport::Type(TypeExport { path: symbol_path, ty })
730            },
731
732            SymbolItem::Alias { alias, resolved } => {
733                if let Some(resolved) = resolved.get() {
734                    return self.export_symbol(
735                        resolved,
736                        module_kind,
737                        symbol_path,
738                        mast_forest_builder,
739                    );
740                }
741
742                let Some(ResolvedProcedure { node, signature }) = self.resolve_target(
743                    InvokeKind::ProcRef,
744                    &alias.target().into(),
745                    gid,
746                    mast_forest_builder,
747                )?
748                else {
749                    return Err(self.unresolved_alias_report("export", &symbol_path, alias));
750                };
751
752                let digest = mast_forest_builder
753                    .get_mast_node(node)
754                    .expect("resolved alias export node must exist")
755                    .digest();
756                let pctx = ProcedureContext::new(
757                    gid,
758                    /* is_program_entrypoint= */ false,
759                    symbol_path.clone(),
760                    Visibility::Public,
761                    signature.clone(),
762                    module_kind.is_kernel(),
763                    self.source_manager.clone(),
764                );
765                let procedure = pctx.into_procedure(digest, node);
766                self.linker.register_procedure_root(gid, digest)?;
767                mast_forest_builder.insert_procedure(gid, procedure)?;
768
769                return Ok(LibraryExport::Procedure(ProcedureExport {
770                    node,
771                    path: symbol_path,
772                    signature: signature.map(Arc::unwrap_or_clone),
773                    attributes: Default::default(),
774                }));
775            },
776        };
777
778        Ok(export)
779    }
780
781    /// Compiles the provided module into a [`Program`]. The resulting program can be executed on
782    /// Miden VM.
783    ///
784    /// # Errors
785    ///
786    /// Returns an error if parsing or compilation of the specified program fails, or if the source
787    /// doesn't have an entrypoint.
788    pub fn assemble_program(self, source: impl Parse) -> Result<Program, Report> {
789        let options = ParseOptions {
790            kind: ModuleKind::Executable,
791            warnings_as_errors: self.warnings_as_errors,
792            path: Some(Path::exec_path().into()),
793        };
794
795        let program = source.parse_with_options(self.source_manager.clone(), options)?;
796        assert!(program.is_executable());
797
798        self.assemble_executable_modules(program, [])?.into_program()
799    }
800
801    pub(crate) fn assemble_library_modules(
802        mut self,
803        modules: impl IntoIterator<Item = Box<ast::Module>>,
804        kind: TargetType,
805    ) -> Result<AssemblyProduct, Report> {
806        let module_indices = self.linker.link(modules)?;
807        self.assemble_library_product(&module_indices, kind)
808    }
809
810    pub(crate) fn assemble_kernel_module(
811        mut self,
812        module: Box<ast::Module>,
813    ) -> Result<AssemblyProduct, Report> {
814        let module_indices = self.linker.link_kernel(module)?;
815        self.assemble_library_product(&module_indices, TargetType::Kernel)
816    }
817
818    pub(crate) fn assemble_executable_modules(
819        mut self,
820        program: Box<ast::Module>,
821        support_modules: impl IntoIterator<Item = Box<ast::Module>>,
822    ) -> Result<AssemblyProduct, Report> {
823        self.linker.link_modules(support_modules)?;
824
825        // Recompute graph with executable module, and start compiling
826        let module_index = self.linker.link([program])?[0];
827
828        // Find the executable entrypoint Note: it is safe to use `unwrap_ast()` here, since this is
829        // the module we just added, which is in AST representation.
830        let entrypoint = self.linker[module_index]
831            .symbols()
832            .position(|symbol| symbol.name().as_str() == Ident::MAIN)
833            .map(|index| module_index + ItemIndex::new(index))
834            .ok_or(SemanticAnalysisError::MissingEntrypoint)?;
835
836        // Compile the linked module graph rooted at the entrypoint
837        let staticlibs = self.linker.libraries().filter_map(|lib| {
838            if matches!(lib.linkage, Linkage::Static) {
839                Some(lib.mast.as_ref())
840            } else {
841                None
842            }
843        });
844        let mut mast_forest_builder = MastForestBuilder::new(staticlibs)?;
845        mast_forest_builder.set_emit_debug_info(self.emit_debug_info);
846
847        if let Some(advice_map) = self.linker[module_index].advice_map() {
848            mast_forest_builder.merge_advice_map(advice_map)?;
849        }
850
851        self.compile_subgraph(SubgraphRoot::with_entrypoint(entrypoint), &mut mast_forest_builder)?;
852        let entry_node_id = mast_forest_builder
853            .get_procedure(entrypoint)
854            .expect("compilation succeeded but root not found in cache")
855            .body_node_id();
856
857        // in case the node IDs changed, update the entrypoint ID to the new value
858        let (mast_forest, id_remappings) = mast_forest_builder.build();
859        let entry_node_id = *id_remappings.get(&entry_node_id).unwrap_or(&entry_node_id);
860
861        self.finish_program_product(mast_forest, entry_node_id, self.linker.kernel().clone())
862    }
863
864    fn finish_library_product(
865        &self,
866        mut mast_forest: miden_core::mast::MastForest,
867        exports: BTreeMap<Arc<Path>, LibraryExport>,
868        kind: TargetType,
869    ) -> Result<AssemblyProduct, Report> {
870        self.apply_debug_options(&mut mast_forest);
871
872        let library = Library::new(Arc::new(mast_forest), exports)?;
873        let manifest = PackageManifest::from_library(&library);
874        let debug_info = self.emit_debug_info.then(|| {
875            #[cfg_attr(not(feature = "std"), expect(unused_mut))]
876            let mut debug_info = self.debug_info.clone();
877            #[cfg(feature = "std")]
878            if let Some(trimmer) = self.source_path_trimmer() {
879                debug_info.trim_paths(&trimmer);
880            }
881            debug_info
882        });
883
884        Ok(AssemblyProduct::new(kind, Arc::new(library), None, manifest, debug_info))
885    }
886
887    fn finish_program_product(
888        &self,
889        mut mast_forest: miden_core::mast::MastForest,
890        entrypoint: MastNodeId,
891        kernel: Kernel,
892    ) -> Result<AssemblyProduct, Report> {
893        self.apply_debug_options(&mut mast_forest);
894
895        let mast = Arc::new(mast_forest);
896        let entry: Arc<Path> = Path::exec_path().join(ast::ProcedureName::MAIN_PROC_NAME).into();
897        let entrypoint = LibraryExport::Procedure(ProcedureExport {
898            node: entrypoint,
899            path: entry.clone(),
900            signature: None,
901            attributes: Default::default(),
902        });
903        let library = Arc::new(Library::new(mast, BTreeMap::from_iter([(entry, entrypoint)]))?);
904        let manifest = PackageManifest::from_library(&library);
905        let debug_info = self.emit_debug_info.then(|| {
906            #[cfg_attr(not(feature = "std"), expect(unused_mut))]
907            let mut debug_info = self.debug_info.clone();
908            #[cfg(feature = "std")]
909            if let Some(trimmer) = self.source_path_trimmer() {
910                debug_info.trim_paths(&trimmer);
911            }
912            debug_info
913        });
914
915        Ok(AssemblyProduct::new(
916            TargetType::Executable,
917            library,
918            Some(kernel),
919            manifest,
920            debug_info,
921        ))
922    }
923
924    fn apply_debug_options(&self, mast_forest: &mut miden_core::mast::MastForest) {
925        if !self.emit_debug_info {
926            mast_forest.clear_debug_info();
927            return;
928        }
929
930        if self.trim_paths {
931            #[cfg(feature = "std")]
932            if let Some(trimmer) = self.source_path_trimmer() {
933                mast_forest.debug_info_mut().rewrite_source_locations(
934                    |location| trimmer.trim_location(location),
935                    |location| trimmer.trim_file_line_col(location),
936                );
937            }
938        }
939    }
940
941    #[cfg(feature = "std")]
942    fn source_path_trimmer(&self) -> Option<debuginfo::SourcePathTrimmer> {
943        if !self.trim_paths {
944            return None;
945        }
946
947        std::env::current_dir().ok().map(debuginfo::SourcePathTrimmer::new)
948    }
949
950    /// Compile the uncompiled procedure in the linked module graph which are members of the
951    /// subgraph rooted at `root`, placing them in the MAST forest builder once compiled.
952    ///
953    /// Returns an error if any of the provided Miden Assembly is invalid.
954    fn compile_subgraph(
955        &mut self,
956        root: SubgraphRoot,
957        mast_forest_builder: &mut MastForestBuilder,
958    ) -> Result<(), Report> {
959        let mut worklist: Vec<GlobalItemIndex> = self
960            .linker
961            .topological_sort_from_root(root.proc_id)
962            .map_err(|cycle| {
963                let iter = cycle.into_node_ids();
964                let mut nodes = Vec::with_capacity(iter.len());
965                for node in iter {
966                    let module = self.linker[node.module].path();
967                    let proc = self.linker[node].name();
968                    nodes.push(format!("{}", module.join(proc)));
969                }
970                LinkerError::Cycle { nodes: nodes.into() }
971            })?
972            .into_iter()
973            .filter(|&gid| {
974                matches!(
975                    self.linker[gid].item(),
976                    SymbolItem::Procedure(_) | SymbolItem::Alias { .. }
977                )
978            })
979            .collect();
980
981        assert!(!worklist.is_empty());
982
983        self.process_graph_worklist(&mut worklist, &root, mast_forest_builder)
984    }
985
986    /// Compiles all procedures in the `worklist`.
987    fn process_graph_worklist(
988        &mut self,
989        worklist: &mut Vec<GlobalItemIndex>,
990        root: &SubgraphRoot,
991        mast_forest_builder: &mut MastForestBuilder,
992    ) -> Result<(), Report> {
993        // Process the topological ordering in reverse order (bottom-up), so that
994        // each procedure is compiled with all of its dependencies fully compiled
995        while let Some(procedure_gid) = worklist.pop() {
996            // If we have already compiled this procedure, do not recompile
997            if let Some(proc) = mast_forest_builder.get_procedure(procedure_gid) {
998                self.linker.register_procedure_root(procedure_gid, proc.mast_root())?;
999                continue;
1000            }
1001            // Fetch procedure metadata from the graph
1002            let (module_kind, module_path) = {
1003                let module = &self.linker[procedure_gid.module];
1004                (module.kind(), module.path().clone())
1005            };
1006            match self.linker[procedure_gid].item() {
1007                SymbolItem::Procedure(proc) => {
1008                    let proc = proc.borrow();
1009                    let num_locals = proc.num_locals();
1010                    let path = Arc::<Path>::from(module_path.join(proc.name().as_str()));
1011                    let signature = self.linker.resolve_signature(procedure_gid)?;
1012                    let is_program_entrypoint =
1013                        root.is_program_entrypoint && root.proc_id == procedure_gid;
1014
1015                    let pctx = ProcedureContext::new(
1016                        procedure_gid,
1017                        is_program_entrypoint,
1018                        path.clone(),
1019                        proc.visibility(),
1020                        signature.clone(),
1021                        module_kind.is_kernel(),
1022                        self.source_manager.clone(),
1023                    )
1024                    .with_num_locals(num_locals)
1025                    .with_span(proc.span());
1026
1027                    // Compile this procedure
1028                    let procedure = self.compile_procedure(pctx, mast_forest_builder)?;
1029                    // TODO: if a re-exported procedure with the same MAST root had been previously
1030                    // added to the builder, this will result in unreachable nodes added to the
1031                    // MAST forest. This is because while we won't insert a duplicate node for the
1032                    // procedure body node itself, all nodes that make up the procedure body would
1033                    // be added to the forest.
1034
1035                    // Record the debug info for this procedure
1036                    self.debug_info
1037                        .register_procedure_debug_info(&procedure, self.source_manager.as_ref())?;
1038
1039                    // Cache the compiled procedure
1040                    drop(proc);
1041                    self.linker.register_procedure_root(procedure_gid, procedure.mast_root())?;
1042                    mast_forest_builder.insert_procedure(procedure_gid, procedure)?;
1043                },
1044                SymbolItem::Alias { alias, resolved } => {
1045                    let path: Arc<Path> = module_path.join(alias.name().as_str()).into();
1046                    let procedure_gid = match resolved.get() {
1047                        Some(procedure_gid) => {
1048                            match self.linker[procedure_gid].item() {
1049                                SymbolItem::Procedure(_)
1050                                | SymbolItem::Compiled(ItemInfo::Procedure(_)) => {},
1051                                SymbolItem::Constant(_)
1052                                | SymbolItem::Type(_)
1053                                | SymbolItem::Compiled(_) => {
1054                                    continue;
1055                                },
1056                                // A resolved alias will always refer to a non-alias item, this is
1057                                // because when aliases are resolved, they are resolved
1058                                // recursively. Had the alias chain been cyclical, we would have
1059                                // raised an error already.
1060                                SymbolItem::Alias { .. } => unreachable!(),
1061                            }
1062                            procedure_gid
1063                        },
1064                        None => procedure_gid,
1065                    };
1066                    // A program entrypoint is never an alias
1067                    let is_program_entrypoint = false;
1068                    let mut pctx = ProcedureContext::new(
1069                        procedure_gid,
1070                        is_program_entrypoint,
1071                        path,
1072                        Visibility::Public,
1073                        None,
1074                        module_kind.is_kernel(),
1075                        self.source_manager.clone(),
1076                    )
1077                    .with_span(alias.span());
1078
1079                    // We must resolve aliases at this point to their real definition, in order to
1080                    // know whether we need to emit a MAST node for a foreign procedure item. If
1081                    // the aliased item is not a procedure, we can ignore the alias entirely.
1082                    let Some(ResolvedProcedure { node: proc_node_id, signature }) = self
1083                        .resolve_target(
1084                            InvokeKind::ProcRef,
1085                            &alias.target().into(),
1086                            procedure_gid,
1087                            mast_forest_builder,
1088                        )?
1089                    else {
1090                        continue;
1091                    };
1092
1093                    pctx.set_signature(signature);
1094
1095                    let proc_mast_root =
1096                        mast_forest_builder.get_mast_node(proc_node_id).unwrap().digest();
1097
1098                    let procedure = pctx.into_procedure(proc_mast_root, proc_node_id);
1099
1100                    // Make the MAST root available to all dependents
1101                    self.linker.register_procedure_root(procedure_gid, proc_mast_root)?;
1102                    mast_forest_builder.insert_procedure(procedure_gid, procedure)?;
1103                },
1104                SymbolItem::Compiled(_) | SymbolItem::Constant(_) | SymbolItem::Type(_) => {
1105                    // There is nothing to do for other items that might have edges in the graph
1106                },
1107            }
1108        }
1109
1110        Ok(())
1111    }
1112
1113    fn unresolved_alias_report(
1114        &self,
1115        action: &'static str,
1116        symbol_path: &Path,
1117        alias: &ast::Alias,
1118    ) -> Report {
1119        let span = alias.target().span();
1120        let reason = match alias.target() {
1121            ast::AliasTarget::MastRoot(_) => {
1122                "this digest target does not resolve to a known procedure"
1123            },
1124            ast::AliasTarget::Path(_) => "this alias target does not resolve to a concrete item",
1125        };
1126
1127        RelatedLabel::error(format!(
1128            "unable to {action} alias '{symbol_path}' targeting '{}'",
1129            alias.target()
1130        ))
1131        .with_labeled_span(span, reason)
1132        .with_help("aliases must resolve to a concrete item before they can be used")
1133        .with_source_file(self.source_manager.get(span.source_id()).ok())
1134        .into()
1135    }
1136
1137    /// Compiles a single Miden Assembly procedure to its MAST representation.
1138    fn compile_procedure(
1139        &self,
1140        mut proc_ctx: ProcedureContext,
1141        mast_forest_builder: &mut MastForestBuilder,
1142    ) -> Result<Procedure, Report> {
1143        // Make sure the current procedure context is available during codegen
1144        let gid = proc_ctx.id();
1145
1146        let num_locals = proc_ctx.num_locals();
1147
1148        let proc = match self.linker[gid].item() {
1149            SymbolItem::Procedure(proc) => proc.borrow(),
1150            _ => panic!("expected item to be a procedure AST"),
1151        };
1152        let body_wrapper = if proc_ctx.is_program_entrypoint() {
1153            assert!(num_locals == 0, "program entrypoint cannot have locals");
1154
1155            Some(BodyWrapper {
1156                prologue: fmp_initialization_sequence(),
1157                epilogue: Vec::new(),
1158            })
1159        } else if num_locals > 0 {
1160            Some(BodyWrapper {
1161                prologue: fmp_start_frame_sequence(num_locals),
1162                epilogue: fmp_end_frame_sequence(num_locals),
1163            })
1164        } else {
1165            None
1166        };
1167
1168        let proc_body_id =
1169            self.compile_body(proc.iter(), &mut proc_ctx, body_wrapper, mast_forest_builder, 0)?;
1170
1171        let proc_body_node = mast_forest_builder
1172            .get_mast_node(proc_body_id)
1173            .expect("no MAST node for compiled procedure");
1174        Ok(proc_ctx.into_procedure(proc_body_node.digest(), proc_body_id))
1175    }
1176
1177    /// Creates an assembly operation decorator for control flow nodes.
1178    fn create_asmop_decorator(
1179        &self,
1180        span: &SourceSpan,
1181        op_name: &str,
1182        proc_ctx: &ProcedureContext,
1183    ) -> AssemblyOp {
1184        let location = proc_ctx.source_manager().location(*span).ok();
1185        let context_name = proc_ctx.path().to_string();
1186        let num_cycles = 0;
1187        AssemblyOp::new(location, context_name, num_cycles, op_name.to_string())
1188    }
1189
1190    fn compile_body<'a, I>(
1191        &self,
1192        body: I,
1193        proc_ctx: &mut ProcedureContext,
1194        wrapper: Option<BodyWrapper>,
1195        mast_forest_builder: &mut MastForestBuilder,
1196        nesting_depth: usize,
1197    ) -> Result<MastNodeId, Report>
1198    where
1199        I: Iterator<Item = &'a ast::Op>,
1200    {
1201        use ast::Op;
1202
1203        let mut body_node_ids: Vec<MastNodeId> = Vec::new();
1204        let mut block_builder = BasicBlockBuilder::new(wrapper, mast_forest_builder);
1205
1206        for op in body {
1207            match op {
1208                Op::Inst(inst) => {
1209                    if let Some(node_id) =
1210                        self.compile_instruction(inst, &mut block_builder, proc_ctx)?
1211                    {
1212                        if let Some(basic_block_id) = block_builder.make_basic_block()? {
1213                            body_node_ids.push(basic_block_id);
1214                        } else if let Some(decorator_ids) = block_builder.drain_decorators() {
1215                            block_builder
1216                                .mast_forest_builder_mut()
1217                                .append_before_enter(node_id, decorator_ids)
1218                                .into_diagnostic()?;
1219                        }
1220
1221                        body_node_ids.push(node_id);
1222                    }
1223                },
1224
1225                Op::If { then_blk, else_blk, span } => {
1226                    if let Some(basic_block_id) = block_builder.make_basic_block()? {
1227                        body_node_ids.push(basic_block_id);
1228                    }
1229
1230                    let next_depth = nesting_depth + 1;
1231                    if next_depth > MAX_CONTROL_FLOW_NESTING {
1232                        return Err(Report::new(AssemblerError::ControlFlowNestingDepthExceeded {
1233                            span: *span,
1234                            source_file: proc_ctx.source_manager().get(span.source_id()).ok(),
1235                            max_depth: MAX_CONTROL_FLOW_NESTING,
1236                        }));
1237                    }
1238
1239                    let then_blk = self.compile_body(
1240                        then_blk.iter(),
1241                        proc_ctx,
1242                        None,
1243                        block_builder.mast_forest_builder_mut(),
1244                        next_depth,
1245                    )?;
1246                    let else_blk = self.compile_body(
1247                        else_blk.iter(),
1248                        proc_ctx,
1249                        None,
1250                        block_builder.mast_forest_builder_mut(),
1251                        next_depth,
1252                    )?;
1253
1254                    let asm_op = self.create_asmop_decorator(span, "if.true", proc_ctx);
1255                    let mut split_builder = SplitNodeBuilder::new([then_blk, else_blk]);
1256                    if let Some(decorator_ids) = block_builder.drain_decorators() {
1257                        split_builder.append_before_enter(decorator_ids);
1258                    }
1259
1260                    let split_node_id = block_builder
1261                        .mast_forest_builder_mut()
1262                        .ensure_node_with_asm_op(split_builder, asm_op)?;
1263
1264                    body_node_ids.push(split_node_id);
1265                },
1266
1267                Op::Repeat { count, body, span } => {
1268                    if let Some(basic_block_id) = block_builder.make_basic_block()? {
1269                        body_node_ids.push(basic_block_id);
1270                    }
1271
1272                    let next_depth = nesting_depth + 1;
1273                    if next_depth > MAX_CONTROL_FLOW_NESTING {
1274                        return Err(Report::new(AssemblerError::ControlFlowNestingDepthExceeded {
1275                            span: *span,
1276                            source_file: proc_ctx.source_manager().get(span.source_id()).ok(),
1277                            max_depth: MAX_CONTROL_FLOW_NESTING,
1278                        }));
1279                    }
1280
1281                    let repeat_node_id = self.compile_body(
1282                        body.iter(),
1283                        proc_ctx,
1284                        None,
1285                        block_builder.mast_forest_builder_mut(),
1286                        next_depth,
1287                    )?;
1288
1289                    let iteration_count = (*count).expect_value();
1290                    if iteration_count == 0 {
1291                        return Err(RelatedLabel::error("invalid repeat count")
1292                            .with_help("repeat count must be greater than 0")
1293                            .with_labeled_span(count.span(), "repeat count must be at least 1")
1294                            .with_source_file(
1295                                proc_ctx.source_manager().get(proc_ctx.span().source_id()).ok(),
1296                            )
1297                            .into());
1298                    }
1299                    if iteration_count > MAX_REPEAT_COUNT {
1300                        return Err(RelatedLabel::error("invalid repeat count")
1301                            .with_help(format!(
1302                                "repeat count must be less than or equal to {MAX_REPEAT_COUNT}",
1303                            ))
1304                            .with_labeled_span(
1305                                count.span(),
1306                                format!("repeat count exceeds {MAX_REPEAT_COUNT}"),
1307                            )
1308                            .with_source_file(
1309                                proc_ctx.source_manager().get(proc_ctx.span().source_id()).ok(),
1310                            )
1311                            .into());
1312                    }
1313
1314                    if let Some(decorator_ids) = block_builder.drain_decorators() {
1315                        // Attach decorators before the first iteration. We must carry the
1316                        // original node's external metadata into the dedup fingerprint,
1317                        // otherwise structurally identical nodes with different source mappings
1318                        // can alias.
1319                        let first_repeat_builder = block_builder.mast_forest_builder()
1320                            [repeat_node_id]
1321                            .clone()
1322                            .to_builder(block_builder.mast_forest_builder().mast_forest())
1323                            .with_before_enter(decorator_ids);
1324                        let first_repeat_node_id = block_builder
1325                            .mast_forest_builder_mut()
1326                            .ensure_node_preserving_debug_vars(
1327                                first_repeat_builder,
1328                                repeat_node_id,
1329                            )?;
1330
1331                        body_node_ids.push(first_repeat_node_id);
1332                        let remaining_iterations =
1333                            iteration_count.checked_sub(1).ok_or_else(|| {
1334                                Report::new(
1335                                    RelatedLabel::error("invalid repeat count")
1336                                        .with_help("repeat count must be greater than 0")
1337                                        .with_labeled_span(
1338                                            count.span(),
1339                                            "repeat count must be at least 1",
1340                                        )
1341                                        .with_source_file(
1342                                            proc_ctx
1343                                                .source_manager()
1344                                                .get(proc_ctx.span().source_id())
1345                                                .ok(),
1346                                        ),
1347                                )
1348                            })?;
1349                        for _ in 0..remaining_iterations {
1350                            body_node_ids.push(repeat_node_id);
1351                        }
1352                    } else {
1353                        for _ in 0..iteration_count {
1354                            body_node_ids.push(repeat_node_id);
1355                        }
1356                    }
1357                },
1358
1359                Op::While { body, span } => {
1360                    if let Some(basic_block_id) = block_builder.make_basic_block()? {
1361                        body_node_ids.push(basic_block_id);
1362                    }
1363
1364                    let next_depth = nesting_depth + 1;
1365                    if next_depth > MAX_CONTROL_FLOW_NESTING {
1366                        return Err(Report::new(AssemblerError::ControlFlowNestingDepthExceeded {
1367                            span: *span,
1368                            source_file: proc_ctx.source_manager().get(span.source_id()).ok(),
1369                            max_depth: MAX_CONTROL_FLOW_NESTING,
1370                        }));
1371                    }
1372
1373                    let loop_body_node_id = self.compile_body(
1374                        body.iter(),
1375                        proc_ctx,
1376                        None,
1377                        block_builder.mast_forest_builder_mut(),
1378                        next_depth,
1379                    )?;
1380                    let mut loop_builder = LoopNodeBuilder::new(loop_body_node_id);
1381                    if let Some(decorator_ids) = block_builder.drain_decorators() {
1382                        loop_builder.append_before_enter(decorator_ids);
1383                    }
1384
1385                    let asm_op = self.create_asmop_decorator(span, "while.true", proc_ctx);
1386                    let loop_node_id = block_builder
1387                        .mast_forest_builder_mut()
1388                        .ensure_node_with_asm_op(loop_builder, asm_op)?;
1389
1390                    body_node_ids.push(loop_node_id);
1391                },
1392            }
1393        }
1394
1395        let maybe_post_decorators: Option<Vec<DecoratorId>> =
1396            match block_builder.try_into_basic_block()? {
1397                BasicBlockOrDecorators::BasicBlock(basic_block_id) => {
1398                    body_node_ids.push(basic_block_id);
1399                    None
1400                },
1401                BasicBlockOrDecorators::Decorators(decorator_ids) => {
1402                    // the procedure body ends with a list of decorators
1403                    Some(decorator_ids)
1404                },
1405                BasicBlockOrDecorators::Nothing => None,
1406            };
1407
1408        let procedure_body_id = if body_node_ids.is_empty() {
1409            // We cannot allow only decorators in a procedure body, since decorators don't change
1410            // the MAST digest of a node. Hence, two empty procedures with different decorators
1411            // would look the same to the `MastForestBuilder`.
1412            if maybe_post_decorators.is_some() {
1413                return Err(Report::new(
1414                    RelatedLabel::error("invalid procedure")
1415                        .with_labeled_span(
1416                            proc_ctx.span(),
1417                            "body must contain at least one instruction if it has decorators",
1418                        )
1419                        .with_source_file(
1420                            proc_ctx.source_manager().get(proc_ctx.span().source_id()).ok(),
1421                        ),
1422                ));
1423            }
1424
1425            mast_forest_builder.ensure_block(
1426                vec![Operation::Noop],
1427                Vec::new(),
1428                vec![],
1429                vec![],
1430                vec![],
1431                vec![],
1432            )?
1433        } else {
1434            let asm_op = self.create_asmop_decorator(&proc_ctx.span(), "begin", proc_ctx);
1435            mast_forest_builder.join_nodes(body_node_ids, Some(asm_op))?
1436        };
1437
1438        // Make sure that any post decorators are added at the end of the procedure body
1439        if let Some(post_decorator_ids) = maybe_post_decorators {
1440            mast_forest_builder
1441                .append_after_exit(procedure_body_id, post_decorator_ids)
1442                .into_diagnostic()?;
1443        }
1444
1445        Ok(procedure_body_id)
1446    }
1447
1448    /// Resolves the specified target to the corresponding procedure root [`MastNodeId`].
1449    ///
1450    /// If the resolved target is a non-procedure item, this returns `Ok(None)`.
1451    ///
1452    /// If no [`MastNodeId`] exists for that procedure root, we wrap the root in an
1453    /// [`crate::mast::ExternalNode`], and return the resulting [`MastNodeId`].
1454    pub(super) fn resolve_target(
1455        &self,
1456        kind: InvokeKind,
1457        target: &InvocationTarget,
1458        caller_id: GlobalItemIndex,
1459        mast_forest_builder: &mut MastForestBuilder,
1460    ) -> Result<Option<ResolvedProcedure>, Report> {
1461        let caller = SymbolResolutionContext {
1462            span: target.span(),
1463            module: caller_id.module,
1464            kind: Some(kind),
1465        };
1466        let resolved = self.linker.resolve_invoke_target(&caller, target)?;
1467        match resolved {
1468            SymbolResolution::MastRoot(mast_root) => {
1469                let node = self.ensure_valid_procedure_mast_root(
1470                    kind,
1471                    target.span(),
1472                    mast_root.into_inner(),
1473                    mast_forest_builder,
1474                )?;
1475                Ok(Some(ResolvedProcedure { node, signature: None }))
1476            },
1477            SymbolResolution::Exact { gid, .. } => {
1478                match mast_forest_builder.get_procedure(gid) {
1479                    Some(proc) => Ok(Some(ResolvedProcedure {
1480                        node: proc.body_node_id(),
1481                        signature: proc.signature(),
1482                    })),
1483                    // We didn't find the procedure in our current MAST forest. We still need to
1484                    // check if it exists in one of a library dependency.
1485                    None => match self.linker[gid].item() {
1486                        SymbolItem::Compiled(ItemInfo::Procedure(p)) => {
1487                            let node = self.ensure_valid_procedure_mast_root(
1488                                kind,
1489                                target.span(),
1490                                p.digest,
1491                                mast_forest_builder,
1492                            )?;
1493                            Ok(Some(ResolvedProcedure { node, signature: p.signature.clone() }))
1494                        },
1495                        SymbolItem::Procedure(_) => panic!(
1496                            "AST procedure {gid:?} exists in the linker, but not in the MastForestBuilder"
1497                        ),
1498                        SymbolItem::Alias { .. } => {
1499                            unreachable!("unexpected reference to ast alias item from {gid:?}")
1500                        },
1501                        SymbolItem::Compiled(_) | SymbolItem::Type(_) | SymbolItem::Constant(_) => {
1502                            Ok(None)
1503                        },
1504                    },
1505                }
1506            },
1507            SymbolResolution::Module { .. }
1508            | SymbolResolution::External(_)
1509            | SymbolResolution::Local(_) => unreachable!(),
1510        }
1511    }
1512
1513    /// Verifies the validity of the MAST root as a procedure root hash, and adds it to the forest.
1514    ///
1515    /// If the root is present in the vendored MAST, its subtree is copied. Otherwise an
1516    /// external node is added to the forest.
1517    fn ensure_valid_procedure_mast_root(
1518        &self,
1519        kind: InvokeKind,
1520        span: SourceSpan,
1521        mast_root: Word,
1522        mast_forest_builder: &mut MastForestBuilder,
1523    ) -> Result<MastNodeId, Report> {
1524        // Get the procedure from the assembler
1525        let current_source_file = self.source_manager.get(span.source_id()).ok();
1526
1527        if matches!(kind, InvokeKind::SysCall) && self.linker.has_nonempty_kernel() {
1528            // NOTE: The assembler is expected to know the full set of all kernel
1529            // procedures at this point, so if the digest is not present in the kernel,
1530            // it is a definite error.
1531            if !self.linker.kernel().contains_proc(mast_root) {
1532                let callee = mast_forest_builder
1533                    .find_procedure_by_mast_root(&mast_root)
1534                    .map(|proc| proc.path().clone())
1535                    .unwrap_or_else(|| {
1536                        let digest_path = format!("{mast_root}");
1537                        Arc::<Path>::from(Path::new(&digest_path))
1538                    });
1539                return Err(Report::new(LinkerError::InvalidSysCallTarget {
1540                    span,
1541                    source_file: current_source_file,
1542                    callee,
1543                }));
1544            }
1545        }
1546
1547        mast_forest_builder.ensure_external_link(mast_root)
1548    }
1549}
1550
1551// HELPERS
1552// ================================================================================================
1553
1554/// Information about the root of a subgraph to be compiled.
1555///
1556/// `is_program_entrypoint` is true if the root procedure is the entrypoint of an executable
1557/// program.
1558struct SubgraphRoot {
1559    proc_id: GlobalItemIndex,
1560    is_program_entrypoint: bool,
1561}
1562
1563impl SubgraphRoot {
1564    fn with_entrypoint(proc_id: GlobalItemIndex) -> Self {
1565        Self { proc_id, is_program_entrypoint: true }
1566    }
1567
1568    fn not_as_entrypoint(proc_id: GlobalItemIndex) -> Self {
1569        Self { proc_id, is_program_entrypoint: false }
1570    }
1571}
1572
1573/// Contains a set of operations which need to be executed before and after a sequence of AST
1574/// nodes (i.e., code body).
1575pub(crate) struct BodyWrapper {
1576    pub prologue: Vec<Operation>,
1577    pub epilogue: Vec<Operation>,
1578}
1579
1580pub(super) struct ResolvedProcedure {
1581    pub node: MastNodeId,
1582    pub signature: Option<Arc<FunctionType>>,
1583}