#[cfg(any(test, feature = "arbitrary"))]
pub mod arbitrary;
mod error;
mod id;
mod manifest;
mod section;
#[cfg(test)]
mod seed_gen;
mod serialization;
mod target_type;
use alloc::{
borrow::Cow,
boxed::Box,
collections::BTreeMap,
format,
string::{String, ToString},
sync::Arc,
vec::Vec,
};
use miden_assembly_syntax::{
Path, Report,
ast::{self, QualifiedProcedureName},
module::ModuleInfo,
};
#[cfg(feature = "std")]
use miden_core::serde::DeserializationError;
use miden_core::{
Word,
advice::AdviceMap,
crypto::hash::Poseidon2,
mast::{MastForest, MastNode, MastNodeExt, MastNodeId},
program::Kernel,
serde::{ByteReader, ByteWriter, Deserializable, Serializable, SliceReader},
};
pub use self::{
error::{PackageDebugInfoError, PackageStripError},
id::PackageId,
manifest::{
ConstantExport, ManifestValidationError, PackageExport, PackageManifest, PackageModule,
PackageSubmodule, ProcedureExport, TypeExport,
},
section::{InvalidSectionIdError, Section, SectionId},
target_type::{InvalidTargetTypeError, TargetType},
};
use crate::{
Dependency, Version,
debug_info::{
DebugFunctionInfo, DebugFunctionsSection, DebugSourceNodeId, DebugSourcesSection,
DebugTypeIdx, DebugTypeInfo, DebugTypesSection, PackageDebugInfo,
},
};
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Package {
pub name: PackageId,
pub version: Version,
digest: Word,
pub description: Option<String>,
pub kind: TargetType,
mast: Arc<MastForest>,
pub manifest: PackageManifest,
pub sections: Vec<Section>,
debug_sections_trusted: bool,
}
impl Package {
pub fn create(
name: PackageId,
version: Version,
kind: TargetType,
mast: Arc<MastForest>,
exports: impl IntoIterator<Item = PackageExport>,
dependencies: impl IntoIterator<Item = Dependency>,
) -> Result<Self, ManifestValidationError> {
Self::create_with_modules(name, version, kind, mast, exports, [], dependencies)
}
pub fn create_with_modules(
name: PackageId,
version: Version,
kind: TargetType,
mast: Arc<MastForest>,
exports: impl IntoIterator<Item = PackageExport>,
modules: impl IntoIterator<Item = PackageModule>,
dependencies: impl IntoIterator<Item = Dependency>,
) -> Result<Self, ManifestValidationError> {
let manifest = PackageManifest::new(exports)?
.with_modules(modules)?
.with_dependencies(dependencies)?;
if manifest.entrypoint().is_some() && !kind.is_executable() {
return Err(ManifestValidationError::NonExecutableEntrypoint);
}
for export in manifest.exports() {
if let Some(proc) = export.as_procedure()
&& let Some(node) = proc.node
&& !mast.is_procedure_root_with_exact_digest(node, proc.digest)
{
return Err(ManifestValidationError::InvalidProcedureExport {
path: proc.path.clone(),
});
}
}
let mut package = Self {
name,
version,
digest: Default::default(),
description: None,
kind,
mast,
manifest,
sections: Vec::new(),
debug_sections_trusted: true,
};
package.recompute_mast_commitment()?;
Ok(package)
}
fn recompute_mast_commitment(&mut self) -> Result<(), ManifestValidationError> {
let mut node_ids = Vec::with_capacity(self.manifest.num_exports());
for export in self.manifest.exports() {
if let PackageExport::Procedure(export) = export {
if let Some(node_id) = export.node {
node_ids.push(node_id);
} else {
node_ids.push(self.mast.find_procedure_root(export.digest).ok_or_else(
|| ManifestValidationError::MissingProcedureMast {
path: export.path.clone(),
digest: export.digest,
},
)?);
}
}
}
let digest = self.mast.compute_nodes_commitment(node_ids.iter());
self.digest = digest;
Ok(())
}
pub fn with_advice_map(mut self, advice_map: AdviceMap) -> Self {
self.extend_advice_map(advice_map);
self
}
pub fn extend_advice_map(&mut self, advice_map: AdviceMap) {
self.mast = Arc::new(self.mast.as_ref().clone().with_advice_map(advice_map));
}
pub fn strip_debug_info(&mut self) -> Result<(), PackageStripError> {
for section in self.sections.iter_mut().filter(|section| section.id == SectionId::KERNEL) {
let mut kernel_package = Self::read_from_bytes(section.data.as_ref())
.map_err(|source| PackageStripError::DecodeEmbeddedKernel { source })?;
kernel_package.strip_debug_info()?;
section.data = Cow::Owned(kernel_package.to_bytes());
}
self.sections.retain(|section| !section.id.is_debug());
Ok(())
}
pub fn without_debug_info(mut self) -> Result<Self, PackageStripError> {
self.strip_debug_info()?;
Ok(self)
}
}
impl Package {
pub const EXTENSION: &str = "masp";
#[inline]
pub fn mast_forest(&self) -> &Arc<MastForest> {
&self.mast
}
#[inline]
pub fn digest(&self) -> Word {
self.digest
}
pub fn content_digest(&self) -> Word {
let mut bytes = Vec::new();
self.write_content_digest_preimage(&mut bytes, None);
Poseidon2::hash(&bytes)
}
fn write_content_digest_preimage<W: ByteWriter>(
&self,
target: &mut W,
kernel_digest: Option<&Word>,
) {
target.write_bytes(b"miden.package.content.v2");
self.digest().write_into(target);
self.name.write_into(target);
self.version.to_string().write_into(target);
target.write_u8(self.kind.into());
self.manifest.write_into(target);
self.write_content_digest_sections(target);
target.write_bool(kernel_digest.is_some());
if let Some(kernel_digest) = kernel_digest {
kernel_digest.write_into(target);
}
}
fn write_content_digest_sections<W: ByteWriter>(&self, target: &mut W) {
let semantic_sections = self
.sections
.iter()
.filter(|section| section.id == SectionId::ACCOUNT_COMPONENT_METADATA)
.collect::<Vec<_>>();
target.write_usize(semantic_sections.len());
for section in semantic_sections {
section.write_into(target);
}
}
pub fn is_program(&self) -> bool {
self.kind.is_executable()
}
pub fn is_library(&self) -> bool {
self.kind.is_library()
}
pub fn is_kernel(&self) -> bool {
matches!(self.kind, TargetType::Kernel)
}
#[inline]
pub fn entrypoint(&self) -> Option<Arc<Path>> {
self.manifest.entrypoint()
}
#[inline]
pub fn entrypoint_source_node(&self) -> Option<DebugSourceNodeId> {
self.entrypoint()
.as_deref()
.and_then(|entrypoint| self.get_export_by_lookup_path(entrypoint))
.and_then(PackageExport::as_procedure)
.and_then(|procedure| procedure.source_node)
}
pub fn kernel_module_info(&self) -> Result<ModuleInfo, Report> {
self.try_module_infos()
.map_err(Report::msg)?
.into_iter()
.find(|mi| mi.path().is_kernel_path())
.ok_or_else(|| Report::msg("invalid kernel package: does not contain kernel module"))
}
pub fn kernel_runtime_dependency(&self) -> Result<Option<&Dependency>, Report> {
let mut kernel_dependencies = self
.manifest
.dependencies()
.filter(|dependency| dependency.kind == TargetType::Kernel);
let Some(kernel_dependency) = kernel_dependencies.next() else {
return Ok(None);
};
if kernel_dependencies.next().is_some() {
return Err(Report::msg(format!(
"package '{}' declares multiple kernel runtime dependencies",
self.name
)));
}
Ok(Some(kernel_dependency))
}
pub fn debug_info(&self) -> Result<Option<PackageDebugInfo>, PackageDebugInfoError> {
if !self.debug_sections_trusted && self.sections.iter().any(|section| section.id.is_debug())
{
return Err(PackageDebugInfoError::UntrustedSections);
}
let debug_info = PackageDebugInfo {
types: self.read_debug_section(SectionId::DEBUG_TYPES)?,
sources: self.read_debug_section(SectionId::DEBUG_SOURCES)?,
functions: self.read_debug_section(SectionId::DEBUG_FUNCTIONS)?,
source_graph: self.read_debug_section(SectionId::DEBUG_SOURCE_GRAPH)?,
source_map: self.read_debug_section(SectionId::DEBUG_SOURCE_MAP)?,
error_messages: self.read_debug_section(SectionId::DEBUG_ERROR_MESSAGES)?,
};
if debug_info.is_empty() {
return Ok(None);
}
self.validate_debug_info(&debug_info)?;
Ok(Some(debug_info))
}
pub fn get_export_node_id(&self, path: impl AsRef<Path>) -> MastNodeId {
self.get_export_by_lookup_path(path.as_ref())
.and_then(PackageExport::as_procedure)
.and_then(|export| export.node.or_else(|| self.mast.find_procedure_root(export.digest)))
.expect("procedure not exported from this package")
}
pub fn is_reexport(&self, path: impl AsRef<Path>) -> bool {
self.get_export_by_lookup_path(path.as_ref())
.and_then(PackageExport::as_procedure)
.and_then(|export| export.node.or_else(|| self.mast.find_procedure_root(export.digest)))
.map(|node| self.mast[node].is_external())
.unwrap_or(false)
}
pub fn get_procedure_root_by_path(&self, path: impl AsRef<Path>) -> Option<Word> {
self.get_export_by_lookup_path(path.as_ref())
.and_then(PackageExport::as_procedure)
.map(|proc| proc.digest)
}
pub fn get_procedure_node_by_path(&self, path: impl AsRef<Path>) -> Option<MastNodeId> {
self.get_export_by_lookup_path(path.as_ref())
.and_then(PackageExport::as_procedure)
.and_then(|export| export.node.or_else(|| self.mast.find_procedure_root(export.digest)))
}
fn get_export_by_lookup_path(&self, path: &Path) -> Option<&PackageExport> {
self.manifest
.get_export(path)
.or_else(|| path.is_absolute().then(|| self.manifest.get_export(path.to_relative()))?)
.or_else(|| {
if path.is_absolute() {
None
} else {
path.to_absolute().ok().and_then(|path| self.manifest.get_export(path.as_ref()))
}
})
}
pub fn module_infos(&self) -> impl Iterator<Item = ModuleInfo> {
let mut modules_by_path: BTreeMap<Arc<Path>, ModuleInfo> = BTreeMap::new();
for module in self.manifest.modules() {
let mut module_info = ModuleInfo::new(module.path.clone(), None);
for submodule in module.submodules() {
module_info.add_submodule(ast::SubmoduleDecl {
visibility: ast::Visibility::Public,
name: submodule.name.clone(),
});
}
modules_by_path.insert(module.path.clone(), module_info);
}
for export in self.manifest.exports() {
let module_name =
Arc::from(export.path().parent().unwrap().to_path_buf().into_boxed_path());
let module = modules_by_path
.entry(Arc::clone(&module_name))
.or_insert_with(|| ModuleInfo::new(module_name, None));
match export {
PackageExport::Procedure(ProcedureExport {
node,
source_node,
digest,
path,
signature,
attributes,
}) => {
let name = path.last().unwrap();
module.add_procedure_with_provenance(
ast::ProcedureName::new(name).expect("valid procedure name"),
*digest,
signature.clone().map(Arc::new),
attributes.clone(),
*node,
source_node.map(u32::from),
Some(self.mast.commitment()),
);
},
PackageExport::Constant(ConstantExport { path, value }) => {
let name = ast::Ident::new(path.last().unwrap()).expect("valid identifier");
module.add_constant(name, value.clone());
},
PackageExport::Type(TypeExport { path, ty }) => {
let name = ast::Ident::new(path.last().unwrap()).expect("valid identifier");
module.add_type(name, ty.clone());
},
}
}
modules_by_path.into_values()
}
pub fn try_module_infos(&self) -> Result<Vec<ModuleInfo>, ManifestValidationError> {
let mut modules_by_path: BTreeMap<Arc<Path>, ModuleInfo> = BTreeMap::new();
for module in self.manifest.modules() {
let mut module_info = ModuleInfo::new(module.path.clone(), None);
for submodule in module.submodules() {
module_info.add_submodule(ast::SubmoduleDecl {
visibility: ast::Visibility::Public,
name: submodule.name.clone(),
});
}
modules_by_path.insert(module.path.clone(), module_info);
}
for module in self.manifest.modules() {
for submodule in module.submodules() {
let child_path: Arc<Path> =
Arc::from(module.path.join(&submodule.name).into_boxed_path());
if !modules_by_path.contains_key(child_path.as_ref()) {
return Err(ManifestValidationError::MissingDeclaredSubmoduleSurface {
parent: module.path.clone(),
name: submodule.name.to_string(),
module: child_path,
});
}
}
}
for module in self.manifest.modules() {
let Some(parent_path) = module.path.parent() else {
continue;
};
let parent_path: Arc<Path> = Arc::from(parent_path.to_path_buf().into_boxed_path());
let Some(parent) = self.manifest.get_module(parent_path.as_ref()) else {
continue;
};
let name = module.path.last().expect("module paths have at least one component");
if !parent.submodules().iter().any(|submodule| submodule.name.as_str() == name) {
return Err(ManifestValidationError::UndeclaredModuleSurface {
module: module.path.clone(),
parent: parent.path.clone(),
name: name.to_string(),
});
}
}
for export in self.manifest.exports() {
let module_name: Arc<Path> =
Arc::from(export.path().parent().unwrap().to_path_buf().into_boxed_path());
let module = modules_by_path.get_mut(module_name.as_ref()).ok_or_else(|| {
ManifestValidationError::MissingExportModuleSurface {
export: export.path(),
module: module_name.clone(),
}
})?;
match export {
PackageExport::Procedure(ProcedureExport {
node,
source_node,
digest,
path,
signature,
attributes,
}) => {
let name = path.last().unwrap();
module.add_procedure_with_provenance(
ast::ProcedureName::new(name).expect("valid procedure name"),
*digest,
signature.clone().map(Arc::new),
attributes.clone(),
*node,
source_node.map(u32::from),
Some(self.mast.commitment()),
);
},
PackageExport::Constant(ConstantExport { path, value }) => {
let name = ast::Ident::new(path.last().unwrap()).expect("valid identifier");
module.add_constant(name, value.clone());
},
PackageExport::Type(TypeExport { path, ty }) => {
let name = ast::Ident::new(path.last().unwrap()).expect("valid identifier");
module.add_type(name, ty.clone());
},
}
}
Ok(modules_by_path.into_values().collect())
}
fn read_debug_section<T>(&self, id: SectionId) -> Result<Option<T>, PackageDebugInfoError>
where
T: Deserializable,
{
let mut sections = self.sections.iter().filter(|section| section.id == id);
let Some(section) = sections.next() else {
return Ok(None);
};
if sections.next().is_some() {
return Err(PackageDebugInfoError::DuplicateSection { id });
}
read_section_payload(&id, section.data.as_ref()).map(Some)
}
fn validate_debug_info(
&self,
debug_info: &PackageDebugInfo,
) -> Result<(), PackageDebugInfoError> {
if let Some(types) = debug_info.types.as_ref() {
self.validate_debug_types(types)?;
}
if let Some(sources) = debug_info.sources.as_ref() {
self.validate_debug_sources(sources)?;
}
if let Some(functions) = debug_info.functions.as_ref() {
self.validate_debug_functions(
debug_info.types.as_ref(),
debug_info.sources.as_ref(),
functions,
)?;
}
let source_graph = debug_info.source_graph.as_ref();
if let Some(source_map) = debug_info.source_map.as_ref()
&& source_graph.is_none()
&& !source_map.is_empty()
{
return Err(PackageDebugInfoError::InvalidReference {
message: "debug source map is present without a debug source graph".to_string(),
});
}
let Some(source_graph) = source_graph else {
return Ok(());
};
for root in source_graph.roots().iter().copied() {
if debug_info.source_node(root).is_none() {
return Err(PackageDebugInfoError::InvalidReference {
message: format!("debug source root {root:?} is not present in the graph"),
});
}
}
for (source_index, source_node) in source_graph.nodes().iter().enumerate() {
let source_id = DebugSourceNodeId::from(source_index as u32);
let Some(exec_node) = self.mast.get_node_by_id(source_node.exec_node) else {
return Err(PackageDebugInfoError::InvalidReference {
message: format!(
"debug source node {source_id:?} references missing execution node {:?}",
source_node.exec_node,
),
});
};
if source_node.op_start > source_node.op_end {
return Err(PackageDebugInfoError::InvalidReference {
message: format!(
"debug source node {source_id:?} has invalid operation range {}..{}",
source_node.op_start, source_node.op_end,
),
});
}
if let MastNode::Block(block) = exec_node {
let num_ops = block.num_operations();
if source_node.op_end > num_ops {
return Err(PackageDebugInfoError::InvalidReference {
message: format!(
"debug source node {source_id:?} has operation range {}..{}, outside execution node {:?} operation count {num_ops}",
source_node.op_start, source_node.op_end, source_node.exec_node,
),
});
}
}
let mut exec_children = Vec::new();
exec_node.for_each_child(|child_id| exec_children.push(child_id));
if exec_children.len() != source_node.children.len() {
return Err(PackageDebugInfoError::InvalidReference {
message: format!(
"debug source node {source_id:?} has {} children, expected {} from execution node {:?}",
source_node.children.len(),
exec_children.len(),
source_node.exec_node,
),
});
}
for (child_index, child_source_id) in source_node.children.iter().copied().enumerate() {
let Some(child_source_node) = debug_info.source_node(child_source_id) else {
return Err(PackageDebugInfoError::InvalidReference {
message: format!(
"debug source node {source_id:?} references missing child source node {child_source_id:?}",
),
});
};
if child_source_node.exec_node != exec_children[child_index] {
return Err(PackageDebugInfoError::InvalidReference {
message: format!(
"debug source node {source_id:?} child {child_index} maps to {:?}, expected {:?}",
child_source_node.exec_node, exec_children[child_index],
),
});
}
}
}
if let Some(source_map) = debug_info.source_map.as_ref() {
for row in source_map.asm_ops() {
self.validate_source_map_row(
source_graph,
row.source_node,
row.op_idx,
"assembly op",
)?;
}
for row in source_map.debug_vars() {
self.validate_source_map_row(
source_graph,
row.source_node,
row.op_idx,
"debug variable",
)?;
}
let function_count =
debug_info.functions.as_ref().map_or(0, |section| section.functions.len());
let file_count = debug_info.sources.as_ref().map_or(0, |section| section.files.len());
for row in source_map.inline_calls() {
self.validate_source_map_row(
source_graph,
row.source_node,
row.op_idx,
"inline call",
)?;
if row.callee_idx as usize >= function_count {
return Err(PackageDebugInfoError::InvalidReference {
message: format!(
"debug inline call callee index {} is outside debug function table length {function_count}",
row.callee_idx,
),
});
}
if row.file_idx as usize >= file_count {
return Err(PackageDebugInfoError::InvalidReference {
message: format!(
"debug inline call file index {} is outside debug source file table length {file_count}",
row.file_idx,
),
});
}
}
}
for export in self.manifest.exports() {
let Some(procedure) = export.as_procedure() else {
continue;
};
let Some(source_node_id) = procedure.source_node else {
continue;
};
let Some(source_node) = debug_info.source_node(source_node_id) else {
return Err(PackageDebugInfoError::InvalidReference {
message: format!(
"procedure export '{}' references missing source node {source_node_id:?}",
procedure.path,
),
});
};
let Some(export_node) =
procedure.node.or_else(|| self.mast.find_procedure_root(procedure.digest))
else {
return Err(PackageDebugInfoError::InvalidReference {
message: format!(
"procedure export '{}' does not resolve to an execution node",
procedure.path,
),
});
};
if source_node.exec_node != export_node {
return Err(PackageDebugInfoError::InvalidReference {
message: format!(
"procedure export '{}' source node {source_node_id:?} maps to {:?}, expected {export_node:?}",
procedure.path, source_node.exec_node,
),
});
}
}
Ok(())
}
fn validate_debug_types(&self, types: &DebugTypesSection) -> Result<(), PackageDebugInfoError> {
let type_count = types.types.len();
let string_count = types.strings.len();
for (type_index, ty) in types.types.iter().enumerate() {
self.validate_debug_type(ty, type_index, type_count, string_count)?;
}
Ok(())
}
fn validate_debug_type(
&self,
ty: &DebugTypeInfo,
type_index: usize,
type_count: usize,
string_count: usize,
) -> Result<(), PackageDebugInfoError> {
match ty {
DebugTypeInfo::Primitive(_) | DebugTypeInfo::Unknown => Ok(()),
DebugTypeInfo::Pointer { pointee_type_idx } => self.validate_type_index(
*pointee_type_idx,
type_count,
format!("debug type {type_index} pointer target"),
),
DebugTypeInfo::Array { element_type_idx, .. } => self.validate_type_index(
*element_type_idx,
type_count,
format!("debug type {type_index} array element"),
),
DebugTypeInfo::Struct { name_idx, fields, .. } => {
self.validate_string_index(
*name_idx,
string_count,
format!("debug type {type_index} struct name"),
)?;
for (field_index, field) in fields.iter().enumerate() {
self.validate_string_index(
field.name_idx,
string_count,
format!("debug type {type_index} field {field_index} name"),
)?;
self.validate_type_index(
field.type_idx,
type_count,
format!("debug type {type_index} field {field_index} type"),
)?;
}
Ok(())
},
DebugTypeInfo::Function { return_type_idx, param_type_indices } => {
if let Some(return_type_idx) = return_type_idx {
self.validate_type_index(
*return_type_idx,
type_count,
format!("debug type {type_index} function return type"),
)?;
}
for (param_index, param_type_idx) in param_type_indices.iter().copied().enumerate()
{
self.validate_type_index(
param_type_idx,
type_count,
format!("debug type {type_index} function parameter {param_index}"),
)?;
}
Ok(())
},
DebugTypeInfo::Enum {
name_idx,
discriminant_type_idx,
variants,
..
} => {
self.validate_string_index(
*name_idx,
string_count,
format!("debug type {type_index} enum name"),
)?;
self.validate_type_index(
*discriminant_type_idx,
type_count,
format!("debug type {type_index} enum discriminant"),
)?;
for (variant_index, variant) in variants.iter().enumerate() {
self.validate_string_index(
variant.name_idx,
string_count,
format!("debug type {type_index} variant {variant_index} name"),
)?;
if let Some(type_idx) = variant.type_idx {
self.validate_type_index(
type_idx,
type_count,
format!("debug type {type_index} variant {variant_index} payload"),
)?;
}
}
Ok(())
},
}
}
fn validate_debug_sources(
&self,
sources: &DebugSourcesSection,
) -> Result<(), PackageDebugInfoError> {
let string_count = sources.strings.len();
for (file_index, file) in sources.files.iter().enumerate() {
self.validate_string_index(
file.path_idx,
string_count,
format!("debug source file {file_index} path"),
)?;
}
Ok(())
}
fn validate_debug_functions(
&self,
types: Option<&DebugTypesSection>,
sources: Option<&DebugSourcesSection>,
functions: &DebugFunctionsSection,
) -> Result<(), PackageDebugInfoError> {
let string_count = functions.strings.len();
let file_count = sources.map_or(0, |sources| sources.files.len());
let type_count = types.map_or(0, |types| types.types.len());
for (function_index, function) in functions.functions.iter().enumerate() {
self.validate_debug_function(
function,
function_index,
string_count,
file_count,
type_count,
)?;
}
Ok(())
}
fn validate_debug_function(
&self,
function: &DebugFunctionInfo,
function_index: usize,
string_count: usize,
file_count: usize,
type_count: usize,
) -> Result<(), PackageDebugInfoError> {
self.validate_string_index(
function.name_idx,
string_count,
format!("debug function {function_index} name"),
)?;
if let Some(linkage_name_idx) = function.linkage_name_idx {
self.validate_string_index(
linkage_name_idx,
string_count,
format!("debug function {function_index} linkage name"),
)?;
}
if function.file_idx as usize >= file_count {
return Err(PackageDebugInfoError::InvalidReference {
message: format!(
"debug function {function_index} file index {} is outside debug source file table length {file_count}",
function.file_idx,
),
});
}
if let Some(type_idx) = function.type_idx {
self.validate_type_index(
type_idx,
type_count,
format!("debug function {function_index} type"),
)?;
}
Ok(())
}
fn validate_string_index(
&self,
index: u32,
string_count: usize,
context: String,
) -> Result<(), PackageDebugInfoError> {
if index as usize >= string_count {
return Err(PackageDebugInfoError::InvalidReference {
message: format!(
"{context} string index {index} is outside string table length {string_count}",
),
});
}
Ok(())
}
fn validate_type_index(
&self,
index: DebugTypeIdx,
type_count: usize,
context: String,
) -> Result<(), PackageDebugInfoError> {
if index.as_u32() as usize >= type_count {
return Err(PackageDebugInfoError::InvalidReference {
message: format!(
"{context} type index {} is outside type table length {type_count}",
index.as_u32(),
),
});
}
Ok(())
}
fn validate_source_map_row(
&self,
source_graph: &crate::debug_info::DebugSourceGraphSection,
source_node_id: DebugSourceNodeId,
op_idx: u32,
row_kind: &'static str,
) -> Result<(), PackageDebugInfoError> {
let Some(source_node) = source_graph.nodes().get(source_node_id.as_u32() as usize) else {
return Err(PackageDebugInfoError::InvalidReference {
message: format!(
"{row_kind} row references missing source node {source_node_id:?}"
),
});
};
if op_idx < source_node.op_start || op_idx >= source_node.op_end {
return Err(PackageDebugInfoError::InvalidReference {
message: format!(
"{row_kind} row for source node {source_node_id:?} has op index {op_idx}, outside source range {}..{}",
source_node.op_start, source_node.op_end,
),
});
}
Ok(())
}
}
fn read_section_payload<T>(id: &SectionId, bytes: &[u8]) -> Result<T, PackageDebugInfoError>
where
T: Deserializable,
{
let mut reader = SliceReader::new(bytes);
let section = T::read_from(&mut reader)
.map_err(|source| PackageDebugInfoError::DecodeSection { id: id.clone(), source })?;
if reader.has_more_bytes() {
return Err(PackageDebugInfoError::TrailingBytes { id: id.clone() });
}
Ok(section)
}
impl Package {
pub fn to_kernel(&self) -> Result<Kernel, Report> {
let exports = self
.manifest
.exports()
.filter_map(|export| {
if export.namespace().is_kernel_path()
&& let PackageExport::Procedure(p) = export
{
Some(p.digest)
} else {
None
}
})
.collect::<Vec<_>>();
if exports.is_empty() {
return Err(Report::msg(
"invalid kernel package: does not export any kernel procedures",
));
}
Kernel::new(&exports).map_err(|err| Report::msg(format!("invalid kernel package: {err}")))
}
#[doc(hidden)]
pub fn try_into_program(&self) -> Result<miden_core::program::Program, Report> {
use miden_assembly_syntax::{Path as MasmPath, ast};
use miden_core::program::Program;
if !self.is_program() {
return Err(Report::msg(format!(
"cannot convert package of type {} to Executable",
self.kind
)));
}
let entrypoint = self.manifest.entrypoint().unwrap_or_else(|| {
MasmPath::exec_path().join(ast::ProcedureName::MAIN_PROC_NAME).into()
});
if let Some(entrypoint) = self.get_procedure_node_by_path(&entrypoint) {
let mast_forest = self.mast.clone();
let kernel_dependency = self.kernel_runtime_dependency()?.cloned();
match (self.try_embedded_kernel_package()?, kernel_dependency) {
(Some(kernel_package), _) => {
Ok(Program::with_kernel(mast_forest, entrypoint, kernel_package.to_kernel()?))
},
(None, Some(kernel_dependency)) => Err(Report::msg(format!(
"package '{}' declares kernel runtime dependency '{}@{}#{}', but does not embed the kernel package required to reconstruct a program",
self.name,
kernel_dependency.name,
kernel_dependency.version,
kernel_dependency.digest
))),
(None, None) => Ok(Program::new(mast_forest, entrypoint)),
}
} else {
Err(Report::msg(format!(
"malformed executable package: no procedure root for '{entrypoint}'"
)))
}
}
#[doc(hidden)]
pub fn unwrap_program(&self) -> miden_core::program::Program {
assert_eq!(self.kind, TargetType::Executable);
self.try_into_program().unwrap_or_else(|err| panic!("{err}"))
}
pub fn try_embedded_kernel_package(&self) -> Result<Option<Box<Self>>, Report> {
let Some(kernel_package) = self.embedded_kernel_package()? else {
return Ok(None);
};
self.validate_embedded_kernel_dependency(&kernel_package)?;
Ok(Some(kernel_package))
}
fn embedded_kernel_package(&self) -> Result<Option<Box<Self>>, Report> {
let mut sections = self.sections.iter().filter(|section| section.id == SectionId::KERNEL);
let Some(section) = sections.next() else {
return Ok(None);
};
if sections.next().is_some() {
return Err(Report::msg(format!(
"package '{}' contains multiple '{}' sections",
self.name,
SectionId::KERNEL
)));
}
Self::read_from_bytes(section.data.as_ref())
.map(Box::new)
.map(Some)
.map_err(|error| {
Report::msg(format!(
"failed to decode embedded kernel package for '{}': {error}",
self.name
))
})
}
fn validate_embedded_kernel_dependency(&self, kernel_package: &Self) -> Result<(), Report> {
if !kernel_package.is_kernel() {
return Err(Report::msg(format!(
"package '{}' embeds '{}', but its kind is '{}'",
self.name, kernel_package.name, kernel_package.kind
)));
}
let Some(kernel_dependency) = self.kernel_runtime_dependency()? else {
return Err(Report::msg(format!(
"package '{}' embeds a kernel package, but does not declare a kernel runtime dependency",
self.name
)));
};
if kernel_dependency.name != kernel_package.name
|| kernel_dependency.version != kernel_package.version
|| kernel_dependency.digest != kernel_package.digest()
{
return Err(Report::msg(format!(
"package '{}' declares kernel runtime dependency '{}@{}#{}', but that does not match the embedded kernel package '{}@{}#{}'",
self.name,
kernel_dependency.name,
kernel_dependency.version,
kernel_dependency.digest,
kernel_package.name,
kernel_package.version,
kernel_package.digest()
)));
}
Ok(())
}
pub fn to_dependency(&self) -> Dependency {
Dependency {
name: self.name.clone(),
version: self.version.clone(),
kind: self.kind,
digest: self.digest(),
}
}
pub fn make_executable(&self, entrypoint: &QualifiedProcedureName) -> Result<Self, Report> {
use miden_assembly_syntax::Path as MasmPath;
if !self.is_library() {
return Err(Report::msg("expected library but got an executable"));
}
let entrypoint =
Arc::<MasmPath>::from(entrypoint.to_absolute().map_err(Report::msg)?.to_path_buf());
if let Some(export) = self.get_export_by_lookup_path(&entrypoint) {
match export {
PackageExport::Constant(_) | PackageExport::Type(_) => {
let actual = match export {
PackageExport::Constant(_) => "constant",
PackageExport::Type(_) => "type",
_ => unreachable!(),
};
Err(Report::msg(ManifestValidationError::UnexpectedExportType {
path: entrypoint,
expected: "procedure",
actual,
}))
},
PackageExport::Procedure(procedure) => {
let executable_entrypoint: Arc<MasmPath> =
MasmPath::exec_path().join(ast::ProcedureName::MAIN_PROC_NAME).into();
let mut procedure = procedure.clone();
procedure.path = executable_entrypoint;
let mut package = Self::create(
self.name.clone(),
self.version.clone(),
TargetType::Executable,
self.mast.clone(),
[PackageExport::Procedure(procedure)],
self.manifest.dependencies.clone(),
)
.map_err(Report::msg)?;
package.description = self.description.clone();
package.sections = self.sections.clone();
package.debug_sections_trusted = self.debug_sections_trusted;
Ok(package)
},
}
} else {
Err(Report::msg(format!(
"invalid entrypoint: library does not export '{entrypoint}'"
)))
}
}
}
impl Package {
#[cfg(feature = "std")]
pub fn write_to_file(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
use miden_core::serde::Serializable;
let path = path.as_ref();
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir)?;
}
let mut file = std::fs::File::create(path)?;
<Self as Serializable>::write_into(self, &mut file);
Ok(())
}
#[cfg(feature = "std")]
pub fn write_masp_file(&self, dir: impl AsRef<std::path::Path>) -> std::io::Result<()> {
let dir = dir.as_ref();
let package_name: &str = &self.name;
self.write_to_file(dir.join(package_name).with_extension(Self::EXTENSION))
.map_err(|err| std::io::Error::other(err.to_string()))
}
#[cfg(feature = "std")]
pub fn deserialize_from_file(
path: impl AsRef<std::path::Path>,
) -> Result<Self, DeserializationError> {
let bytes = read_package_file(path)?;
Self::read_from_bytes(&bytes)
}
#[cfg(feature = "std")]
pub fn deserialize_from_file_trusted(
path: impl AsRef<std::path::Path>,
) -> Result<Self, DeserializationError> {
let bytes = read_package_file(path)?;
Self::read_from_bytes_trusted(&bytes)
}
}
#[cfg(feature = "std")]
fn read_package_file(path: impl AsRef<std::path::Path>) -> Result<Vec<u8>, DeserializationError> {
let path = path.as_ref();
std::fs::read(path).map_err(|err| {
DeserializationError::InvalidValue(format!(
"failed to open file at {}: {err}",
path.to_string_lossy()
))
})
}
#[cfg(test)]
mod tests {
use alloc::{sync::Arc, vec, vec::Vec};
use core::{assert_matches, str::FromStr};
use miden_assembly_syntax::ast::{
Path as AstPath, PathBuf, ProcedureName, QualifiedProcedureName,
};
use miden_core::{
advice::AdviceMap,
mast::{
BasicBlockNodeBuilder, ExternalNodeBuilder, MastForest, MastForestContributor,
MastNode, MastNodeExt, MastNodeId, SplitNodeBuilder,
},
operations::Operation,
serde::Serializable,
utils::IndexVec,
};
use miden_debug_types::{ColumnNumber, LineNumber};
use super::*;
use crate::{
Dependency, Version,
debug_info::{
DebugFileInfo, DebugFunctionInfo, DebugFunctionsSection, DebugSourceAsmOp,
DebugSourceGraphSection, DebugSourceInlineCall, DebugSourceMapSection, DebugSourceNode,
DebugSourceNodeId, DebugSourcesSection, DebugTypeIdx, DebugTypeInfo, DebugTypesSection,
},
};
fn build_forest() -> (MastForest, MastNodeId) {
let mut forest = MastForest::new();
let node_id = BasicBlockNodeBuilder::new(vec![Operation::Add])
.add_to_forest(&mut forest)
.expect("failed to build basic block");
forest.make_root(node_id);
(forest, node_id)
}
fn build_split_forest() -> (MastForest, MastNodeId, MastNodeId, MastNodeId) {
let mut forest = MastForest::new();
let left_id = BasicBlockNodeBuilder::new(vec![Operation::Add])
.add_to_forest(&mut forest)
.expect("failed to build left basic block");
let right_id = BasicBlockNodeBuilder::new(vec![Operation::Mul])
.add_to_forest(&mut forest)
.expect("failed to build right basic block");
let root_id = SplitNodeBuilder::new([left_id, right_id])
.add_to_forest(&mut forest)
.expect("failed to build split node");
forest.make_root(root_id);
(forest, root_id, left_id, right_id)
}
fn absolute_path(name: &str) -> Arc<AstPath> {
let path = PathBuf::new(name).expect("invalid path");
let path = path.as_path().to_absolute().unwrap().into_owned();
Arc::from(path.into_boxed_path())
}
fn relative_path(name: &str) -> Arc<AstPath> {
let path = PathBuf::relative(name);
Arc::from(path.into_boxed_path())
}
fn build_package_exports(export: &str) -> (Arc<MastForest>, Vec<PackageExport>) {
let (forest, node_id) = build_forest();
let root = forest[node_id].digest();
let path = absolute_path(export);
let export = ProcedureExport::new(Arc::clone(&path), Some(node_id), root, None);
(Arc::new(forest), vec![PackageExport::Procedure(export)])
}
fn build_split_package_exports(
export: &str,
source_node: Option<DebugSourceNodeId>,
) -> (Arc<MastForest>, Vec<PackageExport>, MastNodeId, MastNodeId, MastNodeId) {
let (forest, root_id, left_id, right_id) = build_split_forest();
let root = forest[root_id].digest();
let path = absolute_path(export);
let export = ProcedureExport::new(Arc::clone(&path), Some(root_id), root, None)
.with_source_node(source_node);
(
Arc::new(forest),
vec![PackageExport::Procedure(export)],
root_id,
left_id,
right_id,
)
}
fn build_same_digest_package_exports(
exports: &[(&str, &str)],
) -> (Arc<MastForest>, Vec<PackageExport>, Vec<Section>) {
let mut nodes = IndexVec::<MastNodeId, MastNode>::new();
let mut roots = Vec::new();
let mut new_exports = vec![];
let mut source_nodes = Vec::new();
let mut asm_ops = Vec::new();
for (source_idx, (path_str, context_name)) in exports.iter().enumerate() {
let node = BasicBlockNodeBuilder::new(vec![Operation::Add])
.build()
.expect("failed to build basic block");
let num_ops = node.num_operations() as usize;
let digest = node.digest();
let node_id = nodes.push(node.into()).expect("failed to add basic block");
let source_node = DebugSourceNodeId::from(source_idx as u32);
source_nodes.push(DebugSourceNode::new(node_id, Vec::new(), 0, num_ops as u32));
asm_ops.push(DebugSourceAsmOp::new(
source_node,
0,
None,
(*context_name).into(),
"add".into(),
1,
));
roots.push(node_id);
let path = absolute_path(path_str);
new_exports.push(PackageExport::Procedure(
ProcedureExport::new(path, Some(node_id), digest, None)
.with_source_node(Some(source_node)),
));
}
let source_graph = DebugSourceGraphSection::from_parts(
source_nodes,
(0..exports.len())
.map(|source_idx| DebugSourceNodeId::from(source_idx as u32))
.collect(),
);
let source_map = DebugSourceMapSection::from_parts(asm_ops, Vec::new());
let sections = vec![
Section::new(SectionId::DEBUG_SOURCE_GRAPH, source_graph.to_bytes()),
Section::new(SectionId::DEBUG_SOURCE_MAP, source_map.to_bytes()),
];
let forest = MastForest::from_raw_parts(nodes, roots, AdviceMap::default())
.expect("failed to build forest");
(Arc::new(forest), new_exports, sections)
}
fn build_package(
name: &str,
kind: TargetType,
export: &str,
dependencies: impl IntoIterator<Item = Dependency>,
sections: Vec<Section>,
) -> Package {
let (mast, exports) = build_package_exports(export);
let mut package = Package::create(
PackageId::from(name),
Version::new(1, 0, 0),
kind,
mast,
exports,
dependencies,
)
.unwrap();
package.sections = sections;
package
}
fn build_kernel_package(name: &str) -> Package {
build_package(name, TargetType::Kernel, &format!("{name}::boot"), [], Vec::new())
}
fn build_debug_package(name: &str, kind: TargetType, export: &str, context: &str) -> Package {
let (mast, exports, sections) = build_same_digest_package_exports(&[(export, context)]);
let mut package = Package::create(
PackageId::from(name),
Version::new(1, 0, 0),
kind,
mast,
exports,
None,
)
.unwrap();
package.sections = sections;
package
}
fn debug_sections() -> Vec<Section> {
vec![
Section::new(SectionId::DEBUG_SOURCES, vec![1, 2, 3]),
Section::new(SectionId::DEBUG_FUNCTIONS, vec![4, 5, 6]),
Section::new(SectionId::DEBUG_TYPES, vec![7, 8, 9]),
Section::new(SectionId::DEBUG_SOURCE_GRAPH, vec![10, 11, 12]),
Section::new(SectionId::DEBUG_SOURCE_MAP, vec![13, 14, 15]),
Section::new(SectionId::DEBUG_ERROR_MESSAGES, vec![16, 17, 18]),
]
}
#[test]
fn package_without_debug_sections_has_no_package_debug_info() {
let package = build_package("app", TargetType::Library, "app::entry", [], Vec::new());
assert!(package.debug_info().unwrap().is_none());
}
#[test]
fn package_debug_info_decodes_source_graph_and_map() {
let mut package = build_package("app", TargetType::Library, "app::entry", [], Vec::new());
let exec_node = package.get_export_node_id("app::entry");
let source_node = DebugSourceNodeId::from(0);
let source_graph = DebugSourceGraphSection::from_parts(
vec![DebugSourceNode::new(exec_node, Vec::new(), 0, 1)],
vec![source_node],
);
let source_map = DebugSourceMapSection::from_parts(
vec![DebugSourceAsmOp::new(
source_node,
0,
None,
"app::entry".into(),
"add".into(),
1,
)],
Vec::new(),
);
package.sections = vec![
Section::new(SectionId::DEBUG_SOURCE_GRAPH, source_graph.to_bytes()),
Section::new(SectionId::DEBUG_SOURCE_MAP, source_map.to_bytes()),
];
let debug_info = package
.debug_info()
.expect("debug sections should decode")
.expect("debug sections should be present");
assert_eq!(debug_info.source_node(source_node).unwrap().exec_node, exec_node);
assert_eq!(
debug_info.asm_op_for_operation(source_node, 0).unwrap().context_name,
"app::entry"
);
}
#[test]
fn package_debug_info_rejects_duplicate_debug_sections() {
let mut package = build_package("app", TargetType::Library, "app::entry", [], Vec::new());
package.sections = vec![
Section::new(SectionId::DEBUG_SOURCE_MAP, DebugSourceMapSection::new().to_bytes()),
Section::new(SectionId::DEBUG_SOURCE_MAP, DebugSourceMapSection::new().to_bytes()),
];
let error = package.debug_info().expect_err("duplicate debug sections should be rejected");
assert!(matches!(
error,
PackageDebugInfoError::DuplicateSection { id } if id == SectionId::DEBUG_SOURCE_MAP
));
}
#[test]
fn package_debug_info_rejects_malformed_debug_sections() {
let mut package = build_package("app", TargetType::Library, "app::entry", [], Vec::new());
package.sections = vec![Section::new(SectionId::DEBUG_SOURCE_GRAPH, vec![u8::MAX])];
let error = package.debug_info().expect_err("malformed debug sections should be rejected");
assert!(matches!(
error,
PackageDebugInfoError::DecodeSection { id, .. } if id == SectionId::DEBUG_SOURCE_GRAPH
));
}
#[test]
fn package_debug_info_rejects_invalid_non_source_graph_table_indices() {
let mut package = build_package("app", TargetType::Library, "app::entry", [], Vec::new());
let mut types = DebugTypesSection::new();
types
.types
.push(DebugTypeInfo::Pointer { pointee_type_idx: DebugTypeIdx::from(99) });
package.sections = vec![Section::new(SectionId::DEBUG_TYPES, types.to_bytes())];
assert!(matches!(
package.debug_info(),
Err(PackageDebugInfoError::InvalidReference { .. })
));
let mut sources = DebugSourcesSection::new();
sources.files.push(DebugFileInfo::new(0));
package.sections = vec![Section::new(SectionId::DEBUG_SOURCES, sources.to_bytes())];
assert!(matches!(
package.debug_info(),
Err(PackageDebugInfoError::InvalidReference { .. })
));
let mut functions = DebugFunctionsSection::new();
let name_idx = functions.add_string(Arc::from("app::entry"));
functions.add_function(DebugFunctionInfo::new(
name_idx,
0,
LineNumber::new(1).unwrap(),
ColumnNumber::new(1).unwrap(),
));
package.sections = vec![Section::new(SectionId::DEBUG_FUNCTIONS, functions.to_bytes())];
assert!(matches!(
package.debug_info(),
Err(PackageDebugInfoError::InvalidReference { .. })
));
}
#[test]
fn package_debug_info_rejects_invalid_inline_call_indices() {
let source_node = DebugSourceNodeId::from(0);
let mut sources = DebugSourcesSection::new();
let path_idx = sources.add_string(Arc::from("app.masm"));
sources.add_file(DebugFileInfo::new(path_idx));
let mut functions = DebugFunctionsSection::new();
let name_idx = functions.add_string(Arc::from("app::entry"));
functions.add_function(DebugFunctionInfo::new(
name_idx,
0,
LineNumber::new(1).unwrap(),
ColumnNumber::new(1).unwrap(),
));
let source_graph = DebugSourceGraphSection::from_parts(
vec![DebugSourceNode::new(MastNodeId::new_unchecked(0), vec![], 0, 1)],
vec![source_node],
);
let source_map = DebugSourceMapSection::from_parts_with_inline_calls(
Vec::new(),
Vec::new(),
vec![DebugSourceInlineCall::new(
source_node,
0,
1,
0,
LineNumber::new(1).unwrap(),
ColumnNumber::new(1).unwrap(),
)],
);
let mut package = build_package("app", TargetType::Library, "app::entry", [], Vec::new());
package.sections = vec![
Section::new(SectionId::DEBUG_SOURCES, sources.to_bytes()),
Section::new(SectionId::DEBUG_FUNCTIONS, functions.to_bytes()),
Section::new(SectionId::DEBUG_SOURCE_GRAPH, source_graph.to_bytes()),
Section::new(SectionId::DEBUG_SOURCE_MAP, source_map.to_bytes()),
];
let err = package.debug_info().expect_err("bad inline call index should be rejected");
assert!(matches!(err, PackageDebugInfoError::InvalidReference { .. }));
}
#[test]
fn package_debug_info_rejects_source_graph_child_exec_mismatch() {
let source_root = DebugSourceNodeId::from(0);
let source_left = DebugSourceNodeId::from(1);
let source_right = DebugSourceNodeId::from(2);
let (mast, exports, root_id, left_id, right_id) =
build_split_package_exports("app::entry", Some(source_root));
let mut package = Package::create(
PackageId::from("app"),
Version::new(1, 0, 0),
TargetType::Library,
mast,
exports,
None,
)
.unwrap();
let source_graph = DebugSourceGraphSection::from_parts(
vec![
DebugSourceNode::new(root_id, vec![source_right, source_left], 0, 1),
DebugSourceNode::new(left_id, Vec::new(), 0, 1),
DebugSourceNode::new(right_id, Vec::new(), 0, 1),
],
vec![source_root],
);
package.sections =
vec![Section::new(SectionId::DEBUG_SOURCE_GRAPH, source_graph.to_bytes())];
let error = package.debug_info().expect_err("mismatched source child should be rejected");
assert!(matches!(error, PackageDebugInfoError::InvalidReference { .. }));
}
#[test]
fn package_debug_info_rejects_invalid_source_node_operation_ranges() {
let mut package = build_package("app", TargetType::Library, "app::entry", [], Vec::new());
let exec_node = package.get_export_node_id("app::entry");
let source_node = DebugSourceNodeId::from(0);
for (op_start, op_end) in [(1, 0), (0, 2)] {
let source_graph = DebugSourceGraphSection::from_parts(
vec![DebugSourceNode::new(exec_node, Vec::new(), op_start, op_end)],
vec![source_node],
);
package.sections =
vec![Section::new(SectionId::DEBUG_SOURCE_GRAPH, source_graph.to_bytes())];
let error = package
.debug_info()
.expect_err("invalid source node operation range should be rejected");
assert!(matches!(error, PackageDebugInfoError::InvalidReference { .. }));
}
}
#[test]
fn package_debug_info_rejects_source_map_missing_source_node() {
let mut package = build_package("app", TargetType::Library, "app::entry", [], Vec::new());
let exec_node = package.get_export_node_id("app::entry");
let source_node = DebugSourceNodeId::from(0);
let missing_source_node = DebugSourceNodeId::from(1);
let source_graph = DebugSourceGraphSection::from_parts(
vec![DebugSourceNode::new(exec_node, Vec::new(), 0, 1)],
vec![source_node],
);
let source_map = DebugSourceMapSection::from_parts(
vec![DebugSourceAsmOp::new(
missing_source_node,
0,
None,
"app::entry".into(),
"add".into(),
1,
)],
Vec::new(),
);
package.sections = vec![
Section::new(SectionId::DEBUG_SOURCE_GRAPH, source_graph.to_bytes()),
Section::new(SectionId::DEBUG_SOURCE_MAP, source_map.to_bytes()),
];
let error = package
.debug_info()
.expect_err("source map row with missing source node should be rejected");
assert!(matches!(error, PackageDebugInfoError::InvalidReference { .. }));
}
#[test]
fn package_debug_info_rejects_export_source_node_exec_mismatch() {
let source_root = DebugSourceNodeId::from(0);
let source_left = DebugSourceNodeId::from(1);
let source_right = DebugSourceNodeId::from(2);
let (mast, exports, root_id, left_id, right_id) =
build_split_package_exports("app::entry", Some(source_left));
let mut package = Package::create(
PackageId::from("app"),
Version::new(1, 0, 0),
TargetType::Library,
mast,
exports,
None,
)
.unwrap();
let source_graph = DebugSourceGraphSection::from_parts(
vec![
DebugSourceNode::new(root_id, vec![source_left, source_right], 0, 1),
DebugSourceNode::new(left_id, Vec::new(), 0, 1),
DebugSourceNode::new(right_id, Vec::new(), 0, 1),
],
vec![source_root],
);
package.sections =
vec![Section::new(SectionId::DEBUG_SOURCE_GRAPH, source_graph.to_bytes())];
let error = package
.debug_info()
.expect_err("export source node mapped to child exec node should be rejected");
assert!(matches!(error, PackageDebugInfoError::InvalidReference { .. }));
}
#[test]
fn to_kernel_rejects_empty_kernel_exports() {
let mut package = build_package("kernel", TargetType::Kernel, "$kernel::boot", [], vec![]);
package.manifest = PackageManifest {
exports: Default::default(),
modules: Default::default(),
dependencies: Default::default(),
entrypoint: None,
};
let error = package
.to_kernel()
.expect_err("kernel packages without exported procedures should be rejected");
assert!(
error
.to_string()
.contains("invalid kernel package: does not export any kernel procedures")
);
}
fn kernel_dependency(package: &Package) -> Dependency {
Dependency {
name: package.name.clone(),
kind: TargetType::Kernel,
version: package.version.clone(),
digest: package.digest(),
}
}
#[test]
fn embedded_kernel_package_rejects_duplicate_kernel_sections() {
let kernel = build_kernel_package("kernel");
let kernel_bytes = kernel.to_bytes();
let package = build_package(
"app",
TargetType::Library,
"app::entry",
vec![kernel_dependency(&kernel)],
vec![
Section::new(SectionId::KERNEL, kernel_bytes.clone()),
Section::new(SectionId::KERNEL, kernel_bytes),
],
);
let error = package
.try_embedded_kernel_package()
.expect_err("duplicate kernel sections should be rejected");
assert!(error.to_string().contains("multiple 'kernel' sections"));
}
#[test]
fn embedded_kernel_package_rejects_multiple_kernel_runtime_dependencies() {
let kernel_a = build_kernel_package("kernel-a");
let kernel_b = build_kernel_package("kernel-b");
let package = build_package(
"app",
TargetType::Library,
"app::entry",
vec![kernel_dependency(&kernel_a), kernel_dependency(&kernel_b)],
vec![Section::new(SectionId::KERNEL, kernel_a.to_bytes())],
);
let error = package
.try_embedded_kernel_package()
.expect_err("multiple kernel runtime dependencies should be rejected");
assert!(error.to_string().contains("declares multiple kernel runtime dependencies"));
}
#[test]
fn untrusted_embedded_kernel_decode_discards_nested_debug_info() {
let kernel =
build_debug_package("kernel", TargetType::Kernel, "kernel::boot", "kernel_ctx");
assert!(kernel.debug_info().unwrap().is_some());
let package = build_package(
"app",
TargetType::Executable,
"app::entry",
vec![kernel_dependency(&kernel)],
vec![Section::new(SectionId::KERNEL, kernel.to_bytes())],
);
let round_tripped = Package::read_from_bytes(&package.to_bytes())
.expect("untrusted package read should succeed");
let raw_kernel_bytes = round_tripped
.sections
.iter()
.find(|section| section.id == SectionId::KERNEL)
.expect("kernel section should remain available as opaque bytes")
.data
.as_ref();
let trusted_kernel = Package::read_from_bytes_trusted(raw_kernel_bytes)
.expect("trusted direct kernel read should succeed");
assert!(
trusted_kernel.debug_info().unwrap().is_some(),
"opaque kernel bytes may still contain trusted-cache debug metadata"
);
let untrusted_kernel = round_tripped
.try_embedded_kernel_package()
.expect("embedded kernel should decode")
.expect("kernel should be present");
assert!(
!untrusted_kernel.sections.iter().any(|section| section.id.is_debug()),
"untrusted embedded-kernel decode should discard nested debug sections"
);
assert!(untrusted_kernel.debug_info().unwrap().is_none());
}
#[test]
fn strip_debug_info_removes_package_and_embedded_kernel_debug() {
let mut kernel =
build_debug_package("kernel", TargetType::Kernel, "kernel::boot", "kernel_ctx");
kernel.sections = debug_sections();
kernel
.sections
.push(Section::new(SectionId::ACCOUNT_COMPONENT_METADATA, vec![42, 43, 44]));
assert!(kernel.sections.iter().any(|section| section.id.is_debug()));
let mut package =
build_debug_package("app", TargetType::Executable, "app::entry", "app_ctx");
let digest = package.digest();
package.sections = debug_sections();
package
.sections
.push(Section::new(SectionId::ACCOUNT_COMPONENT_METADATA, vec![1, 3, 5]));
package.sections.push(Section::new(SectionId::KERNEL, kernel.to_bytes()));
let content_digest = package.content_digest();
assert!(package.sections.iter().any(|section| section.id.is_debug()));
package.strip_debug_info().expect("strip should succeed");
assert_eq!(package.digest(), digest);
assert_eq!(package.content_digest(), content_digest);
assert!(!package.sections.iter().any(|section| section.id.is_debug()));
assert!(
package
.sections
.iter()
.any(|section| section.id == SectionId::ACCOUNT_COMPONENT_METADATA)
);
let stripped_kernel = package
.embedded_kernel_package()
.unwrap()
.expect("kernel should remain embedded");
assert!(!stripped_kernel.sections.iter().any(|section| section.id.is_debug()));
assert!(
stripped_kernel
.sections
.iter()
.any(|section| section.id == SectionId::ACCOUNT_COMPONENT_METADATA)
);
let raw_kernel_bytes = package
.sections
.iter()
.find(|section| section.id == SectionId::KERNEL)
.expect("kernel section should remain embedded")
.data
.as_ref();
let trusted_stripped_kernel = Package::read_from_bytes_trusted(raw_kernel_bytes)
.expect("trusted stripped kernel read should succeed");
assert!(
!trusted_stripped_kernel.sections.iter().any(|section| section.id.is_debug()),
"stripping should remove nested debug sections from raw kernel bytes"
);
}
#[test]
fn malformed_procedure_lookup_paths_are_not_exported() {
let package = build_package("app", TargetType::Library, "app::entry", [], Vec::new());
let invalid_path = alloc::format!("::{}", "a".repeat(AstPath::MAX_COMPONENT_LENGTH + 1));
let invalid_path = AstPath::new(&invalid_path);
assert_eq!(package.get_procedure_root_by_path(invalid_path), None);
assert_eq!(package.get_procedure_node_by_path(invalid_path), None);
assert!(!package.is_reexport(invalid_path));
}
#[test]
fn procedure_lookup_accepts_relative_and_absolute_export_paths() {
let (forest, node_id) = build_forest();
let digest = forest[node_id].digest();
let path = relative_path("app::entry");
let export =
PackageExport::Procedure(ProcedureExport::new(path, Some(node_id), digest, None));
let package = Package::create(
PackageId::from("app"),
Version::new(1, 0, 0),
TargetType::Library,
Arc::new(forest),
vec![export],
None,
)
.expect("package should be valid");
assert_eq!(package.get_procedure_root_by_path("app::entry"), Some(digest));
assert_eq!(package.get_procedure_root_by_path("::app::entry"), Some(digest));
assert_eq!(package.get_procedure_node_by_path("app::entry"), Some(node_id));
assert_eq!(package.get_procedure_node_by_path("::app::entry"), Some(node_id));
assert_eq!(package.get_export_node_id("::app::entry"), node_id);
assert!(!package.is_reexport("::app::entry"));
}
#[test]
fn make_executable_preserves_selected_same_digest_root_metadata() {
let (mast, exports, sections) = build_same_digest_package_exports(&[
("app::alias_a", "alias_a"),
("app::alias_b", "alias_b"),
]);
let mut package = Package::create(
PackageId::from("app"),
Version::new(1, 0, 0),
TargetType::Library,
mast,
exports,
None,
)
.expect("package should be valid");
package.sections = sections;
let entrypoint = QualifiedProcedureName::from_str("app::alias_b").unwrap();
let executable = package.make_executable(&entrypoint).unwrap();
let main_path = Path::exec_path().join(ProcedureName::MAIN_PROC_NAME);
let entrypoint_node = executable.get_procedure_node_by_path(&main_path).unwrap();
let main_export = executable
.manifest
.get_export(&main_path)
.and_then(PackageExport::as_procedure)
.expect("main export should exist");
let source_node = main_export.source_node.expect("main export should retain source node");
let debug_info = executable
.debug_info()
.expect("debug sections should decode")
.expect("debug sections should be present");
assert_eq!(debug_info.source_node(source_node).unwrap().exec_node, entrypoint_node);
assert_eq!(
debug_info.first_asm_op_for_source_node(source_node).unwrap().context_name,
"alias_b"
);
let program = executable.try_into_program().unwrap();
assert_eq!(program.entrypoint(), entrypoint_node);
}
#[test]
fn make_executable_preserves_debug_section_trust_state() {
let (mast, exports, sections) = build_same_digest_package_exports(&[
("app::alias_a", "alias_a"),
("app::alias_b", "alias_b"),
]);
let mut package = Package::create(
PackageId::from("app"),
Version::new(1, 0, 0),
TargetType::Library,
mast,
exports,
None,
)
.expect("package should be valid");
package.sections = sections;
package.debug_sections_trusted = false;
let executable = package
.make_executable(&QualifiedProcedureName::from_str("app::alias_b").unwrap())
.unwrap();
assert!(!executable.debug_sections_trusted);
assert_matches!(executable.debug_info(), Err(PackageDebugInfoError::UntrustedSections));
}
#[test]
fn make_executable_accepts_relative_entrypoint_export_path() {
let (forest, node_id) = build_forest();
let digest = forest[node_id].digest();
let path = relative_path("app::entry");
let export =
PackageExport::Procedure(ProcedureExport::new(path, Some(node_id), digest, None));
let package = Package::create(
PackageId::from("app"),
Version::new(1, 0, 0),
TargetType::Library,
Arc::new(forest),
[export],
None,
)
.expect("package should be valid");
let entrypoint = QualifiedProcedureName::from_str("app::entry").unwrap();
let executable = package.make_executable(&entrypoint).unwrap();
let main_path = Path::exec_path().join(ProcedureName::MAIN_PROC_NAME);
assert_eq!(executable.get_procedure_root_by_path(&main_path), Some(digest));
assert_eq!(executable.get_procedure_node_by_path(&main_path), Some(node_id));
}
#[test]
fn merge_source_debug_keeps_concrete_metadata_distinct_from_external_placeholder() {
fn debug_info_for_root(root: MastNodeId, context: &str) -> PackageDebugInfo {
let source_node = DebugSourceNodeId::from(0);
PackageDebugInfo {
source_graph: Some(DebugSourceGraphSection::from_parts(
vec![DebugSourceNode::new(root, vec![], 0, 1)],
vec![source_node],
)),
source_map: Some(DebugSourceMapSection::from_parts(
vec![DebugSourceAsmOp::new(
source_node,
0,
None,
context.into(),
"add".into(),
1,
)],
Vec::new(),
)),
..PackageDebugInfo::default()
}
}
let mut concrete_forest = MastForest::new();
let concrete_root = BasicBlockNodeBuilder::new(vec![Operation::Add])
.add_to_forest(&mut concrete_forest)
.unwrap();
concrete_forest.make_root(concrete_root);
let concrete_digest = concrete_forest[concrete_root].digest();
let mut placeholder_forest = MastForest::new();
let placeholder_root = ExternalNodeBuilder::new(concrete_digest)
.add_to_forest(&mut placeholder_forest)
.unwrap();
placeholder_forest.make_root(placeholder_root);
let placeholder_debug = debug_info_for_root(placeholder_root, "placeholder");
let concrete_debug = debug_info_for_root(concrete_root, "concrete");
let (_merged_forest, root_map) =
MastForest::merge([&placeholder_forest, &concrete_forest]).unwrap();
let merged_placeholder = root_map.map_root(0, &placeholder_root).unwrap();
let merged_concrete = root_map.map_root(1, &concrete_root).unwrap();
assert_eq!(merged_placeholder, merged_concrete);
let merged_debug = PackageDebugInfo::merge_source_debug(
[(0, &placeholder_debug), (1, &concrete_debug)],
&root_map,
)
.unwrap();
let source_graph = merged_debug.source_graph.as_ref().unwrap();
assert_eq!(source_graph.nodes().len(), 2);
assert!(source_graph.nodes().iter().all(|node| node.exec_node == merged_concrete));
let placeholder_source = source_graph.roots()[0];
let concrete_source = source_graph.roots()[1];
assert_ne!(placeholder_source, concrete_source);
assert_eq!(
merged_debug
.first_asm_op_for_source_node(placeholder_source)
.unwrap()
.context_name,
"placeholder",
);
assert_eq!(
merged_debug.first_asm_op_for_source_node(concrete_source).unwrap().context_name,
"concrete",
);
}
#[test]
fn make_executable_same_digest_selection_is_export_order_independent() {
fn selected_context_for_alias_b(exports: &[(&str, &str)]) -> String {
let (mast, exports, sections) = build_same_digest_package_exports(exports);
let mut package = Package::create(
PackageId::from("app"),
Version::new(1, 0, 0),
TargetType::Library,
mast,
exports,
None,
)
.expect("package should be valid");
package.sections = sections;
let executable = package
.make_executable(&QualifiedProcedureName::from_str("app::alias_b").unwrap())
.unwrap();
let main_path = Path::exec_path().join(ProcedureName::MAIN_PROC_NAME);
let main_export = executable
.manifest
.get_export(&main_path)
.and_then(PackageExport::as_procedure)
.expect("main export should exist");
let source_node =
main_export.source_node.expect("main export should retain source node");
let debug_info = executable
.debug_info()
.expect("debug sections should decode")
.expect("debug sections should be present");
debug_info
.first_asm_op_for_source_node(source_node)
.unwrap()
.context_name
.clone()
}
assert_eq!(
selected_context_for_alias_b(&[
("app::alias_a", "alias_a"),
("app::alias_b", "alias_b")
]),
"alias_b",
);
assert_eq!(
selected_context_for_alias_b(&[
("app::alias_b", "alias_b"),
("app::alias_a", "alias_a")
]),
"alias_b",
);
}
}