hugr_core/package.rs
1//! Bundles of hugr modules along with the extension required to load them.
2
3use std::io;
4
5use crate::envelope::{EnvelopeConfig, EnvelopeError, read_envelope, write_envelope};
6use crate::extension::ExtensionRegistry;
7use crate::hugr::{HugrView, ValidationError};
8use crate::std_extensions::STD_REG;
9use crate::{Hugr, Node};
10use thiserror::Error;
11
12#[derive(Debug, Default, Clone, PartialEq)]
13/// Package of module HUGRs.
14pub struct Package {
15 /// Module HUGRs included in the package.
16 pub modules: Vec<Hugr>,
17 /// Extensions used in the modules.
18 ///
19 /// This is a superset of the extensions used in the modules.
20 pub extensions: ExtensionRegistry,
21}
22
23impl Package {
24 /// Create a new package from a list of hugrs.
25 ///
26 /// The hugr extensions are not automatically added to the package. Make
27 /// sure to manually include any non-standard extensions to
28 /// [`Package::extensions`].
29 pub fn new(modules: impl IntoIterator<Item = Hugr>) -> Self {
30 let modules: Vec<Hugr> = modules.into_iter().collect();
31 Self {
32 modules,
33 extensions: ExtensionRegistry::default(),
34 }
35 }
36
37 /// Create a new package containing a single HUGR.
38 ///
39 /// The hugr extensions are not automatically added to the package. Make
40 /// sure to manually include any non-standard extensions to
41 /// [`Package::extensions`].
42 pub fn from_hugr(hugr: Hugr) -> Self {
43 Package {
44 extensions: ExtensionRegistry::default(),
45 modules: vec![hugr],
46 }
47 }
48
49 /// Validate the modules of the package.
50 ///
51 /// Ensures that the top-level extension list is a superset of the extensions used in the modules.
52 pub fn validate(&self) -> Result<(), PackageValidationError> {
53 for hugr in &self.modules {
54 hugr.validate()?;
55 }
56 Ok(())
57 }
58
59 /// Read a Package from a HUGR envelope.
60 ///
61 /// To load a Package, all the extensions used in its definition must be
62 /// available. The Envelope may include some of the extensions, but any
63 /// additional extensions must be provided in the `extensions` parameter. If
64 /// `extensions` is `None`, the default [`crate::std_extensions::STD_REG`]
65 /// is used.
66 pub fn load(
67 reader: impl io::BufRead,
68 extensions: Option<&ExtensionRegistry>,
69 ) -> Result<Self, EnvelopeError> {
70 let extensions = extensions.unwrap_or(&STD_REG);
71 let (_, pkg) = read_envelope(reader, extensions)?;
72 Ok(pkg)
73 }
74
75 /// Read a Package from a HUGR envelope encoded in a string.
76 ///
77 /// Note that not all envelopes are valid strings. In the general case,
78 /// it is recommended to use `Package::load` with a bytearray instead.
79 ///
80 /// To load a Package, all the extensions used in its definition must be
81 /// available. The Envelope may include some of the extensions, but any
82 /// additional extensions must be provided in the `extensions` parameter. If
83 /// `extensions` is `None`, the default [`crate::std_extensions::STD_REG`]
84 /// is used.
85 pub fn load_str(
86 envelope: impl AsRef<str>,
87 extensions: Option<&ExtensionRegistry>,
88 ) -> Result<Self, EnvelopeError> {
89 Self::load(envelope.as_ref().as_bytes(), extensions)
90 }
91
92 /// Store the Package in a HUGR envelope.
93 ///
94 /// The Envelope will embed the definitions of the extensions in
95 /// [`Package::extensions`]. Any other extension used in the definition must
96 /// be passed to [`Package::load`] to load back the package.
97 pub fn store(
98 &self,
99 writer: impl io::Write,
100 config: EnvelopeConfig,
101 ) -> Result<(), EnvelopeError> {
102 write_envelope(writer, self, config)
103 }
104
105 /// Store the Package in a HUGR envelope encoded in a string.
106 ///
107 /// Note that not all envelopes are valid strings. In the general case,
108 /// it is recommended to use `Package::store` with a bytearray instead.
109 /// See [`EnvelopeFormat::ascii_printable`][crate::envelope::EnvelopeFormat::ascii_printable].
110 ///
111 /// The Envelope will embed the definitions of the extensions in
112 /// [`Package::extensions`]. Any other extension used in the definition must
113 /// be passed to [`Package::load_str`] to load back the package.
114 pub fn store_str(&self, config: EnvelopeConfig) -> Result<String, EnvelopeError> {
115 if !config.format.ascii_printable() {
116 return Err(EnvelopeError::NonASCIIFormat {
117 format: config.format,
118 });
119 }
120
121 let mut buf = Vec::new();
122 self.store(&mut buf, config)?;
123 Ok(String::from_utf8(buf).expect("Envelope is valid utf8"))
124 }
125}
126
127impl AsRef<[Hugr]> for Package {
128 fn as_ref(&self) -> &[Hugr] {
129 &self.modules
130 }
131}
132
133/// Error raised while validating a package.
134#[derive(Debug, Error)]
135#[non_exhaustive]
136#[error("Package validation error.")]
137pub enum PackageValidationError {
138 /// Error raised while validating the package hugrs.
139 Validation(#[from] ValidationError<Node>),
140}
141
142#[cfg(test)]
143mod test {
144 use super::*;
145 use crate::builder::test::{
146 simple_cfg_hugr, simple_dfg_hugr, simple_funcdef_hugr, simple_module_hugr,
147 };
148 use rstest::rstest;
149
150 #[rstest]
151 #[case::module("module", simple_module_hugr())]
152 #[case::funcdef("funcdef", simple_funcdef_hugr())]
153 #[case::dfg("dfg", simple_dfg_hugr())]
154 #[case::cfg("cfg", simple_cfg_hugr())]
155 #[cfg_attr(miri, ignore)] // Opening files is not supported in (isolated) miri
156 fn hugr_to_package(#[case] test_name: &str, #[case] hugr: Hugr) {
157 let package = &Package::from_hugr(hugr.clone());
158 assert_eq!(package.modules.len(), 1);
159
160 assert_eq!(
161 package.modules[0].entrypoint_optype(),
162 hugr.entrypoint_optype()
163 );
164
165 insta::assert_snapshot!(test_name, hugr.mermaid_string());
166 }
167
168 #[rstest]
169 fn package_properties() {
170 let module = simple_module_hugr();
171 let dfg = simple_dfg_hugr();
172
173 let pkg = Package::new([module, dfg]);
174 pkg.validate().unwrap();
175
176 assert_eq!(pkg.modules.len(), 2);
177 }
178}