1use alloc::{
2 collections::BTreeMap,
3 string::{String, ToString},
4 sync::Arc,
5 vec::Vec,
6};
7use core::fmt;
8
9use miden_assembly_syntax::ast::{
10 self, AttributeSet, Path,
11 types::{FunctionType, Type},
12};
13#[cfg(all(feature = "arbitrary", test))]
14use miden_core::serde::{Deserializable, Serializable};
15use miden_core::{Word, mast::MastNodeId, utils::DisplayHex};
16#[cfg(any(test, feature = "arbitrary"))]
17use proptest::prelude::{Strategy, any};
18use thiserror::Error;
19
20use crate::{Dependency, PackageId, debug_info::DebugSourceNodeId};
21
22#[derive(Debug, Clone, PartialEq, Eq)]
37#[cfg_attr(any(test, feature = "arbitrary"), derive(proptest_derive::Arbitrary))]
38#[cfg_attr(
39 all(feature = "arbitrary", test),
40 miden_test_serialization_macros::serialization_test
41)]
42pub struct PackageManifest {
43 #[cfg_attr(
45 any(test, feature = "arbitrary"),
46 proptest(
47 strategy = "proptest::collection::vec(any::<PackageExport>(), 1..10).prop_filter_map(\"package exports must have unique paths\", |exports| PackageManifest::new(exports).ok().map(|manifest| manifest.exports))"
48 )
49 )]
50 pub(super) exports: BTreeMap<Arc<Path>, PackageExport>,
51 #[cfg_attr(any(test, feature = "arbitrary"), proptest(value = "Default::default()"))]
53 pub(super) modules: BTreeMap<Arc<Path>, PackageModule>,
54 #[cfg_attr(
57 any(test, feature = "arbitrary"),
58 proptest(strategy = "arbitrary_dependencies()")
59 )]
60 pub(super) dependencies: Vec<Dependency>,
61 #[cfg_attr(any(test, feature = "arbitrary"), proptest(value = "None"))]
63 pub(super) entrypoint: Option<Arc<Path>>,
64}
65
66#[derive(Debug, Error)]
67pub enum ManifestValidationError {
68 #[error("duplicate export path '{0}' in package manifest")]
69 DuplicateExport(Arc<Path>),
70 #[error("duplicate module path '{0}' in package manifest")]
71 DuplicateModule(Arc<Path>),
72 #[error("duplicate submodule '{name}' in module '{module}' in package manifest")]
73 DuplicateSubmodule { module: Arc<Path>, name: String },
74 #[error(
75 "package manifest declares export '{export}' in module '{module}', but no module surface was provided for that module"
76 )]
77 MissingExportModuleSurface { export: Arc<Path>, module: Arc<Path> },
78 #[error(
79 "package manifest declares submodule '{module}' from module '{parent}', but no module surface was provided for it"
80 )]
81 MissingDeclaredSubmoduleSurface {
82 parent: Arc<Path>,
83 name: String,
84 module: Arc<Path>,
85 },
86 #[error(
87 "package manifest contains module surface '{module}', but parent module '{parent}' does not declare submodule '{name}'"
88 )]
89 UndeclaredModuleSurface {
90 module: Arc<Path>,
91 parent: Arc<Path>,
92 name: String,
93 },
94 #[error("duplicate dependency '{0}' in package manifest")]
95 DuplicateDependency(PackageId),
96 #[error("multiple entrypoint procedures found: '{duplicate}' conflicts with '{original}'")]
97 DuplicateEntrypoint {
98 original: Arc<Path>,
99 duplicate: Arc<Path>,
100 },
101 #[error("invalid {expected} path '{path}': found export of type {actual}")]
102 UnexpectedExportType {
103 path: Arc<Path>,
104 expected: &'static str,
105 actual: &'static str,
106 },
107 #[error("found an executable entrypoint in a package declared with non-executable type")]
108 NonExecutableEntrypoint,
109 #[error("invalid entrypoint path '{path}': no export with that path was found in the manifest")]
110 MissingEntrypoint { path: Arc<Path> },
111 #[error(
112 "package manifest declares export for procedure '{path}', but no procedure root with its digest was found in the MAST"
113 )]
114 MissingProcedureMast { path: Arc<Path>, digest: Word },
115 #[error(
116 "invalid procedure export '{path}': the declared node id and digest do not correspond to a procedure root in the MAST"
117 )]
118 InvalidProcedureExport { path: Arc<Path> },
119 #[error("invalid export path '{path}': {error}")]
120 InvalidExportPath { path: Arc<Path>, error: ast::PathError },
121 #[error("invalid module path '{path}': {error}")]
122 InvalidModulePath { path: Arc<Path>, error: ast::PathError },
123 #[error("package must contain at least one exported procedure")]
124 NoProcedures,
125}
126
127impl PackageManifest {
128 pub fn new(
131 exports: impl IntoIterator<Item = PackageExport>,
132 ) -> Result<Self, ManifestValidationError> {
133 let mut manifest = Self {
134 exports: Default::default(),
135 modules: Default::default(),
136 dependencies: Default::default(),
137 entrypoint: None,
138 };
139 let mut has_procedures = false;
140 for mut export in exports {
141 normalize_export(&mut export)?;
142 if let Some(proc) = export.as_procedure() {
143 has_procedures = true;
144 if proc.path.last().is_some_and(|name| name == ast::ProcedureName::MAIN_PROC_NAME) {
147 if let Some(original) = manifest.entrypoint.clone() {
148 return Err(ManifestValidationError::DuplicateEntrypoint {
149 original,
150 duplicate: proc.path.clone(),
151 });
152 }
153 manifest.entrypoint = Some(proc.path.clone());
154 }
155 }
156 manifest.add_export(export)?;
157 }
158
159 if !has_procedures {
160 return Err(ManifestValidationError::NoProcedures);
161 }
162
163 Ok(manifest)
164 }
165
166 pub fn with_entrypoint(
171 mut self,
172 entrypoint: Arc<Path>,
173 ) -> Result<Self, ManifestValidationError> {
174 self.set_entrypoint(entrypoint)?;
175
176 Ok(self)
177 }
178
179 pub(super) fn set_entrypoint(
184 &mut self,
185 entrypoint: Arc<Path>,
186 ) -> Result<(), ManifestValidationError> {
187 if let Some(original) = self.entrypoint.clone() {
188 if original == entrypoint {
189 Ok(())
190 } else {
191 Err(ManifestValidationError::DuplicateEntrypoint {
192 original,
193 duplicate: entrypoint,
194 })
195 }
196 } else if let Some(export) = self.get_export(&entrypoint) {
197 match export {
198 PackageExport::Procedure(proc) => {
199 self.entrypoint = Some(proc.path.clone());
200 Ok(())
201 },
202 other @ (PackageExport::Constant(_) | PackageExport::Type(_)) => {
203 let actual = match other {
204 PackageExport::Constant(_) => "constant",
205 PackageExport::Type(_) => "type",
206 _ => unreachable!(),
207 };
208 Err(ManifestValidationError::UnexpectedExportType {
209 path: entrypoint,
210 expected: "procedure",
211 actual,
212 })
213 },
214 }
215 } else {
216 Err(ManifestValidationError::MissingEntrypoint { path: entrypoint })
217 }
218 }
219
220 pub fn with_dependencies(
222 mut self,
223 dependencies: impl IntoIterator<Item = Dependency>,
224 ) -> Result<Self, ManifestValidationError> {
225 for dependency in dependencies {
226 self.add_dependency(dependency)?;
227 }
228
229 Ok(self)
230 }
231
232 pub fn with_modules(
234 mut self,
235 modules: impl IntoIterator<Item = PackageModule>,
236 ) -> Result<Self, ManifestValidationError> {
237 for module in modules {
238 self.add_module(module)?;
239 }
240
241 Ok(self)
242 }
243
244 pub fn add_module(&mut self, mut module: PackageModule) -> Result<(), ManifestValidationError> {
246 normalize_module(&mut module)?;
247 let path = module.path.clone();
248 if self.modules.insert(path.clone(), module).is_some() {
249 return Err(ManifestValidationError::DuplicateModule(path));
250 }
251
252 Ok(())
253 }
254
255 pub fn add_dependency(
257 &mut self,
258 dependency: Dependency,
259 ) -> Result<(), ManifestValidationError> {
260 if self.dependencies.iter().any(|existing| existing.id() == dependency.id()) {
261 return Err(ManifestValidationError::DuplicateDependency(dependency.name));
262 }
263
264 self.dependencies.push(dependency);
265 Ok(())
266 }
267
268 pub fn num_dependencies(&self) -> usize {
270 self.dependencies.len()
271 }
272
273 pub fn dependencies(&self) -> impl Iterator<Item = &Dependency> {
275 self.dependencies.iter()
276 }
277
278 pub fn num_exports(&self) -> usize {
280 self.exports.len()
281 }
282
283 pub fn exports(&self) -> impl Iterator<Item = &PackageExport> {
285 self.exports.values()
286 }
287
288 pub fn get_export(&self, name: impl AsRef<Path>) -> Option<&PackageExport> {
290 self.exports.get(name.as_ref())
291 }
292
293 pub fn num_modules(&self) -> usize {
295 self.modules.len()
296 }
297
298 pub fn modules(&self) -> impl Iterator<Item = &PackageModule> {
300 self.modules.values()
301 }
302
303 pub fn get_module(&self, name: impl AsRef<Path>) -> Option<&PackageModule> {
305 self.modules.get(name.as_ref())
306 }
307
308 pub fn get_procedures_by_digest(
311 &self,
312 digest: &Word,
313 ) -> impl Iterator<Item = &ProcedureExport> + '_ {
314 let digest = *digest;
315 self.exports.values().filter_map(move |export| match export {
316 PackageExport::Procedure(export) if export.digest == digest => Some(export),
317 PackageExport::Procedure(_) => None,
318 PackageExport::Constant(_) | PackageExport::Type(_) => None,
319 })
320 }
321
322 pub fn entrypoint(&self) -> Option<Arc<Path>> {
324 self.entrypoint.clone()
325 }
326
327 fn add_export(&mut self, export: PackageExport) -> Result<(), ManifestValidationError> {
328 let path = export.path();
329 if self.exports.insert(path.clone(), export).is_some() {
330 return Err(ManifestValidationError::DuplicateExport(path));
331 }
332
333 Ok(())
334 }
335}
336
337#[derive(Debug, Clone, PartialEq, Eq)]
339pub struct PackageModule {
340 pub path: Arc<Path>,
342 pub submodules: Vec<PackageSubmodule>,
344}
345
346impl PackageModule {
347 pub fn new(path: Arc<Path>, submodules: impl IntoIterator<Item = PackageSubmodule>) -> Self {
348 Self {
349 path,
350 submodules: submodules.into_iter().collect(),
351 }
352 }
353
354 #[inline]
356 pub fn path(&self) -> &Arc<Path> {
357 &self.path
358 }
359
360 #[inline]
362 pub fn submodules(&self) -> &[PackageSubmodule] {
363 &self.submodules
364 }
365}
366
367#[derive(Debug, Clone, PartialEq, Eq)]
369pub struct PackageSubmodule {
370 pub name: ast::Ident,
372}
373
374impl PackageSubmodule {
375 pub fn new(name: ast::Ident) -> Self {
376 Self { name }
377 }
378}
379
380#[derive(Debug, Clone, PartialEq, Eq)]
382#[repr(u8)]
383#[cfg_attr(
384 all(feature = "arbitrary", test),
385 miden_test_serialization_macros::serialization_test
386)]
387pub enum PackageExport {
388 Procedure(ProcedureExport) = 1,
390 Constant(ConstantExport),
392 Type(TypeExport),
394}
395
396impl PackageExport {
397 pub fn path(&self) -> Arc<Path> {
399 match self {
400 Self::Procedure(export) => export.path.clone(),
401 Self::Constant(export) => export.path.clone(),
402 Self::Type(export) => export.path.clone(),
403 }
404 }
405
406 pub fn namespace(&self) -> &Path {
410 match self {
411 Self::Procedure(ProcedureExport { path, .. })
412 | Self::Constant(ConstantExport { path, .. })
413 | Self::Type(TypeExport { path, .. }) => path.parent().unwrap(),
414 }
415 }
416
417 pub fn name(&self) -> &str {
421 match self {
422 Self::Procedure(ProcedureExport { path, .. })
423 | Self::Constant(ConstantExport { path, .. })
424 | Self::Type(TypeExport { path, .. }) => path.last().unwrap(),
425 }
426 }
427
428 #[inline]
430 pub fn is_procedure(&self) -> bool {
431 matches!(self, Self::Procedure(_))
432 }
433
434 #[inline]
436 pub fn is_constant(&self) -> bool {
437 matches!(self, Self::Constant(_))
438 }
439
440 #[inline]
442 pub fn is_type(&self) -> bool {
443 matches!(self, Self::Type(_))
444 }
445
446 #[inline]
448 pub fn as_procedure(&self) -> Option<&ProcedureExport> {
449 match self {
450 Self::Procedure(export) => Some(export),
451 _ => None,
452 }
453 }
454
455 #[inline]
457 pub fn as_constant(&self) -> Option<&ConstantExport> {
458 match self {
459 Self::Constant(export) => Some(export),
460 _ => None,
461 }
462 }
463
464 #[inline]
466 pub fn as_type(&self) -> Option<&TypeExport> {
467 match self {
468 Self::Type(export) => Some(export),
469 _ => None,
470 }
471 }
472
473 pub(crate) const fn tag(&self) -> u8 {
474 unsafe { *(self as *const Self).cast::<u8>() }
481 }
482}
483
484#[cfg(any(test, feature = "arbitrary"))]
485impl proptest::arbitrary::Arbitrary for PackageExport {
486 type Parameters = ();
487
488 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
489 use proptest::{arbitrary::any, prop_oneof, strategy::Strategy};
490
491 prop_oneof![
492 any::<ProcedureExport>().prop_map(Self::Procedure),
493 any::<ConstantExport>().prop_map(Self::Constant),
494 any::<TypeExport>().prop_map(Self::Type),
495 ]
496 .boxed()
497 }
498
499 type Strategy = proptest::prelude::BoxedStrategy<Self>;
500}
501
502#[derive(Clone, PartialEq, Eq)]
504#[cfg_attr(any(test, feature = "arbitrary"), derive(proptest_derive::Arbitrary))]
505#[cfg_attr(
506 all(feature = "arbitrary", test),
507 miden_test_serialization_macros::serialization_test
508)]
509pub struct ProcedureExport {
510 #[cfg_attr(
512 any(test, feature = "arbitrary"),
513 proptest(strategy = "miden_assembly_syntax::arbitrary::path::bare_path_random_length(2)")
514 )]
515 pub path: Arc<Path>,
516 #[cfg_attr(any(test, feature = "arbitrary"), proptest(value = "None"))]
535 pub node: Option<MastNodeId>,
536 #[cfg_attr(any(test, feature = "arbitrary"), proptest(value = "None"))]
542 pub source_node: Option<DebugSourceNodeId>,
543 #[cfg_attr(any(test, feature = "arbitrary"), proptest(value = "Word::default()"))]
545 pub digest: Word,
546 #[cfg_attr(any(test, feature = "arbitrary"), proptest(value = "None"))]
548 pub signature: Option<FunctionType>,
549 #[cfg_attr(any(test, feature = "arbitrary"), proptest(value = "AttributeSet::default()"))]
551 pub attributes: AttributeSet,
552}
553
554impl ProcedureExport {
555 pub fn new(
556 path: Arc<Path>,
557 node: Option<MastNodeId>,
558 digest: Word,
559 signature: Option<FunctionType>,
560 ) -> Self {
561 Self {
562 path,
563 node,
564 source_node: None,
565 digest,
566 signature,
567 attributes: Default::default(),
568 }
569 }
570
571 pub fn with_source_node(mut self, source_node: Option<DebugSourceNodeId>) -> Self {
572 self.source_node = source_node;
573 self
574 }
575}
576
577impl fmt::Debug for ProcedureExport {
578 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
579 let Self {
580 path,
581 node,
582 source_node,
583 digest,
584 signature,
585 attributes,
586 } = self;
587 f.debug_struct("PackageExport")
588 .field("path", &format_args!("{path}"))
589 .field("node", node)
590 .field("source_node", source_node)
591 .field("digest", &format_args!("{}", DisplayHex::new(&digest.as_bytes())))
592 .field("signature", signature)
593 .field("attributes", attributes)
594 .finish()
595 }
596}
597
598#[derive(Clone, PartialEq, Eq)]
600#[cfg_attr(any(test, feature = "arbitrary"), derive(proptest_derive::Arbitrary))]
601#[cfg_attr(
602 all(feature = "arbitrary", test),
603 miden_test_serialization_macros::serialization_test
604)]
605pub struct ConstantExport {
606 #[cfg_attr(
608 any(test, feature = "arbitrary"),
609 proptest(
610 strategy = "miden_assembly_syntax::arbitrary::path::constant_path_random_length(1)"
611 )
612 )]
613 pub path: Arc<Path>,
614 pub value: ast::ConstantValue,
621}
622
623impl fmt::Debug for ConstantExport {
624 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
625 let Self { path, value } = self;
626 f.debug_struct("ConstantExport")
627 .field("path", &format_args!("{path}"))
628 .field("value", value)
629 .finish()
630 }
631}
632
633#[derive(Clone, PartialEq, Eq)]
635#[cfg_attr(any(test, feature = "arbitrary"), derive(proptest_derive::Arbitrary))]
636#[cfg_attr(
637 all(feature = "arbitrary", test),
638 miden_test_serialization_macros::serialization_test
639)]
640pub struct TypeExport {
641 #[cfg_attr(
643 any(test, feature = "arbitrary"),
644 proptest(
645 strategy = "miden_assembly_syntax::arbitrary::path::user_defined_type_path_random_length(1)"
646 )
647 )]
648 pub path: Arc<Path>,
649 #[cfg_attr(any(test, feature = "arbitrary"), proptest(value = "Type::Felt"))]
651 pub ty: Type,
652}
653
654impl fmt::Debug for TypeExport {
655 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
656 let Self { path, ty } = self;
657 f.debug_struct("TypeExport")
658 .field("path", &format_args!("{path}"))
659 .field("ty", ty)
660 .finish()
661 }
662}
663
664#[cfg(any(test, feature = "arbitrary"))]
665fn arbitrary_dependencies() -> impl Strategy<Value = Vec<Dependency>> {
666 proptest::collection::vec(any::<Dependency>(), 0..10).prop_filter(
667 "package dependencies must have unique ids",
668 |dependencies| {
669 use alloc::collections::BTreeSet;
670
671 let mut seen = BTreeSet::new();
672 dependencies.iter().all(|dependency| seen.insert(dependency.id().clone()))
673 },
674 )
675}
676
677fn normalize_export(export: &mut PackageExport) -> Result<(), ManifestValidationError> {
678 let canonical_path = canonicalize_export_path(export.path().as_ref())?;
679
680 match export {
681 PackageExport::Procedure(proc) => {
682 let _ = canonical_path
683 .procedure_name()
684 .map_err(|error| ManifestValidationError::InvalidExportPath {
685 path: canonical_path.clone(),
686 error,
687 })?
688 .ok_or_else(|| ManifestValidationError::InvalidExportPath {
689 path: canonical_path.clone(),
690 error: ast::PathError::Empty,
691 })?;
692 proc.path = canonical_path;
693 },
694 PackageExport::Constant(ConstantExport { path, .. })
695 | PackageExport::Type(TypeExport { path, .. }) => {
696 let leaf = canonical_path
697 .components()
698 .next_back()
699 .ok_or_else(|| ManifestValidationError::InvalidExportPath {
700 path: canonical_path.clone(),
701 error: ast::PathError::Empty,
702 })?
703 .map_err(|error| ManifestValidationError::InvalidExportPath {
704 path: canonical_path.clone(),
705 error,
706 })?;
707 let _ = ast::Ident::new(leaf.as_str()).map_err(|err| {
708 ManifestValidationError::InvalidExportPath {
709 path: canonical_path.clone(),
710 error: ast::PathError::InvalidComponent(err),
711 }
712 })?;
713 *path = canonical_path;
714 },
715 }
716
717 Ok(())
718}
719
720fn normalize_module(module: &mut PackageModule) -> Result<(), ManifestValidationError> {
721 use alloc::collections::BTreeSet;
722 let canonical_path = canonicalize_module_path(module.path.as_ref())?;
723 let mut declared = BTreeSet::new();
724
725 for submodule in module.submodules.iter() {
726 let name = submodule.name.as_str();
727 if !declared.insert(name.to_string()) {
728 return Err(ManifestValidationError::DuplicateSubmodule {
729 module: canonical_path,
730 name: name.to_string(),
731 });
732 }
733 }
734
735 module.path = canonical_path;
736 Ok(())
737}
738
739fn canonicalize_module_path(path: &Path) -> Result<Arc<Path>, ManifestValidationError> {
740 let canonical =
741 path.canonicalize()
742 .map_err(|error| ManifestValidationError::InvalidModulePath {
743 error,
744 path: path.to_path_buf().into(),
745 })?;
746 Ok(Arc::<Path>::from(canonical.into_boxed_path()))
747}
748
749fn canonicalize_export_path(path: &Path) -> Result<Arc<Path>, ManifestValidationError> {
750 let canonical =
751 path.canonicalize()
752 .map_err(|error| ManifestValidationError::InvalidExportPath {
753 error,
754 path: path.to_path_buf().into(),
755 })?;
756 Ok(Arc::<Path>::from(canonical.into_boxed_path()))
757}