Skip to main content

miden_mast_package/package/
mod.rs

1mod id;
2mod manifest;
3mod section;
4#[cfg(test)]
5mod seed_gen;
6mod serialization;
7mod target_type;
8
9use alloc::{
10    boxed::Box,
11    format,
12    string::{String, ToString},
13    sync::Arc,
14    vec::Vec,
15};
16
17use miden_assembly_syntax::{
18    KernelLibrary, Library, Report, ast::QualifiedProcedureName, library::ModuleInfo,
19};
20use miden_core::{Word, program::Kernel, serde::Deserializable};
21#[cfg(feature = "serde")]
22use serde::{Deserialize, Serialize};
23
24pub use self::{
25    id::PackageId,
26    manifest::{
27        ConstantExport, ManifestValidationError, PackageExport, PackageManifest, ProcedureExport,
28        TypeExport,
29    },
30    section::{InvalidSectionIdError, Section, SectionId},
31    target_type::{InvalidTargetTypeError, TargetType},
32};
33use crate::{Dependency, Version};
34
35// PACKAGE
36// ================================================================================================
37
38/// A package is a assembled artifact containing:
39///
40/// * Basic metadata like name, description, and semantic version
41/// * The type of target the package represents, e.g. a library or executable
42/// * The assembled [miden_core::mast::MastForest] for that target
43/// * A manifest describing the exported contents of the package, and its runtime dependencies.
44/// * One or more custom sections containing metadata produced by the assembler or other tools which
45///   applies to the package, e.g. debug symbols.
46#[derive(Debug, Clone, Eq, PartialEq)]
47#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
48pub struct Package {
49    /// Name of the package
50    pub name: PackageId,
51    /// An optional semantic version for the package
52    pub version: Version,
53    /// An optional description of the package
54    #[cfg_attr(feature = "serde", serde(default))]
55    pub description: Option<String>,
56    /// The project target type which produced this package
57    pub kind: TargetType,
58    /// The underlying [Library] of this package
59    ///
60    /// NOTE: This will change to `MastForest` soon. We are currently using `Library` because we
61    /// have not yet fully removed the usage of `Library` throughout the assembler, so it is more
62    /// convenient to use. However, this can change at any time, so you should avoid accessing
63    /// this field directly unless you absolutely need to and can handle potential breakage.
64    pub mast: Arc<Library>,
65    /// The package manifest, containing the set of exported procedures and their signatures,
66    /// if known.
67    pub manifest: PackageManifest,
68    /// The set of custom sections included with the package, e.g. debug information, account
69    /// metadata, etc.
70    #[cfg_attr(feature = "serde", serde(default))]
71    pub sections: Vec<Section>,
72}
73
74/// Construction
75impl Package {
76    /// Construct a [Package] from a [Library] by providing the necessary metadata.
77    pub fn from_library(
78        name: PackageId,
79        version: Version,
80        kind: TargetType,
81        library: Arc<Library>,
82        dependencies: impl IntoIterator<Item = Dependency>,
83    ) -> Box<Package> {
84        let manifest = PackageManifest::from_library(&library)
85            .with_dependencies(dependencies)
86            .expect("package dependencies should be unique");
87
88        Box::new(Self {
89            name,
90            version,
91            description: None,
92            kind,
93            mast: library,
94            manifest,
95            sections: Vec::new(),
96        })
97    }
98}
99
100/// Accessors
101impl Package {
102    /// The file extension given to serialized packages
103    pub const EXTENSION: &str = "masp";
104
105    /// Returns the digest of the package's MAST artifact
106    pub fn digest(&self) -> Word {
107        *self.mast.digest()
108    }
109
110    /// Returns true if this package was produced for an executable target
111    pub fn is_program(&self) -> bool {
112        self.kind.is_executable()
113    }
114
115    /// Returns true if this package was produced for a library or kernel target
116    pub fn is_library(&self) -> bool {
117        self.kind.is_library()
118    }
119
120    /// Returns true if this package was produced specifically for a kernel target
121    pub fn is_kernel(&self) -> bool {
122        matches!(self.kind, TargetType::Kernel)
123    }
124
125    /// Get the [ModuleInfo] corresponding to the kernel module, if this package contains the kernel
126    pub fn kernel_module_info(&self) -> Result<ModuleInfo, Report> {
127        self.mast
128            .module_infos()
129            .find(|mi| mi.path().is_kernel_path())
130            .ok_or_else(|| Report::msg("invalid kernel package: does not contain kernel module"))
131    }
132
133    /// Get a [Kernel] from this package, if this package contains one.
134    pub fn to_kernel(&self) -> Result<Kernel, Report> {
135        let exports = self
136            .manifest
137            .exports()
138            .filter_map(|export| {
139                if export.namespace().is_kernel_path()
140                    && let PackageExport::Procedure(p) = export
141                {
142                    Some(p.digest)
143                } else {
144                    None
145                }
146            })
147            .collect::<Vec<_>>();
148        Kernel::new(&exports).map_err(|err| Report::msg(format!("invalid kernel package: {err}")))
149    }
150
151    /// Converts this package into a [KernelLibrary] if it is marked as a kernel package.
152    //
153    // TODO(pauls): This function can be removed when we remove Library/KernelLibrary/Program
154    pub fn try_into_kernel_library(&self) -> Result<KernelLibrary, Report> {
155        if !self.is_kernel() {
156            return Err(Report::msg(format!(
157                "expected package '{}' to contain a kernel, but kind was '{}'",
158                self.name, self.kind
159            )));
160        }
161
162        KernelLibrary::try_from(self.mast.clone()).map_err(|error| Report::msg(error.to_string()))
163    }
164
165    // TODO(pauls): This function can be removed when we remove Library/KernelLibrary/Program
166    #[doc(hidden)]
167    pub fn try_into_program(&self) -> Result<miden_core::program::Program, Report> {
168        use miden_assembly_syntax::{Path as MasmPath, ast};
169        use miden_core::program::Program;
170
171        if !self.is_program() {
172            return Err(Report::msg(format!(
173                "cannot convert package of type {} to Executable",
174                self.kind
175            )));
176        }
177        let main_path = MasmPath::exec_path().join(ast::ProcedureName::MAIN_PROC_NAME);
178        if let Some(digest) = self.mast.get_procedure_root_by_path(&main_path)
179            && let Some(entrypoint) = self.mast.mast_forest().find_procedure_root(digest)
180        {
181            let mast_forest = self.mast.mast_forest().clone();
182            match self.try_embedded_kernel_library()? {
183                Some(kernel_library) => Ok(Program::with_kernel(
184                    mast_forest,
185                    entrypoint,
186                    kernel_library.kernel().clone(),
187                )),
188                None => Ok(Program::new(mast_forest, entrypoint)),
189            }
190        } else {
191            Err(Report::msg(format!(
192                "malformed executable package: no procedure root for '{main_path}'"
193            )))
194        }
195    }
196
197    // TODO(pauls): This function can be removed when we remove Library/KernelLibrary/Program
198    #[doc(hidden)]
199    pub fn unwrap_program(&self) -> miden_core::program::Program {
200        assert_eq!(self.kind, TargetType::Executable);
201        self.try_into_program().unwrap_or_else(|err| panic!("{err}"))
202    }
203
204    #[doc(hidden)]
205    pub fn try_embedded_kernel_package(&self) -> Result<Option<Self>, Report> {
206        let Some(kernel_package) = self.embedded_kernel_package()? else {
207            return Ok(None);
208        };
209        self.validate_embedded_kernel_dependency(&kernel_package)?;
210        Ok(Some(kernel_package))
211    }
212
213    fn try_embedded_kernel_library(&self) -> Result<Option<KernelLibrary>, Report> {
214        let Some(kernel_package) = self.try_embedded_kernel_package()? else {
215            return Ok(None);
216        };
217        kernel_package.try_into_kernel_library().map(Some)
218    }
219
220    /// This function extracts a embedded kernel package from the KERNEL section of this package,
221    /// if present.
222    ///
223    /// This returns an error in the following situations:
224    ///
225    /// * There are duplicate KERNEL sections
226    /// * Deserialization of a package from the KERNEL section fails
227    fn embedded_kernel_package(&self) -> Result<Option<Self>, Report> {
228        let mut sections = self.sections.iter().filter(|section| section.id == SectionId::KERNEL);
229        let Some(section) = sections.next() else {
230            return Ok(None);
231        };
232        if sections.next().is_some() {
233            return Err(Report::msg(format!(
234                "package '{}' contains multiple '{}' sections",
235                self.name,
236                SectionId::KERNEL
237            )));
238        }
239
240        Self::read_from_bytes(section.data.as_ref()).map(Some).map_err(|error| {
241            Report::msg(format!(
242                "failed to decode embedded kernel package for '{}': {error}",
243                self.name
244            ))
245        })
246    }
247
248    fn validate_embedded_kernel_dependency(&self, kernel_package: &Self) -> Result<(), Report> {
249        if !kernel_package.is_kernel() {
250            return Err(Report::msg(format!(
251                "package '{}' embeds '{}', but its kind is '{}'",
252                self.name, kernel_package.name, kernel_package.kind
253            )));
254        }
255
256        let Some(kernel_dependency) = self.kernel_runtime_dependency()? else {
257            return Err(Report::msg(format!(
258                "package '{}' embeds a kernel package, but does not declare a kernel runtime dependency",
259                self.name
260            )));
261        };
262
263        if kernel_dependency.name != kernel_package.name
264            || kernel_dependency.version != kernel_package.version
265            || kernel_dependency.digest != kernel_package.digest()
266        {
267            return Err(Report::msg(format!(
268                "package '{}' declares kernel runtime dependency '{}@{}#{}', but that does not match the embedded kernel package '{}@{}#{}'",
269                self.name,
270                kernel_dependency.name,
271                kernel_dependency.version,
272                kernel_dependency.digest,
273                kernel_package.name,
274                kernel_package.version,
275                kernel_package.digest()
276            )));
277        }
278
279        Ok(())
280    }
281
282    pub fn to_dependency(&self) -> Dependency {
283        Dependency {
284            name: self.name.clone(),
285            version: self.version.clone(),
286            kind: self.kind,
287            digest: self.digest(),
288        }
289    }
290
291    /// If this package depends on a kernel, this method extracts the [Dependency] corresponding to
292    /// it.
293    ///
294    /// Returns `Err` if the dependency metadata for this package contains multiple kernels.
295    pub fn kernel_runtime_dependency(&self) -> Result<Option<&Dependency>, Report> {
296        let mut kernel_dependencies = self
297            .manifest
298            .dependencies()
299            .filter(|dependency| dependency.kind == TargetType::Kernel);
300        let Some(kernel_dependency) = kernel_dependencies.next() else {
301            return Ok(None);
302        };
303        if kernel_dependencies.next().is_some() {
304            return Err(Report::msg(format!(
305                "package '{}' declares multiple kernel runtime dependencies",
306                self.name
307            )));
308        }
309
310        Ok(Some(kernel_dependency))
311    }
312
313    /// Derive a new executable package from this one by specifying the entrypoint to use.
314    ///
315    /// To succeed, the following must be true:
316    ///
317    /// * This package was produced from a library target
318    /// * The `entrypoint` procedure is exported from this package according to the manifest
319    /// * The `entrypoint` procedure can be resolved to a node in the MAST of this package
320    ///
321    /// The resulting package has a target type and manifest reflecting what would have been used
322    /// if the package was originally assembled as an executable, however the underlying
323    /// [miden_core::mast::MastForest] is left untouched, so the resulting package may still contain
324    /// nodes in the forest which are now unused.
325    pub fn make_executable(&self, entrypoint: &QualifiedProcedureName) -> Result<Self, Report> {
326        use miden_assembly_syntax::{
327            Path as MasmPath, ast as masm,
328            library::{self, LibraryExport},
329        };
330        if !self.is_library() {
331            return Err(Report::msg("expected library but got an executable"));
332        }
333
334        let module = self
335            .mast
336            .module_infos()
337            .find(|info| info.path() == entrypoint.namespace())
338            .ok_or_else(|| {
339                Report::msg(format!(
340                    "invalid entrypoint: library does not contain a module named '{}'",
341                    entrypoint.namespace()
342                ))
343            })?;
344        if let Some(digest) = module.get_procedure_digest_by_name(entrypoint.name()) {
345            let mast_forest = self.mast.mast_forest().clone();
346            let node_id = mast_forest.find_procedure_root(digest).ok_or_else(|| {
347                Report::msg(
348                    "invalid entrypoint: malformed library - procedure exported, but digest has \
349                     no node in the forest",
350                )
351            })?;
352
353            let exec_path: Arc<MasmPath> =
354                MasmPath::exec_path().join(masm::ProcedureName::MAIN_PROC_NAME).into();
355            Ok(Self {
356                name: self.name.clone(),
357                version: self.version.clone(),
358                description: self.description.clone(),
359                kind: TargetType::Executable,
360                mast: Arc::new(Library::new(
361                    mast_forest,
362                    alloc::collections::BTreeMap::from_iter([(
363                        exec_path.clone(),
364                        LibraryExport::Procedure(library::ProcedureExport {
365                            node: node_id,
366                            path: exec_path,
367                            signature: None,
368                            attributes: Default::default(),
369                        }),
370                    )]),
371                )?),
372                manifest: PackageManifest::new(
373                    self.manifest
374                        .get_procedures_by_digest(&digest)
375                        .cloned()
376                        .map(PackageExport::Procedure),
377                )
378                .and_then(|manifest| {
379                    manifest.with_dependencies(self.manifest.dependencies().cloned())
380                })
381                .expect("executable package manifest should remain valid"),
382                sections: self.sections.clone(),
383            })
384        } else {
385            Err(Report::msg(format!(
386                "invalid entrypoint: library does not export '{entrypoint}'"
387            )))
388        }
389    }
390
391    /// Returns the procedure name for the given MAST root digest, if present.
392    ///
393    /// This allows debuggers to resolve human-readable procedure names during execution.
394    pub fn procedure_name(&self, digest: &Word) -> Option<&str> {
395        self.mast.mast_forest().procedure_name(digest)
396    }
397
398    /// Returns an iterator over all (digest, name) pairs of procedure names.
399    pub fn procedure_names(&self) -> impl Iterator<Item = (Word, &Arc<str>)> {
400        self.mast.mast_forest().procedure_names()
401    }
402
403    /// Write this package to `path`
404    #[cfg(feature = "std")]
405    pub fn write_to_file(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
406        use miden_core::serde::Serializable;
407
408        let path = path.as_ref();
409        if let Some(dir) = path.parent() {
410            std::fs::create_dir_all(dir)?;
411        }
412
413        let mut file = std::fs::File::create(path)?;
414        <Self as Serializable>::write_into(self, &mut file);
415        Ok(())
416    }
417
418    /// Write this package to a file in `dir` named `$name.masp`, where `$name` is the package name.
419    #[cfg(feature = "std")]
420    pub fn write_masp_file(&self, dir: impl AsRef<std::path::Path>) -> std::io::Result<()> {
421        let dir = dir.as_ref();
422        let package_name: &str = &self.name;
423        self.write_to_file(dir.join(package_name).with_extension(Self::EXTENSION))
424            .map_err(|err| std::io::Error::other(err.to_string()))
425    }
426}
427
428#[cfg(feature = "arbitrary")]
429impl Package {
430    pub fn generate(
431        name: PackageId,
432        version: Version,
433        kind: TargetType,
434        dependencies: impl IntoIterator<Item = Dependency>,
435    ) -> Box<Self> {
436        let library = arbitrary_library();
437
438        Self::from_library(name, version, kind, library, dependencies)
439    }
440}
441
442#[cfg(feature = "arbitrary")]
443fn arbitrary_library() -> Arc<Library> {
444    use proptest::prelude::*;
445
446    let mut runner = proptest::test_runner::TestRunner::deterministic();
447    let value_tree = <Library as Arbitrary>::arbitrary().new_tree(&mut runner).unwrap();
448    Arc::new(value_tree.current())
449}
450
451// TESTS
452// ================================================================================================
453
454#[cfg(test)]
455mod tests {
456    use alloc::{collections::BTreeMap, sync::Arc, vec, vec::Vec};
457
458    use miden_assembly_syntax::{
459        Library,
460        ast::{Path as AstPath, PathBuf},
461        library::{LibraryExport, ProcedureExport as LibraryProcedureExport},
462    };
463    use miden_core::{
464        mast::{BasicBlockNodeBuilder, MastForest, MastForestContributor, MastNodeId},
465        operations::Operation,
466        serde::Serializable,
467    };
468
469    use super::*;
470    use crate::{Dependency, Version};
471
472    fn build_forest() -> (MastForest, MastNodeId) {
473        let mut forest = MastForest::new();
474        let node_id = BasicBlockNodeBuilder::new(vec![Operation::Add], Vec::new())
475            .add_to_forest(&mut forest)
476            .expect("failed to build basic block");
477        forest.make_root(node_id);
478        (forest, node_id)
479    }
480
481    fn absolute_path(name: &str) -> Arc<AstPath> {
482        let path = PathBuf::new(name).expect("invalid path");
483        let path = path.as_path().to_absolute().into_owned();
484        Arc::from(path.into_boxed_path())
485    }
486
487    fn build_library(export: &str) -> Arc<Library> {
488        let (forest, node_id) = build_forest();
489        let path = absolute_path(export);
490        let export = LibraryProcedureExport::new(node_id, Arc::clone(&path));
491
492        let mut exports = BTreeMap::new();
493        exports.insert(path, LibraryExport::Procedure(export));
494
495        Arc::new(Library::new(Arc::new(forest), exports).expect("failed to build library"))
496    }
497
498    fn build_package(
499        name: &str,
500        kind: TargetType,
501        export: &str,
502        dependencies: impl IntoIterator<Item = Dependency>,
503        sections: Vec<Section>,
504    ) -> Package {
505        let mut package = *Package::from_library(
506            PackageId::from(name),
507            Version::new(1, 0, 0),
508            kind,
509            build_library(export),
510            dependencies,
511        );
512        package.sections = sections;
513        package
514    }
515
516    fn build_kernel_package(name: &str) -> Package {
517        build_package(name, TargetType::Kernel, &format!("{name}::boot"), [], Vec::new())
518    }
519
520    fn kernel_dependency(package: &Package) -> Dependency {
521        Dependency {
522            name: package.name.clone(),
523            kind: TargetType::Kernel,
524            version: package.version.clone(),
525            digest: package.digest(),
526        }
527    }
528
529    #[test]
530    fn embedded_kernel_package_rejects_duplicate_kernel_sections() {
531        let kernel = build_kernel_package("kernel");
532        let kernel_bytes = kernel.to_bytes();
533        let package = build_package(
534            "app",
535            TargetType::Library,
536            "app::entry",
537            vec![kernel_dependency(&kernel)],
538            vec![
539                Section::new(SectionId::KERNEL, kernel_bytes.clone()),
540                Section::new(SectionId::KERNEL, kernel_bytes),
541            ],
542        );
543
544        let error = package
545            .try_embedded_kernel_package()
546            .expect_err("duplicate kernel sections should be rejected");
547
548        assert!(error.to_string().contains("multiple 'kernel' sections"));
549    }
550
551    #[test]
552    fn embedded_kernel_package_rejects_multiple_kernel_runtime_dependencies() {
553        let kernel_a = build_kernel_package("kernel-a");
554        let kernel_b = build_kernel_package("kernel-b");
555        let package = build_package(
556            "app",
557            TargetType::Library,
558            "app::entry",
559            vec![kernel_dependency(&kernel_a), kernel_dependency(&kernel_b)],
560            vec![Section::new(SectionId::KERNEL, kernel_a.to_bytes())],
561        );
562
563        let error = package
564            .try_embedded_kernel_package()
565            .expect_err("multiple kernel runtime dependencies should be rejected");
566
567        assert!(error.to_string().contains("declares multiple kernel runtime dependencies"));
568    }
569}