use alloc::{string::String, sync::Arc};
use core::fmt;
use miden_assembly_syntax::{
Report,
diagnostics::{Diagnostic, miette},
};
use miden_core::serde::DeserializationError;
use miden_debug_types::Uri;
use miden_mast_package::{Package, PackageDebugInfoError};
const PACKAGE_MAGIC: &[u8; 5] = b"MASP\0";
const TOOLCHAIN_HELP: &str = "package, MAST, and debug-info formats are tied to the Miden \
toolchain that produced them; rerun with that toolchain, for \
example `miden +0.16.0 debug <FILE>`";
#[derive(Debug, thiserror::Error, Diagnostic)]
enum PackageLoadError {
#[error("failed to load Miden package from {input}")]
DecodePackage {
input: Uri,
#[source]
cause: DeserializationError,
},
#[error("Miden package from {input} uses unsupported package format {version}")]
#[diagnostic(code(miden_debug::incompatible_package_format))]
IncompatiblePackageFormat {
input: Uri,
version: PackageFormatVersion,
#[source]
cause: DeserializationError,
#[help]
help: String,
},
#[error("failed to load debug information from Miden package {input}")]
DecodeDebugInfo {
input: Uri,
#[source]
cause: PackageDebugInfoError,
},
#[error("Miden package from {input} uses an unsupported debug-info format")]
#[diagnostic(code(miden_debug::incompatible_debug_info_format))]
IncompatibleDebugInfoFormat {
input: Uri,
#[source]
cause: PackageDebugInfoError,
#[help]
help: String,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct PackageFormatVersion([u8; 3]);
impl fmt::Display for PackageFormatVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}.{}.{}", self.0[0], self.0[1], self.0[2])
}
}
#[doc(hidden)]
#[cfg(feature = "std")]
pub fn load_package_from_path(path: &std::path::Path) -> Result<Arc<Package>, Report> {
use miden_assembly_syntax::diagnostics::IntoDiagnostic;
let bytes = std::fs::read(path).into_diagnostic()?;
let uri = Uri::from(path);
read_package_from_bytes(&bytes, &uri)
}
#[doc(hidden)]
pub fn read_package_from_bytes(bytes: &[u8], input: &Uri) -> Result<Arc<Package>, Report> {
let package = Package::read_from_bytes_trusted(bytes).map_err(|cause| {
if let Some(version) = incompatible_package_format(bytes, &cause) {
Report::new(PackageLoadError::IncompatiblePackageFormat {
input: input.clone(),
version,
cause,
help: TOOLCHAIN_HELP.into(),
})
} else {
Report::new(PackageLoadError::DecodePackage {
input: input.clone(),
cause,
})
}
})?;
if let Err(cause) = package.debug_info() {
let error = if is_incompatible_debug_info(&cause) {
PackageLoadError::IncompatibleDebugInfoFormat {
input: input.clone(),
cause,
help: TOOLCHAIN_HELP.into(),
}
} else {
PackageLoadError::DecodeDebugInfo {
input: input.clone(),
cause,
}
};
return Err(Report::new(error));
}
Ok(Arc::new(package))
}
fn incompatible_package_format(
bytes: &[u8],
error: &DeserializationError,
) -> Option<PackageFormatVersion> {
let DeserializationError::InvalidValue(message) = error else {
return None;
};
if !message.starts_with("unsupported version.") {
return None;
}
let version = bytes.strip_prefix(PACKAGE_MAGIC)?.get(..3)?.try_into().ok()?;
Some(PackageFormatVersion(version))
}
fn is_incompatible_debug_info(error: &PackageDebugInfoError) -> bool {
let PackageDebugInfoError::DecodeSection { source, .. } = error else {
return false;
};
matches!(
source,
DeserializationError::InvalidValue(message)
if message.starts_with("unsupported debug_info version:")
)
}
#[cfg(test)]
mod tests {
use miden_assembly::{Assembler, DefaultSourceManager};
use miden_core::serde::Serializable;
use miden_mast_package::SectionId;
use super::*;
#[test]
fn current_package_with_debug_info_loads() {
let package = Assembler::new(Arc::new(DefaultSourceManager::default()))
.assemble_program("test", "begin push.1 drop end")
.unwrap();
let uri = Uri::new("current.masp");
let restored = read_package_from_bytes(&package.to_bytes(), &uri).unwrap();
assert_eq!(restored.to_bytes(), package.to_bytes());
assert!(restored.debug_info().unwrap().is_some());
}
#[test]
fn incompatible_package_format_recommends_matching_toolchain() {
let bytes = b"MASP\0\x06\x00\x00";
let uri = Uri::new("old.masp");
let report = read_package_from_bytes(bytes, &uri).unwrap_err();
let error = report
.downcast_ref::<PackageLoadError>()
.expect("expected a classified package load error");
let PackageLoadError::IncompatiblePackageFormat { version, help, .. } = error else {
panic!("expected an incompatible package format error, got {error:?}");
};
assert_eq!(*version, PackageFormatVersion([6, 0, 0]));
assert!(help.contains("miden +0.16.0 debug"));
}
#[test]
fn incompatible_debug_info_recommends_matching_toolchain() {
let mut package = Assembler::new(Arc::new(DefaultSourceManager::default()))
.assemble_program("test", "begin push.1 drop end")
.unwrap();
let debug_info = package
.sections
.iter_mut()
.find(|section| section.id == SectionId::DEBUG_INFO)
.expect("assembled package should contain debug info");
debug_info.data.to_mut()[0] = u8::MAX;
let uri = Uri::new("old-debug.masp");
let report = read_package_from_bytes(&package.to_bytes(), &uri).unwrap_err();
let error = report
.downcast_ref::<PackageLoadError>()
.expect("expected a classified package load error");
let PackageLoadError::IncompatibleDebugInfoFormat { help, .. } = error else {
panic!("expected an incompatible debug-info format error, got {error:?}");
};
assert!(help.contains("miden +0.16.0 debug"));
}
#[test]
fn malformed_package_does_not_get_a_version_hint() {
let uri = Uri::new("broken.masp");
let report = read_package_from_bytes(b"not a package", &uri).unwrap_err();
let error = report
.downcast_ref::<PackageLoadError>()
.expect("expected a classified package load error");
assert!(matches!(error, PackageLoadError::DecodePackage { .. }));
}
}