use alloc::{
collections::BTreeMap,
string::{String, ToString},
sync::Arc,
vec::Vec,
};
use core::fmt;
use miden_assembly_syntax::ast::{
self, AttributeSet, Path,
types::{FunctionType, Type},
};
#[cfg(all(feature = "arbitrary", test))]
use miden_core::serde::{Deserializable, Serializable};
use miden_core::{Word, mast::MastNodeId, utils::DisplayHex};
#[cfg(any(test, feature = "arbitrary"))]
use proptest::prelude::{Strategy, any};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{Dependency, PackageId, debug_info::DebugSourceNodeId};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(any(test, feature = "arbitrary"), derive(proptest_derive::Arbitrary))]
#[cfg_attr(
all(feature = "arbitrary", test),
miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false))
)]
pub struct PackageManifest {
#[cfg_attr(
any(test, feature = "arbitrary"),
proptest(
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))"
)
)]
pub(super) exports: BTreeMap<Arc<Path>, PackageExport>,
#[cfg_attr(any(test, feature = "arbitrary"), proptest(value = "Default::default()"))]
pub(super) modules: BTreeMap<Arc<Path>, PackageModule>,
pub(super) dependencies: Vec<Dependency>,
#[cfg_attr(any(test, feature = "arbitrary"), proptest(value = "None"))]
pub(super) entrypoint: Option<Arc<Path>>,
}
#[derive(Debug, Error)]
pub enum ManifestValidationError {
#[error("duplicate export path '{0}' in package manifest")]
DuplicateExport(Arc<Path>),
#[error("duplicate module path '{0}' in package manifest")]
DuplicateModule(Arc<Path>),
#[error("duplicate submodule '{name}' in module '{module}' in package manifest")]
DuplicateSubmodule { module: Arc<Path>, name: String },
#[error(
"package manifest declares export '{export}' in module '{module}', but no module surface was provided for that module"
)]
MissingExportModuleSurface { export: Arc<Path>, module: Arc<Path> },
#[error(
"package manifest declares submodule '{module}' from module '{parent}', but no module surface was provided for it"
)]
MissingDeclaredSubmoduleSurface {
parent: Arc<Path>,
name: String,
module: Arc<Path>,
},
#[error(
"package manifest contains module surface '{module}', but parent module '{parent}' does not declare submodule '{name}'"
)]
UndeclaredModuleSurface {
module: Arc<Path>,
parent: Arc<Path>,
name: String,
},
#[error("duplicate dependency '{0}' in package manifest")]
DuplicateDependency(PackageId),
#[error("multiple entrypoint procedures found: '{duplicate}' conflicts with '{original}'")]
DuplicateEntrypoint {
original: Arc<Path>,
duplicate: Arc<Path>,
},
#[error("invalid {expected} path '{path}': found export of type {actual}")]
UnexpectedExportType {
path: Arc<Path>,
expected: &'static str,
actual: &'static str,
},
#[error("found an executable entrypoint in a package declared with non-executable type")]
NonExecutableEntrypoint,
#[error("invalid entrypoint path '{path}': no export with that path was found in the manifest")]
MissingEntrypoint { path: Arc<Path> },
#[error(
"package manifest declares export for procedure '{path}', but no procedure root with its digest was found in the MAST"
)]
MissingProcedureMast { path: Arc<Path>, digest: Word },
#[error(
"invalid procedure export '{path}': the declared node id and digest do not correspond to a procedure root in the MAST"
)]
InvalidProcedureExport { path: Arc<Path> },
#[error("invalid export path '{path}': {error}")]
InvalidExportPath { path: Arc<Path>, error: ast::PathError },
#[error("invalid module path '{path}': {error}")]
InvalidModulePath { path: Arc<Path>, error: ast::PathError },
#[error("package must contain at least one exported procedure")]
NoProcedures,
}
impl PackageManifest {
pub fn new(
exports: impl IntoIterator<Item = PackageExport>,
) -> Result<Self, ManifestValidationError> {
let mut manifest = Self {
exports: Default::default(),
modules: Default::default(),
dependencies: Default::default(),
entrypoint: None,
};
let mut has_procedures = false;
for mut export in exports {
normalize_export(&mut export)?;
if let Some(proc) = export.as_procedure() {
has_procedures = true;
if proc.path.last().is_some_and(|name| name == ast::ProcedureName::MAIN_PROC_NAME) {
if let Some(original) = manifest.entrypoint.clone() {
return Err(ManifestValidationError::DuplicateEntrypoint {
original,
duplicate: proc.path.clone(),
});
}
manifest.entrypoint = Some(proc.path.clone());
}
}
manifest.add_export(export)?;
}
if !has_procedures {
return Err(ManifestValidationError::NoProcedures);
}
Ok(manifest)
}
pub fn with_entrypoint(
mut self,
entrypoint: Arc<Path>,
) -> Result<Self, ManifestValidationError> {
self.set_entrypoint(entrypoint)?;
Ok(self)
}
pub(super) fn set_entrypoint(
&mut self,
entrypoint: Arc<Path>,
) -> Result<(), ManifestValidationError> {
if let Some(original) = self.entrypoint.clone() {
if original == entrypoint {
Ok(())
} else {
Err(ManifestValidationError::DuplicateEntrypoint {
original,
duplicate: entrypoint,
})
}
} else if let Some(export) = self.get_export(&entrypoint) {
match export {
PackageExport::Procedure(proc) => {
self.entrypoint = Some(proc.path.clone());
Ok(())
},
other @ (PackageExport::Constant(_) | PackageExport::Type(_)) => {
let actual = match other {
PackageExport::Constant(_) => "constant",
PackageExport::Type(_) => "type",
_ => unreachable!(),
};
Err(ManifestValidationError::UnexpectedExportType {
path: entrypoint,
expected: "procedure",
actual,
})
},
}
} else {
Err(ManifestValidationError::MissingEntrypoint { path: entrypoint })
}
}
pub fn with_dependencies(
mut self,
dependencies: impl IntoIterator<Item = Dependency>,
) -> Result<Self, ManifestValidationError> {
for dependency in dependencies {
self.add_dependency(dependency)?;
}
Ok(self)
}
pub fn with_modules(
mut self,
modules: impl IntoIterator<Item = PackageModule>,
) -> Result<Self, ManifestValidationError> {
for module in modules {
self.add_module(module)?;
}
Ok(self)
}
pub fn add_module(&mut self, mut module: PackageModule) -> Result<(), ManifestValidationError> {
normalize_module(&mut module)?;
let path = module.path.clone();
if self.modules.insert(path.clone(), module).is_some() {
return Err(ManifestValidationError::DuplicateModule(path));
}
Ok(())
}
pub fn add_dependency(
&mut self,
dependency: Dependency,
) -> Result<(), ManifestValidationError> {
if self.dependencies.iter().any(|existing| existing.id() == dependency.id()) {
return Err(ManifestValidationError::DuplicateDependency(dependency.name));
}
self.dependencies.push(dependency);
Ok(())
}
pub fn num_dependencies(&self) -> usize {
self.dependencies.len()
}
pub fn dependencies(&self) -> impl Iterator<Item = &Dependency> {
self.dependencies.iter()
}
pub fn num_exports(&self) -> usize {
self.exports.len()
}
pub fn exports(&self) -> impl Iterator<Item = &PackageExport> {
self.exports.values()
}
pub fn get_export(&self, name: impl AsRef<Path>) -> Option<&PackageExport> {
self.exports.get(name.as_ref())
}
pub fn num_modules(&self) -> usize {
self.modules.len()
}
pub fn modules(&self) -> impl Iterator<Item = &PackageModule> {
self.modules.values()
}
pub fn get_module(&self, name: impl AsRef<Path>) -> Option<&PackageModule> {
self.modules.get(name.as_ref())
}
pub fn get_procedures_by_digest(
&self,
digest: &Word,
) -> impl Iterator<Item = &ProcedureExport> + '_ {
let digest = *digest;
self.exports.values().filter_map(move |export| match export {
PackageExport::Procedure(export) if export.digest == digest => Some(export),
PackageExport::Procedure(_) => None,
PackageExport::Constant(_) | PackageExport::Type(_) => None,
})
}
pub fn entrypoint(&self) -> Option<Arc<Path>> {
self.entrypoint.clone()
}
fn add_export(&mut self, export: PackageExport) -> Result<(), ManifestValidationError> {
let path = export.path();
if self.exports.insert(path.clone(), export).is_some() {
return Err(ManifestValidationError::DuplicateExport(path));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PackageModule {
#[cfg_attr(feature = "serde", serde(with = "miden_assembly_syntax::ast::path"))]
pub path: Arc<Path>,
pub submodules: Vec<PackageSubmodule>,
}
impl PackageModule {
pub fn new(path: Arc<Path>, submodules: impl IntoIterator<Item = PackageSubmodule>) -> Self {
Self {
path,
submodules: submodules.into_iter().collect(),
}
}
#[inline]
pub fn path(&self) -> &Arc<Path> {
&self.path
}
#[inline]
pub fn submodules(&self) -> &[PackageSubmodule] {
&self.submodules
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PackageSubmodule {
pub name: ast::Ident,
}
impl PackageSubmodule {
pub fn new(name: ast::Ident) -> Self {
Self { name }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(u8)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(
all(feature = "arbitrary", test),
miden_test_serde_macros::serde_test(binary_serde(true))
)]
pub enum PackageExport {
Procedure(ProcedureExport) = 1,
Constant(ConstantExport),
Type(TypeExport),
}
impl PackageExport {
pub fn path(&self) -> Arc<Path> {
match self {
Self::Procedure(export) => export.path.clone(),
Self::Constant(export) => export.path.clone(),
Self::Type(export) => export.path.clone(),
}
}
pub fn namespace(&self) -> &Path {
match self {
Self::Procedure(ProcedureExport { path, .. })
| Self::Constant(ConstantExport { path, .. })
| Self::Type(TypeExport { path, .. }) => path.parent().unwrap(),
}
}
pub fn name(&self) -> &str {
match self {
Self::Procedure(ProcedureExport { path, .. })
| Self::Constant(ConstantExport { path, .. })
| Self::Type(TypeExport { path, .. }) => path.last().unwrap(),
}
}
#[inline]
pub fn is_procedure(&self) -> bool {
matches!(self, Self::Procedure(_))
}
#[inline]
pub fn is_constant(&self) -> bool {
matches!(self, Self::Constant(_))
}
#[inline]
pub fn is_type(&self) -> bool {
matches!(self, Self::Type(_))
}
#[inline]
pub fn as_procedure(&self) -> Option<&ProcedureExport> {
match self {
Self::Procedure(export) => Some(export),
_ => None,
}
}
#[inline]
pub fn as_constant(&self) -> Option<&ConstantExport> {
match self {
Self::Constant(export) => Some(export),
_ => None,
}
}
#[inline]
pub fn as_type(&self) -> Option<&TypeExport> {
match self {
Self::Type(export) => Some(export),
_ => None,
}
}
pub(crate) const fn tag(&self) -> u8 {
unsafe { *(self as *const Self).cast::<u8>() }
}
}
#[cfg(any(test, feature = "arbitrary"))]
impl proptest::arbitrary::Arbitrary for PackageExport {
type Parameters = ();
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
use proptest::{arbitrary::any, prop_oneof, strategy::Strategy};
prop_oneof![
any::<ProcedureExport>().prop_map(Self::Procedure),
any::<ConstantExport>().prop_map(Self::Constant),
any::<TypeExport>().prop_map(Self::Type),
]
.boxed()
}
type Strategy = proptest::prelude::BoxedStrategy<Self>;
}
#[derive(Clone, PartialEq, Eq)]
#[cfg_attr(any(test, feature = "arbitrary"), derive(proptest_derive::Arbitrary))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(
all(feature = "arbitrary", test),
miden_test_serde_macros::serde_test(binary_serde(true))
)]
pub struct ProcedureExport {
#[cfg_attr(feature = "serde", serde(with = "miden_assembly_syntax::ast::path"))]
#[cfg_attr(
any(test, feature = "arbitrary"),
proptest(strategy = "miden_assembly_syntax::arbitrary::path::bare_path_random_length(2)")
)]
pub path: Arc<Path>,
#[cfg_attr(any(test, feature = "arbitrary"), proptest(value = "None"))]
#[cfg_attr(feature = "serde", serde(default))]
pub node: Option<MastNodeId>,
#[cfg_attr(any(test, feature = "arbitrary"), proptest(value = "None"))]
#[cfg_attr(feature = "serde", serde(default))]
pub source_node: Option<DebugSourceNodeId>,
#[cfg_attr(any(test, feature = "arbitrary"), proptest(value = "Word::default()"))]
pub digest: Word,
#[cfg_attr(any(test, feature = "arbitrary"), proptest(value = "None"))]
#[cfg_attr(feature = "serde", serde(default))]
pub signature: Option<FunctionType>,
#[cfg_attr(any(test, feature = "arbitrary"), proptest(value = "AttributeSet::default()"))]
#[cfg_attr(feature = "serde", serde(default))]
pub attributes: AttributeSet,
}
impl ProcedureExport {
pub fn new(
path: Arc<Path>,
node: Option<MastNodeId>,
digest: Word,
signature: Option<FunctionType>,
) -> Self {
Self {
path,
node,
source_node: None,
digest,
signature,
attributes: Default::default(),
}
}
pub fn with_source_node(mut self, source_node: Option<DebugSourceNodeId>) -> Self {
self.source_node = source_node;
self
}
}
impl fmt::Debug for ProcedureExport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
path,
node,
source_node,
digest,
signature,
attributes,
} = self;
f.debug_struct("PackageExport")
.field("path", &format_args!("{path}"))
.field("node", node)
.field("source_node", source_node)
.field("digest", &format_args!("{}", DisplayHex::new(&digest.as_bytes())))
.field("signature", signature)
.field("attributes", attributes)
.finish()
}
}
#[derive(Clone, PartialEq, Eq)]
#[cfg_attr(any(test, feature = "arbitrary"), derive(proptest_derive::Arbitrary))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(
all(feature = "arbitrary", test),
miden_test_serde_macros::serde_test(binary_serde(true))
)]
pub struct ConstantExport {
#[cfg_attr(feature = "serde", serde(with = "miden_assembly_syntax::ast::path"))]
#[cfg_attr(
any(test, feature = "arbitrary"),
proptest(
strategy = "miden_assembly_syntax::arbitrary::path::constant_path_random_length(1)"
)
)]
pub path: Arc<Path>,
pub value: ast::ConstantValue,
}
impl fmt::Debug for ConstantExport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self { path, value } = self;
f.debug_struct("ConstantExport")
.field("path", &format_args!("{path}"))
.field("value", value)
.finish()
}
}
#[derive(Clone, PartialEq, Eq)]
#[cfg_attr(any(test, feature = "arbitrary"), derive(proptest_derive::Arbitrary))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(
all(feature = "arbitrary", test),
miden_test_serde_macros::serde_test(binary_serde(true))
)]
pub struct TypeExport {
#[cfg_attr(feature = "serde", serde(with = "miden_assembly_syntax::ast::path"))]
#[cfg_attr(
any(test, feature = "arbitrary"),
proptest(
strategy = "miden_assembly_syntax::arbitrary::path::user_defined_type_path_random_length(1)"
)
)]
pub path: Arc<Path>,
#[cfg_attr(any(test, feature = "arbitrary"), proptest(value = "Type::Felt"))]
pub ty: Type,
}
impl fmt::Debug for TypeExport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self { path, ty } = self;
f.debug_struct("TypeExport")
.field("path", &format_args!("{path}"))
.field("ty", ty)
.finish()
}
}
fn normalize_export(export: &mut PackageExport) -> Result<(), ManifestValidationError> {
let canonical_path = canonicalize_export_path(export.path().as_ref())?;
let leaf = export_raw_leaf(&canonical_path)?;
match export {
PackageExport::Procedure(proc) => {
ast::ProcedureName::new(leaf).map_err(|err| {
ManifestValidationError::InvalidExportPath {
path: proc.path.clone(),
error: ast::PathError::InvalidComponent(err),
}
})?;
proc.path = canonical_path;
},
PackageExport::Constant(ConstantExport { path, .. })
| PackageExport::Type(TypeExport { path, .. }) => {
ast::Ident::new(leaf).map_err(|err| ManifestValidationError::InvalidExportPath {
path: path.clone(),
error: ast::PathError::InvalidComponent(err),
})?;
*path = canonical_path;
},
}
Ok(())
}
fn normalize_module(module: &mut PackageModule) -> Result<(), ManifestValidationError> {
use alloc::collections::BTreeSet;
let canonical_path = canonicalize_module_path(module.path.as_ref())?;
let mut declared = BTreeSet::new();
for submodule in module.submodules.iter() {
let name = submodule.name.as_str();
if !declared.insert(name.to_string()) {
return Err(ManifestValidationError::DuplicateSubmodule {
module: canonical_path,
name: name.to_string(),
});
}
}
module.path = canonical_path;
Ok(())
}
fn canonicalize_module_path(path: &Path) -> Result<Arc<Path>, ManifestValidationError> {
let canonical =
path.canonicalize()
.map_err(|error| ManifestValidationError::InvalidModulePath {
error,
path: path.to_path_buf().into(),
})?;
Ok(Arc::<Path>::from(canonical.into_boxed_path()))
}
fn canonicalize_export_path(path: &Path) -> Result<Arc<Path>, ManifestValidationError> {
let canonical =
path.canonicalize()
.map_err(|error| ManifestValidationError::InvalidExportPath {
error,
path: path.to_path_buf().into(),
})?;
Ok(Arc::<Path>::from(canonical.into_boxed_path()))
}
fn export_raw_leaf(path: &Arc<Path>) -> Result<&str, ManifestValidationError> {
use ast::PathComponent;
match path.components().next_back() {
Some(Ok(PathComponent::Normal(leaf))) => Ok(leaf),
Some(Err(error)) => {
Err(ManifestValidationError::InvalidExportPath { path: path.clone(), error })
},
Some(Ok(PathComponent::Root)) | None => Err(ManifestValidationError::InvalidExportPath {
path: path.clone(),
error: ast::PathError::Empty,
}),
}
}