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