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                            item.source_library_commitment(),
670                            item.source_root_id(),
671                            mast_forest_builder,
672                        )?;
673                        ResolvedProcedure { node, signature: item.signature.clone() }
674                    },
675                };
676                let digest = item.digest;
677                let ResolvedProcedure { node, signature } = resolved;
678                let attributes = item.attributes.clone();
679                let pctx = ProcedureContext::new(
680                    gid,
681                    /* is_program_entrypoint= */ false,
682                    symbol_path.clone(),
683                    Visibility::Public,
684                    signature.clone(),
685                    module_kind.is_kernel(),
686                    self.source_manager.clone(),
687                );
688
689                let procedure = pctx.into_procedure(digest, node);
690                self.linker.register_procedure_root(gid, digest)?;
691                mast_forest_builder.insert_procedure(gid, procedure)?;
692                LibraryExport::Procedure(ProcedureExport {
693                    node,
694                    path: symbol_path,
695                    signature: signature.map(|sig| (*sig).clone()),
696                    attributes,
697                })
698            },
699            SymbolItem::Compiled(ItemInfo::Constant(item)) => {
700                LibraryExport::Constant(ConstantExport {
701                    path: symbol_path,
702                    value: item.value.clone(),
703                })
704            },
705            SymbolItem::Compiled(ItemInfo::Type(item)) => {
706                LibraryExport::Type(TypeExport { path: symbol_path, ty: item.ty.clone() })
707            },
708            SymbolItem::Procedure(_) => {
709                self.compile_subgraph(SubgraphRoot::not_as_entrypoint(gid), mast_forest_builder)?;
710                let node = mast_forest_builder
711                    .get_procedure(gid)
712                    .expect("compilation succeeded but root not found in cache")
713                    .body_node_id();
714                let signature = self.linker.resolve_signature(gid)?;
715                let attributes = self.linker.resolve_attributes(gid)?;
716                LibraryExport::Procedure(ProcedureExport {
717                    node,
718                    path: symbol_path,
719                    signature: signature.map(Arc::unwrap_or_clone),
720                    attributes,
721                })
722            },
723            SymbolItem::Constant(item) => {
724                // Evaluate constant to a concrete value for export
725                let value = self.linker.const_eval(gid, &item.value, &mut cache)?;
726
727                LibraryExport::Constant(ConstantExport { path: symbol_path, value })
728            },
729            SymbolItem::Type(item) => {
730                let ty = self.linker.resolve_type(item.span(), gid)?;
731                LibraryExport::Type(TypeExport { path: symbol_path, ty })
732            },
733
734            SymbolItem::Alias { alias, resolved } => {
735                if let Some(resolved) = resolved.get() {
736                    return self.export_symbol(
737                        resolved,
738                        module_kind,
739                        symbol_path,
740                        mast_forest_builder,
741                    );
742                }
743
744                let Some(ResolvedProcedure { node, signature }) = self.resolve_target(
745                    InvokeKind::ProcRef,
746                    &alias.target().into(),
747                    gid,
748                    mast_forest_builder,
749                )?
750                else {
751                    return Err(self.unresolved_alias_report("export", &symbol_path, alias));
752                };
753
754                let digest = mast_forest_builder
755                    .get_mast_node(node)
756                    .expect("resolved alias export node must exist")
757                    .digest();
758                let pctx = ProcedureContext::new(
759                    gid,
760                    /* is_program_entrypoint= */ false,
761                    symbol_path.clone(),
762                    Visibility::Public,
763                    signature.clone(),
764                    module_kind.is_kernel(),
765                    self.source_manager.clone(),
766                );
767                let procedure = pctx.into_procedure(digest, node);
768                self.linker.register_procedure_root(gid, digest)?;
769                mast_forest_builder.insert_procedure(gid, procedure)?;
770
771                return Ok(LibraryExport::Procedure(ProcedureExport {
772                    node,
773                    path: symbol_path,
774                    signature: signature.map(Arc::unwrap_or_clone),
775                    attributes: Default::default(),
776                }));
777            },
778        };
779
780        Ok(export)
781    }
782
783    /// Compiles the provided module into a [`Program`]. The resulting program can be executed on
784    /// Miden VM.
785    ///
786    /// # Errors
787    ///
788    /// Returns an error if parsing or compilation of the specified program fails, or if the source
789    /// doesn't have an entrypoint.
790    pub fn assemble_program(self, source: impl Parse) -> Result<Program, Report> {
791        let options = ParseOptions {
792            kind: ModuleKind::Executable,
793            warnings_as_errors: self.warnings_as_errors,
794            path: Some(Path::exec_path().into()),
795        };
796
797        let program = source.parse_with_options(self.source_manager.clone(), options)?;
798        assert!(program.is_executable());
799
800        self.assemble_executable_modules(program, [])?.into_program()
801    }
802
803    pub(crate) fn assemble_library_modules(
804        mut self,
805        modules: impl IntoIterator<Item = Box<ast::Module>>,
806        kind: TargetType,
807    ) -> Result<AssemblyProduct, Report> {
808        let module_indices = self.linker.link(modules)?;
809        self.assemble_library_product(&module_indices, kind)
810    }
811
812    pub(crate) fn assemble_kernel_module(
813        mut self,
814        module: Box<ast::Module>,
815    ) -> Result<AssemblyProduct, Report> {
816        let module_indices = self.linker.link_kernel(module)?;
817        self.assemble_library_product(&module_indices, TargetType::Kernel)
818    }
819
820    pub(crate) fn assemble_executable_modules(
821        mut self,
822        program: Box<ast::Module>,
823        support_modules: impl IntoIterator<Item = Box<ast::Module>>,
824    ) -> Result<AssemblyProduct, Report> {
825        self.linker.link_modules(support_modules)?;
826
827        // Recompute graph with executable module, and start compiling
828        let module_index = self.linker.link([program])?[0];
829
830        // Find the executable entrypoint Note: it is safe to use `unwrap_ast()` here, since this is
831        // the module we just added, which is in AST representation.
832        let entrypoint = self.linker[module_index]
833            .symbols()
834            .position(|symbol| symbol.name().as_str() == Ident::MAIN)
835            .map(|index| module_index + ItemIndex::new(index))
836            .ok_or(SemanticAnalysisError::MissingEntrypoint)?;
837
838        // Compile the linked module graph rooted at the entrypoint
839        let staticlibs = self.linker.libraries().filter_map(|lib| {
840            if matches!(lib.linkage, Linkage::Static) {
841                Some(lib.mast.as_ref())
842            } else {
843                None
844            }
845        });
846        let mut mast_forest_builder = MastForestBuilder::new(staticlibs)?;
847        mast_forest_builder.set_emit_debug_info(self.emit_debug_info);
848
849        if let Some(advice_map) = self.linker[module_index].advice_map() {
850            mast_forest_builder.merge_advice_map(advice_map)?;
851        }
852
853        self.compile_subgraph(SubgraphRoot::with_entrypoint(entrypoint), &mut mast_forest_builder)?;
854        let entry_node_id = mast_forest_builder
855            .get_procedure(entrypoint)
856            .expect("compilation succeeded but root not found in cache")
857            .body_node_id();
858
859        // in case the node IDs changed, update the entrypoint ID to the new value
860        let (mast_forest, id_remappings) = mast_forest_builder.build();
861        let entry_node_id = *id_remappings.get(&entry_node_id).unwrap_or(&entry_node_id);
862
863        self.finish_program_product(mast_forest, entry_node_id, self.linker.kernel().clone())
864    }
865
866    fn finish_library_product(
867        &self,
868        mut mast_forest: miden_core::mast::MastForest,
869        exports: BTreeMap<Arc<Path>, LibraryExport>,
870        kind: TargetType,
871    ) -> Result<AssemblyProduct, Report> {
872        self.apply_debug_options(&mut mast_forest);
873
874        let library = Library::new(Arc::new(mast_forest), exports)?;
875        let manifest = PackageManifest::from_library(&library);
876        let debug_info = self.emit_debug_info.then(|| {
877            #[cfg_attr(not(feature = "std"), expect(unused_mut))]
878            let mut debug_info = self.debug_info.clone();
879            #[cfg(feature = "std")]
880            if let Some(trimmer) = self.source_path_trimmer() {
881                debug_info.trim_paths(&trimmer);
882            }
883            debug_info
884        });
885
886        Ok(AssemblyProduct::new(kind, Arc::new(library), None, manifest, debug_info))
887    }
888
889    fn finish_program_product(
890        &self,
891        mut mast_forest: miden_core::mast::MastForest,
892        entrypoint: MastNodeId,
893        kernel: Kernel,
894    ) -> Result<AssemblyProduct, Report> {
895        self.apply_debug_options(&mut mast_forest);
896
897        let mast = Arc::new(mast_forest);
898        let entry: Arc<Path> = Path::exec_path().join(ast::ProcedureName::MAIN_PROC_NAME).into();
899        let entrypoint = LibraryExport::Procedure(ProcedureExport {
900            node: entrypoint,
901            path: entry.clone(),
902            signature: None,
903            attributes: Default::default(),
904        });
905        let library = Arc::new(Library::new(mast, BTreeMap::from_iter([(entry, entrypoint)]))?);
906        let manifest = PackageManifest::from_library(&library);
907        let debug_info = self.emit_debug_info.then(|| {
908            #[cfg_attr(not(feature = "std"), expect(unused_mut))]
909            let mut debug_info = self.debug_info.clone();
910            #[cfg(feature = "std")]
911            if let Some(trimmer) = self.source_path_trimmer() {
912                debug_info.trim_paths(&trimmer);
913            }
914            debug_info
915        });
916
917        Ok(AssemblyProduct::new(
918            TargetType::Executable,
919            library,
920            Some(kernel),
921            manifest,
922            debug_info,
923        ))
924    }
925
926    fn apply_debug_options(&self, mast_forest: &mut miden_core::mast::MastForest) {
927        if !self.emit_debug_info {
928            mast_forest.clear_debug_info();
929            return;
930        }
931
932        if self.trim_paths {
933            #[cfg(feature = "std")]
934            if let Some(trimmer) = self.source_path_trimmer() {
935                mast_forest.debug_info_mut().rewrite_source_locations(
936                    |location| trimmer.trim_location(location),
937                    |location| trimmer.trim_file_line_col(location),
938                );
939            }
940        }
941    }
942
943    #[cfg(feature = "std")]
944    fn source_path_trimmer(&self) -> Option<debuginfo::SourcePathTrimmer> {
945        if !self.trim_paths {
946            return None;
947        }
948
949        std::env::current_dir().ok().map(debuginfo::SourcePathTrimmer::new)
950    }
951
952    /// Compile the uncompiled procedure in the linked module graph which are members of the
953    /// subgraph rooted at `root`, placing them in the MAST forest builder once compiled.
954    ///
955    /// Returns an error if any of the provided Miden Assembly is invalid.
956    fn compile_subgraph(
957        &mut self,
958        root: SubgraphRoot,
959        mast_forest_builder: &mut MastForestBuilder,
960    ) -> Result<(), Report> {
961        let mut worklist: Vec<GlobalItemIndex> = self
962            .linker
963            .topological_sort_from_root(root.proc_id)
964            .map_err(|cycle| {
965                let iter = cycle.into_node_ids();
966                let mut nodes = Vec::with_capacity(iter.len());
967                for node in iter {
968                    let module = self.linker[node.module].path();
969                    let proc = self.linker[node].name();
970                    nodes.push(format!("{}", module.join(proc)));
971                }
972                LinkerError::Cycle { nodes: nodes.into() }
973            })?
974            .into_iter()
975            .filter(|&gid| {
976                matches!(
977                    self.linker[gid].item(),
978                    SymbolItem::Procedure(_) | SymbolItem::Alias { .. }
979                )
980            })
981            .collect();
982
983        assert!(!worklist.is_empty());
984
985        self.process_graph_worklist(&mut worklist, &root, mast_forest_builder)
986    }
987
988    /// Compiles all procedures in the `worklist`.
989    fn process_graph_worklist(
990        &mut self,
991        worklist: &mut Vec<GlobalItemIndex>,
992        root: &SubgraphRoot,
993        mast_forest_builder: &mut MastForestBuilder,
994    ) -> Result<(), Report> {
995        // Process the topological ordering in reverse order (bottom-up), so that
996        // each procedure is compiled with all of its dependencies fully compiled
997        while let Some(procedure_gid) = worklist.pop() {
998            // If we have already compiled this procedure, do not recompile
999            if let Some(proc) = mast_forest_builder.get_procedure(procedure_gid) {
1000                self.linker.register_procedure_root(procedure_gid, proc.mast_root())?;
1001                continue;
1002            }
1003            // Fetch procedure metadata from the graph
1004            let (module_kind, module_path) = {
1005                let module = &self.linker[procedure_gid.module];
1006                (module.kind(), module.path().clone())
1007            };
1008            match self.linker[procedure_gid].item() {
1009                SymbolItem::Procedure(proc) => {
1010                    let proc = proc.borrow();
1011                    let num_locals = proc.num_locals();
1012                    let path = Arc::<Path>::from(module_path.join(proc.name().as_str()));
1013                    let signature = self.linker.resolve_signature(procedure_gid)?;
1014                    let is_program_entrypoint =
1015                        root.is_program_entrypoint && root.proc_id == procedure_gid;
1016
1017                    let pctx = ProcedureContext::new(
1018                        procedure_gid,
1019                        is_program_entrypoint,
1020                        path.clone(),
1021                        proc.visibility(),
1022                        signature.clone(),
1023                        module_kind.is_kernel(),
1024                        self.source_manager.clone(),
1025                    )
1026                    .with_num_locals(num_locals)
1027                    .with_span(proc.span());
1028
1029                    // Compile this procedure
1030                    let procedure = self.compile_procedure(pctx, mast_forest_builder)?;
1031                    // TODO: if a re-exported procedure with the same MAST root had been previously
1032                    // added to the builder, this will result in unreachable nodes added to the
1033                    // MAST forest. This is because while we won't insert a duplicate node for the
1034                    // procedure body node itself, all nodes that make up the procedure body would
1035                    // be added to the forest.
1036
1037                    // Record the debug info for this procedure
1038                    self.debug_info
1039                        .register_procedure_debug_info(&procedure, self.source_manager.as_ref())?;
1040
1041                    // Cache the compiled procedure
1042                    drop(proc);
1043                    self.linker.register_procedure_root(procedure_gid, procedure.mast_root())?;
1044                    mast_forest_builder.insert_procedure(procedure_gid, procedure)?;
1045                },
1046                SymbolItem::Alias { alias, resolved } => {
1047                    let path: Arc<Path> = module_path.join(alias.name().as_str()).into();
1048                    let procedure_gid = match resolved.get() {
1049                        Some(procedure_gid) => {
1050                            match self.linker[procedure_gid].item() {
1051                                SymbolItem::Procedure(_)
1052                                | SymbolItem::Compiled(ItemInfo::Procedure(_)) => {},
1053                                SymbolItem::Constant(_)
1054                                | SymbolItem::Type(_)
1055                                | SymbolItem::Compiled(_) => {
1056                                    continue;
1057                                },
1058                                // A resolved alias will always refer to a non-alias item, this is
1059                                // because when aliases are resolved, they are resolved
1060                                // recursively. Had the alias chain been cyclical, we would have
1061                                // raised an error already.
1062                                SymbolItem::Alias { .. } => unreachable!(),
1063                            }
1064                            procedure_gid
1065                        },
1066                        None => procedure_gid,
1067                    };
1068                    // A program entrypoint is never an alias
1069                    let is_program_entrypoint = false;
1070                    let mut pctx = ProcedureContext::new(
1071                        procedure_gid,
1072                        is_program_entrypoint,
1073                        path,
1074                        Visibility::Public,
1075                        None,
1076                        module_kind.is_kernel(),
1077                        self.source_manager.clone(),
1078                    )
1079                    .with_span(alias.span());
1080
1081                    // We must resolve aliases at this point to their real definition, in order to
1082                    // know whether we need to emit a MAST node for a foreign procedure item. If
1083                    // the aliased item is not a procedure, we can ignore the alias entirely.
1084                    let Some(ResolvedProcedure { node: proc_node_id, signature }) = self
1085                        .resolve_target(
1086                            InvokeKind::ProcRef,
1087                            &alias.target().into(),
1088                            procedure_gid,
1089                            mast_forest_builder,
1090                        )?
1091                    else {
1092                        continue;
1093                    };
1094
1095                    pctx.set_signature(signature);
1096
1097                    let proc_mast_root =
1098                        mast_forest_builder.get_mast_node(proc_node_id).unwrap().digest();
1099
1100                    let procedure = pctx.into_procedure(proc_mast_root, proc_node_id);
1101
1102                    // Make the MAST root available to all dependents
1103                    self.linker.register_procedure_root(procedure_gid, proc_mast_root)?;
1104                    mast_forest_builder.insert_procedure(procedure_gid, procedure)?;
1105                },
1106                SymbolItem::Compiled(_) | SymbolItem::Constant(_) | SymbolItem::Type(_) => {
1107                    // There is nothing to do for other items that might have edges in the graph
1108                },
1109            }
1110        }
1111
1112        Ok(())
1113    }
1114
1115    fn unresolved_alias_report(
1116        &self,
1117        action: &'static str,
1118        symbol_path: &Path,
1119        alias: &ast::Alias,
1120    ) -> Report {
1121        let span = alias.target().span();
1122        let reason = match alias.target() {
1123            ast::AliasTarget::MastRoot(_) => {
1124                "this digest target does not resolve to a known procedure"
1125            },
1126            ast::AliasTarget::Path(_) => "this alias target does not resolve to a concrete item",
1127        };
1128
1129        RelatedLabel::error(format!(
1130            "unable to {action} alias '{symbol_path}' targeting '{}'",
1131            alias.target()
1132        ))
1133        .with_labeled_span(span, reason)
1134        .with_help("aliases must resolve to a concrete item before they can be used")
1135        .with_source_file(self.source_manager.get(span.source_id()).ok())
1136        .into()
1137    }
1138
1139    /// Compiles a single Miden Assembly procedure to its MAST representation.
1140    fn compile_procedure(
1141        &self,
1142        mut proc_ctx: ProcedureContext,
1143        mast_forest_builder: &mut MastForestBuilder,
1144    ) -> Result<Procedure, Report> {
1145        // Make sure the current procedure context is available during codegen
1146        let gid = proc_ctx.id();
1147
1148        let num_locals = proc_ctx.num_locals();
1149
1150        let proc = match self.linker[gid].item() {
1151            SymbolItem::Procedure(proc) => proc.borrow(),
1152            _ => panic!("expected item to be a procedure AST"),
1153        };
1154        let body_wrapper = if proc_ctx.is_program_entrypoint() {
1155            assert!(num_locals == 0, "program entrypoint cannot have locals");
1156
1157            Some(BodyWrapper {
1158                prologue: fmp_initialization_sequence(),
1159                epilogue: Vec::new(),
1160            })
1161        } else if num_locals > 0 {
1162            Some(BodyWrapper {
1163                prologue: fmp_start_frame_sequence(num_locals),
1164                epilogue: fmp_end_frame_sequence(num_locals),
1165            })
1166        } else {
1167            None
1168        };
1169
1170        let proc_body_id =
1171            self.compile_body(proc.iter(), &mut proc_ctx, body_wrapper, mast_forest_builder, 0)?;
1172
1173        let proc_body_node = mast_forest_builder
1174            .get_mast_node(proc_body_id)
1175            .expect("no MAST node for compiled procedure");
1176        Ok(proc_ctx.into_procedure(proc_body_node.digest(), proc_body_id))
1177    }
1178
1179    /// Creates an assembly operation decorator for control flow nodes.
1180    fn create_asmop_decorator(
1181        &self,
1182        span: &SourceSpan,
1183        op_name: &str,
1184        proc_ctx: &ProcedureContext,
1185    ) -> AssemblyOp {
1186        let location = proc_ctx.source_manager().location(*span).ok();
1187        let context_name = proc_ctx.path().to_string();
1188        let num_cycles = 0;
1189        AssemblyOp::new(location, context_name, num_cycles, op_name.to_string())
1190    }
1191
1192    fn compile_body<'a, I>(
1193        &self,
1194        body: I,
1195        proc_ctx: &mut ProcedureContext,
1196        wrapper: Option<BodyWrapper>,
1197        mast_forest_builder: &mut MastForestBuilder,
1198        nesting_depth: usize,
1199    ) -> Result<MastNodeId, Report>
1200    where
1201        I: Iterator<Item = &'a ast::Op>,
1202    {
1203        use ast::Op;
1204
1205        let mut body_node_ids: Vec<MastNodeId> = Vec::new();
1206        let mut block_builder = BasicBlockBuilder::new(wrapper, mast_forest_builder);
1207
1208        for op in body {
1209            match op {
1210                Op::Inst(inst) => {
1211                    if let Some(node_id) =
1212                        self.compile_instruction(inst, &mut block_builder, proc_ctx)?
1213                    {
1214                        if let Some(basic_block_id) = block_builder.make_basic_block()? {
1215                            body_node_ids.push(basic_block_id);
1216                        } else if let Some(decorator_ids) = block_builder.drain_decorators() {
1217                            block_builder
1218                                .mast_forest_builder_mut()
1219                                .append_before_enter(node_id, decorator_ids)
1220                                .into_diagnostic()?;
1221                        }
1222
1223                        body_node_ids.push(node_id);
1224                    }
1225                },
1226
1227                Op::If { then_blk, else_blk, span } => {
1228                    if let Some(basic_block_id) = block_builder.make_basic_block()? {
1229                        body_node_ids.push(basic_block_id);
1230                    }
1231
1232                    let next_depth = nesting_depth + 1;
1233                    if next_depth > MAX_CONTROL_FLOW_NESTING {
1234                        return Err(Report::new(AssemblerError::ControlFlowNestingDepthExceeded {
1235                            span: *span,
1236                            source_file: proc_ctx.source_manager().get(span.source_id()).ok(),
1237                            max_depth: MAX_CONTROL_FLOW_NESTING,
1238                        }));
1239                    }
1240
1241                    let then_blk = self.compile_body(
1242                        then_blk.iter(),
1243                        proc_ctx,
1244                        None,
1245                        block_builder.mast_forest_builder_mut(),
1246                        next_depth,
1247                    )?;
1248                    let else_blk = self.compile_body(
1249                        else_blk.iter(),
1250                        proc_ctx,
1251                        None,
1252                        block_builder.mast_forest_builder_mut(),
1253                        next_depth,
1254                    )?;
1255
1256                    let asm_op = self.create_asmop_decorator(span, "if.true", proc_ctx);
1257                    let mut split_builder = SplitNodeBuilder::new([then_blk, else_blk]);
1258                    if let Some(decorator_ids) = block_builder.drain_decorators() {
1259                        split_builder.append_before_enter(decorator_ids);
1260                    }
1261
1262                    let split_node_id = block_builder
1263                        .mast_forest_builder_mut()
1264                        .ensure_node_with_asm_op(split_builder, asm_op)?;
1265
1266                    body_node_ids.push(split_node_id);
1267                },
1268
1269                Op::Repeat { count, body, span } => {
1270                    if let Some(basic_block_id) = block_builder.make_basic_block()? {
1271                        body_node_ids.push(basic_block_id);
1272                    }
1273
1274                    let next_depth = nesting_depth + 1;
1275                    if next_depth > MAX_CONTROL_FLOW_NESTING {
1276                        return Err(Report::new(AssemblerError::ControlFlowNestingDepthExceeded {
1277                            span: *span,
1278                            source_file: proc_ctx.source_manager().get(span.source_id()).ok(),
1279                            max_depth: MAX_CONTROL_FLOW_NESTING,
1280                        }));
1281                    }
1282
1283                    let repeat_node_id = self.compile_body(
1284                        body.iter(),
1285                        proc_ctx,
1286                        None,
1287                        block_builder.mast_forest_builder_mut(),
1288                        next_depth,
1289                    )?;
1290
1291                    let iteration_count = (*count).expect_value();
1292                    if iteration_count == 0 {
1293                        return Err(RelatedLabel::error("invalid repeat count")
1294                            .with_help("repeat count must be greater than 0")
1295                            .with_labeled_span(count.span(), "repeat count must be at least 1")
1296                            .with_source_file(
1297                                proc_ctx.source_manager().get(proc_ctx.span().source_id()).ok(),
1298                            )
1299                            .into());
1300                    }
1301                    if iteration_count > MAX_REPEAT_COUNT {
1302                        return Err(RelatedLabel::error("invalid repeat count")
1303                            .with_help(format!(
1304                                "repeat count must be less than or equal to {MAX_REPEAT_COUNT}",
1305                            ))
1306                            .with_labeled_span(
1307                                count.span(),
1308                                format!("repeat count exceeds {MAX_REPEAT_COUNT}"),
1309                            )
1310                            .with_source_file(
1311                                proc_ctx.source_manager().get(proc_ctx.span().source_id()).ok(),
1312                            )
1313                            .into());
1314                    }
1315
1316                    if let Some(decorator_ids) = block_builder.drain_decorators() {
1317                        // Attach decorators before the first iteration. We must carry the
1318                        // original node's external metadata into the dedup fingerprint,
1319                        // otherwise structurally identical nodes with different source mappings
1320                        // can alias.
1321                        let first_repeat_builder = block_builder.mast_forest_builder()
1322                            [repeat_node_id]
1323                            .clone()
1324                            .to_builder(block_builder.mast_forest_builder().mast_forest())
1325                            .with_before_enter(decorator_ids);
1326                        let first_repeat_node_id = block_builder
1327                            .mast_forest_builder_mut()
1328                            .ensure_node_preserving_debug_vars(
1329                                first_repeat_builder,
1330                                repeat_node_id,
1331                            )?;
1332
1333                        body_node_ids.push(first_repeat_node_id);
1334                        let remaining_iterations =
1335                            iteration_count.checked_sub(1).ok_or_else(|| {
1336                                Report::new(
1337                                    RelatedLabel::error("invalid repeat count")
1338                                        .with_help("repeat count must be greater than 0")
1339                                        .with_labeled_span(
1340                                            count.span(),
1341                                            "repeat count must be at least 1",
1342                                        )
1343                                        .with_source_file(
1344                                            proc_ctx
1345                                                .source_manager()
1346                                                .get(proc_ctx.span().source_id())
1347                                                .ok(),
1348                                        ),
1349                                )
1350                            })?;
1351                        for _ in 0..remaining_iterations {
1352                            body_node_ids.push(repeat_node_id);
1353                        }
1354                    } else {
1355                        for _ in 0..iteration_count {
1356                            body_node_ids.push(repeat_node_id);
1357                        }
1358                    }
1359                },
1360
1361                Op::While { body, span } => {
1362                    if let Some(basic_block_id) = block_builder.make_basic_block()? {
1363                        body_node_ids.push(basic_block_id);
1364                    }
1365
1366                    let next_depth = nesting_depth + 1;
1367                    if next_depth > MAX_CONTROL_FLOW_NESTING {
1368                        return Err(Report::new(AssemblerError::ControlFlowNestingDepthExceeded {
1369                            span: *span,
1370                            source_file: proc_ctx.source_manager().get(span.source_id()).ok(),
1371                            max_depth: MAX_CONTROL_FLOW_NESTING,
1372                        }));
1373                    }
1374
1375                    let loop_body_node_id = self.compile_body(
1376                        body.iter(),
1377                        proc_ctx,
1378                        None,
1379                        block_builder.mast_forest_builder_mut(),
1380                        next_depth,
1381                    )?;
1382                    let mut loop_builder = LoopNodeBuilder::new(loop_body_node_id);
1383                    if let Some(decorator_ids) = block_builder.drain_decorators() {
1384                        loop_builder.append_before_enter(decorator_ids);
1385                    }
1386
1387                    let asm_op = self.create_asmop_decorator(span, "while.true", proc_ctx);
1388                    let loop_node_id = block_builder
1389                        .mast_forest_builder_mut()
1390                        .ensure_node_with_asm_op(loop_builder, asm_op)?;
1391
1392                    body_node_ids.push(loop_node_id);
1393                },
1394            }
1395        }
1396
1397        let maybe_post_decorators: Option<Vec<DecoratorId>> =
1398            match block_builder.try_into_basic_block()? {
1399                BasicBlockOrDecorators::BasicBlock(basic_block_id) => {
1400                    body_node_ids.push(basic_block_id);
1401                    None
1402                },
1403                BasicBlockOrDecorators::Decorators(decorator_ids) => {
1404                    // the procedure body ends with a list of decorators
1405                    Some(decorator_ids)
1406                },
1407                BasicBlockOrDecorators::Nothing => None,
1408            };
1409
1410        let procedure_body_id = if body_node_ids.is_empty() {
1411            // We cannot allow only decorators in a procedure body, since decorators don't change
1412            // the MAST digest of a node. Hence, two empty procedures with different decorators
1413            // would look the same to the `MastForestBuilder`.
1414            if maybe_post_decorators.is_some() {
1415                return Err(Report::new(
1416                    RelatedLabel::error("invalid procedure")
1417                        .with_labeled_span(
1418                            proc_ctx.span(),
1419                            "body must contain at least one instruction if it has decorators",
1420                        )
1421                        .with_source_file(
1422                            proc_ctx.source_manager().get(proc_ctx.span().source_id()).ok(),
1423                        ),
1424                ));
1425            }
1426
1427            mast_forest_builder.ensure_block(
1428                vec![Operation::Noop],
1429                Vec::new(),
1430                vec![],
1431                vec![],
1432                vec![],
1433                vec![],
1434            )?
1435        } else {
1436            let asm_op = self.create_asmop_decorator(&proc_ctx.span(), "begin", proc_ctx);
1437            mast_forest_builder.join_nodes(body_node_ids, Some(asm_op))?
1438        };
1439
1440        // Make sure that any post decorators are added at the end of the procedure body
1441        if let Some(post_decorator_ids) = maybe_post_decorators {
1442            mast_forest_builder
1443                .append_after_exit(procedure_body_id, post_decorator_ids)
1444                .into_diagnostic()?;
1445        }
1446
1447        Ok(procedure_body_id)
1448    }
1449
1450    /// Resolves the specified target to the corresponding procedure root [`MastNodeId`].
1451    ///
1452    /// If the resolved target is a non-procedure item, this returns `Ok(None)`.
1453    ///
1454    /// If no [`MastNodeId`] exists for that procedure root, we wrap the root in an
1455    /// [`crate::mast::ExternalNode`], and return the resulting [`MastNodeId`].
1456    pub(super) fn resolve_target(
1457        &self,
1458        kind: InvokeKind,
1459        target: &InvocationTarget,
1460        caller_id: GlobalItemIndex,
1461        mast_forest_builder: &mut MastForestBuilder,
1462    ) -> Result<Option<ResolvedProcedure>, Report> {
1463        let caller = SymbolResolutionContext {
1464            span: target.span(),
1465            module: caller_id.module,
1466            kind: Some(kind),
1467        };
1468        let resolved = self.linker.resolve_invoke_target(&caller, target)?;
1469        match resolved {
1470            SymbolResolution::MastRoot(mast_root) => {
1471                // Literal MAST-root references in MASM do not carry any source-level provenance,
1472                // so there is no exact source root or source package commitment to thread through
1473                // here. We could try to guess based on linked libraries, but any such heuristic
1474                // would be ambiguous when multiple procedures share the same digest.
1475                let node = self.ensure_valid_procedure_mast_root(
1476                    kind,
1477                    target.span(),
1478                    mast_root.into_inner(),
1479                    None,
1480                    None,
1481                    mast_forest_builder,
1482                )?;
1483                Ok(Some(ResolvedProcedure { node, signature: None }))
1484            },
1485            SymbolResolution::Exact { gid, .. } => {
1486                match mast_forest_builder.get_procedure(gid) {
1487                    Some(proc) => Ok(Some(ResolvedProcedure {
1488                        node: proc.body_node_id(),
1489                        signature: proc.signature(),
1490                    })),
1491                    // We didn't find the procedure in our current MAST forest. We still need to
1492                    // check if it exists in one of a library dependency.
1493                    None => match self.linker[gid].item() {
1494                        SymbolItem::Compiled(ItemInfo::Procedure(p)) => {
1495                            let node = self.ensure_valid_procedure_mast_root(
1496                                kind,
1497                                target.span(),
1498                                p.digest,
1499                                p.source_library_commitment(),
1500                                p.source_root_id(),
1501                                mast_forest_builder,
1502                            )?;
1503                            Ok(Some(ResolvedProcedure { node, signature: p.signature.clone() }))
1504                        },
1505                        SymbolItem::Procedure(_) => panic!(
1506                            "AST procedure {gid:?} exists in the linker, but not in the MastForestBuilder"
1507                        ),
1508                        SymbolItem::Alias { .. } => {
1509                            unreachable!("unexpected reference to ast alias item from {gid:?}")
1510                        },
1511                        SymbolItem::Compiled(_) | SymbolItem::Type(_) | SymbolItem::Constant(_) => {
1512                            Ok(None)
1513                        },
1514                    },
1515                }
1516            },
1517            SymbolResolution::Module { .. }
1518            | SymbolResolution::External(_)
1519            | SymbolResolution::Local(_) => unreachable!(),
1520        }
1521    }
1522
1523    /// Verifies the validity of the MAST root as a procedure root hash, and adds it to the forest.
1524    ///
1525    /// If the root is present in the vendored MAST, its subtree is copied. Otherwise an
1526    /// external node is added to the forest.
1527    fn ensure_valid_procedure_mast_root(
1528        &self,
1529        kind: InvokeKind,
1530        span: SourceSpan,
1531        mast_root: Word,
1532        source_library_commitment: Option<Word>,
1533        source_root_id: Option<MastNodeId>,
1534        mast_forest_builder: &mut MastForestBuilder,
1535    ) -> Result<MastNodeId, Report> {
1536        // Get the procedure from the assembler
1537        let current_source_file = self.source_manager.get(span.source_id()).ok();
1538
1539        if matches!(kind, InvokeKind::SysCall) && self.linker.has_nonempty_kernel() {
1540            // NOTE: The assembler is expected to know the full set of all kernel
1541            // procedures at this point, so if the digest is not present in the kernel,
1542            // it is a definite error.
1543            if !self.linker.kernel().contains_proc(mast_root) {
1544                let callee = mast_forest_builder
1545                    .find_procedure_by_mast_root(&mast_root)
1546                    .map(|proc| proc.path().clone())
1547                    .unwrap_or_else(|| {
1548                        let digest_path = format!("{mast_root}");
1549                        Arc::<Path>::from(Path::new(&digest_path))
1550                    });
1551                return Err(Report::new(LinkerError::InvalidSysCallTarget {
1552                    span,
1553                    source_file: current_source_file,
1554                    callee,
1555                }));
1556            }
1557        }
1558
1559        mast_forest_builder.ensure_external_link_with_source(
1560            mast_root,
1561            source_library_commitment,
1562            source_root_id,
1563        )
1564    }
1565}
1566
1567// HELPERS
1568// ================================================================================================
1569
1570/// Information about the root of a subgraph to be compiled.
1571///
1572/// `is_program_entrypoint` is true if the root procedure is the entrypoint of an executable
1573/// program.
1574struct SubgraphRoot {
1575    proc_id: GlobalItemIndex,
1576    is_program_entrypoint: bool,
1577}
1578
1579impl SubgraphRoot {
1580    fn with_entrypoint(proc_id: GlobalItemIndex) -> Self {
1581        Self { proc_id, is_program_entrypoint: true }
1582    }
1583
1584    fn not_as_entrypoint(proc_id: GlobalItemIndex) -> Self {
1585        Self { proc_id, is_program_entrypoint: false }
1586    }
1587}
1588
1589/// Contains a set of operations which need to be executed before and after a sequence of AST
1590/// nodes (i.e., code body).
1591pub(crate) struct BodyWrapper {
1592    pub prologue: Vec<Operation>,
1593    pub epilogue: Vec<Operation>,
1594}
1595
1596pub(super) struct ResolvedProcedure {
1597    pub node: MastNodeId,
1598    pub signature: Option<Arc<FunctionType>>,
1599}