Skip to content

Commit 107cccd

Browse files
aborgna-qss2165
andauthored
feat!: Only expose envelope serialization of hugrs and packages (#2167)
Closes #2159 Now `Hugr`s expose `load`/`load_str`/`store`/`store_str` methods instead of implementing `serde` traits, mirroring `Package`s. This uses envelopes interternally, so we no longer support row hugr jsons. `hugr-cli` keeps the backwards compatibility for the moment, by simply wrapping the jsons with the appropriate header when using the `--hugr-json` flag. For users that need it, we define a [`serde_with`](https://docs.rs/serde_with/3.12.0/serde_with/index.html) helper to be able to include hugrs and packages inside serde-encoded structs. The implementation serializes the hugr into an envelope, and includes it as a string/bytes value. For backwards compatibility, we temporarily also try to read Hugr JSONs as a fallback. ```rust use serde::{Deserialize, Serialize}; use serde_json::json; use serde_with::{serde_as}; use hugr_core::Hugr; use hugr_core::package::Package; use hugr_core::envelope::serde_with::AsStringEnvelope; #[serde_as] #[derive(Deserialize, Serialize)] struct A { #[serde_as(as = "AsStringEnvelope")] package: Package, #[serde_as(as = "Vec<AsStringEnvelope>")] hugrs: Vec<Hugr>, } ``` BREAKING CHANGE: `Hugr` and `Package` no longer implement serde traits. Use envelopes instead. See `AsStringEnvelope` for backwards compatibility. --------- Co-authored-by: Seyon Sivarajah <[email protected]>
1 parent 9d91c6e commit 107cccd

37 files changed

+1143
-621
lines changed

Cargo.lock

+258-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ rstest = "0.24.0"
6060
semver = "1.0.26"
6161
serde = "1.0.219"
6262
serde_json = "1.0.140"
63+
serde_with = "3.12.0"
6364
serde_yaml = "0.9.34"
6465
smol_str = "0.3.1"
6566
static_assertions = "1.1.0"

hugr-cli/src/hugr_io.rs

+15-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use hugr::envelope::{read_envelope, EnvelopeError};
55
use hugr::extension::ExtensionRegistry;
66
use hugr::package::Package;
77
use hugr::{Extension, Hugr};
8-
use std::io::BufReader;
8+
use std::io::{BufReader, Read};
99
use std::path::PathBuf;
1010

1111
use crate::CliError;
@@ -57,12 +57,23 @@ impl HugrInputArgs {
5757

5858
/// Read a hugr JSON file from the input.
5959
///
60-
/// If [HugrInputArgs::hugr_json] is `false`, [HugrInputArgs::get_package] should be called instead as
61-
/// reading an input envelope as a HUGR json will fail.
60+
/// This is a legacy option for reading old HUGR JSON files when the
61+
/// [HugrInputArgs::hugr_json] flag is used.
62+
///
63+
/// For most cases, [HugrInputArgs::get_package] should be called instead.
6264
pub fn get_hugr(&mut self) -> Result<Hugr, CliError> {
6365
let extensions = self.load_extensions()?;
6466
let mut buffer = BufReader::new(&mut self.input);
65-
let hugr = Hugr::load_json(&mut buffer, &extensions)?;
67+
68+
/// Wraps the hugr JSON so that it defines a valid envelope.
69+
const PREPEND: &str = r#"HUGRiHJv?@{"modules": ["#;
70+
const APPEND: &str = r#"],"extensions": []}"#;
71+
72+
let mut envelope = PREPEND.to_string();
73+
buffer.read_to_string(&mut envelope)?;
74+
envelope.push_str(APPEND);
75+
76+
let hugr = Hugr::load_str(envelope, Some(&extensions))?;
6677
Ok(hugr)
6778
}
6879

hugr-cli/src/lib.rs

+1-8
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,7 @@ use clap::{crate_version, Parser};
6060
use clap_verbosity_flag::log::Level;
6161
use clap_verbosity_flag::{InfoLevel, Verbosity};
6262
use hugr::envelope::EnvelopeError;
63-
use hugr::hugr::LoadHugrError;
64-
use hugr::package::{PackageEncodingError, PackageValidationError};
63+
use hugr::package::PackageValidationError;
6564
use std::ffi::OsString;
6665

6766
pub mod extensions;
@@ -97,12 +96,6 @@ pub enum CliError {
9796
/// Error parsing input.
9897
#[display("Error parsing package: {_0}")]
9998
Parse(serde_json::Error),
100-
/// Package load error.
101-
#[display("Error parsing package: {_0}")]
102-
PackageLoad(PackageEncodingError),
103-
/// Hugr load error.
104-
#[display("Error loading hugr: {_0}")]
105-
HugrLoad(LoadHugrError),
10699
#[display("Error validating HUGR: {_0}")]
107100
/// Errors produced by the `validate` subcommand.
108101
Validate(PackageValidationError),

hugr-cli/tests/validate.rs

+3-5
Original file line numberDiff line numberDiff line change
@@ -110,30 +110,28 @@ fn bad_hugr_string() -> String {
110110
let df = DFGBuilder::new(Signature::new_endo(vec![qb_t()])).unwrap();
111111
let bad_hugr = df.hugr().clone();
112112

113-
serde_json::to_string(&bad_hugr).unwrap()
113+
bad_hugr.store_str(EnvelopeConfig::text()).unwrap()
114114
}
115115

116116
#[rstest]
117117
fn test_mermaid_invalid(bad_hugr_string: String, mut cmd: Command) {
118118
cmd.arg("mermaid");
119119
cmd.arg("--validate");
120-
cmd.arg("--hugr-json");
121120
cmd.write_stdin(bad_hugr_string);
122121
cmd.assert()
123122
.failure()
124-
.stderr(contains("Error loading hugr"));
123+
.stderr(contains("Error validating HUGR"));
125124
}
126125

127126
#[rstest]
128127
fn test_bad_hugr(bad_hugr_string: String, mut val_cmd: Command) {
129128
val_cmd.write_stdin(bad_hugr_string);
130-
val_cmd.arg("--hugr-json");
131129
val_cmd.arg("-");
132130

133131
val_cmd
134132
.assert()
135133
.failure()
136-
.stderr(contains("Error loading hugr"));
134+
.stderr(contains("Error validating HUGR"));
137135
}
138136

139137
#[rstest]

hugr-core/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ regex = { workspace = true }
5151
# Rc used here for Extension, but unfortunately we must turn the feature on globally
5252
serde = { workspace = true, features = ["derive", "rc"] }
5353
serde_json = { workspace = true }
54+
serde_with = { workspace = true }
5455
serde_yaml = { workspace = true, optional = true }
5556
smol_str = { workspace = true, features = ["serde"] }
5657
static_assertions = { workspace = true }

hugr-core/src/envelope.rs

+43-16
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,14 @@
4141
// https://github.com/JelteF/derive_more/pull/454.
4242

4343
mod header;
44+
mod package_json;
45+
pub mod serde_with;
4446

4547
pub use header::{EnvelopeConfig, EnvelopeFormat, ZstdConfig, MAGIC_NUMBERS};
48+
pub use package_json::PackageEncodingError;
4649

47-
use crate::{
48-
extension::ExtensionRegistry,
49-
package::{Package, PackageEncodingError},
50-
};
50+
use crate::Hugr;
51+
use crate::{extension::ExtensionRegistry, package::Package};
5152
use header::EnvelopeHeader;
5253
use std::io::BufRead;
5354
use std::io::Write;
@@ -90,9 +91,22 @@ pub fn read_envelope(
9091
/// It is recommended to use a buffered writer for better performance.
9192
/// See [`std::io::BufWriter`] for more information.
9293
pub fn write_envelope(
93-
mut writer: impl Write,
94+
writer: impl Write,
9495
package: &Package,
9596
config: EnvelopeConfig,
97+
) -> Result<(), EnvelopeError> {
98+
write_envelope_impl(writer, &package.modules, &package.extensions, config)
99+
}
100+
101+
/// Write a deconstructed HUGR package into an envelope, using the specified configuration.
102+
///
103+
/// It is recommended to use a buffered writer for better performance.
104+
/// See [`std::io::BufWriter`] for more information.
105+
pub(crate) fn write_envelope_impl<'h>(
106+
mut writer: impl Write,
107+
hugrs: impl IntoIterator<Item = &'h Hugr>,
108+
extensions: &ExtensionRegistry,
109+
config: EnvelopeConfig,
96110
) -> Result<(), EnvelopeError> {
97111
let header = config.make_header();
98112
header.write(&mut writer)?;
@@ -101,11 +115,11 @@ pub fn write_envelope(
101115
#[cfg(feature = "zstd")]
102116
Some(zstd) => {
103117
let writer = zstd::Encoder::new(writer, zstd.level())?.auto_finish();
104-
write_impl(writer, package, config)?;
118+
write_impl(writer, hugrs, extensions, config)?;
105119
}
106120
#[cfg(not(feature = "zstd"))]
107121
Some(_) => return Err(EnvelopeError::ZstdUnsupported),
108-
None => write_impl(writer, package, config)?,
122+
None => write_impl(writer, hugrs, extensions, config)?,
109123
}
110124

111125
Ok(())
@@ -164,6 +178,17 @@ pub enum EnvelopeError {
164178
#[display("Zstd compression is not supported. This requires the 'zstd' feature for `hugr`.")]
165179
#[from(ignore)]
166180
ZstdUnsupported,
181+
/// Expected the envelope to contain a single HUGR.
182+
#[display("Expected an envelope containing a single hugr, but it contained {}.", if *count == 0 {
183+
"none".to_string()
184+
} else {
185+
count.to_string()
186+
})]
187+
#[from(ignore)]
188+
ExpectedSingleHugr {
189+
/// The number of HUGRs in the package.
190+
count: usize,
191+
},
167192
/// JSON serialization error.
168193
SerdeError {
169194
/// The source error.
@@ -204,7 +229,7 @@ fn read_impl(
204229
) -> Result<Package, EnvelopeError> {
205230
match header.format {
206231
#[allow(deprecated)]
207-
EnvelopeFormat::PackageJson => Ok(Package::from_json_reader(payload, registry)?),
232+
EnvelopeFormat::PackageJson => Ok(package_json::from_json_reader(payload, registry)?),
208233
EnvelopeFormat::Model | EnvelopeFormat::ModelWithExtensions => {
209234
decode_model(payload, registry, header.format)
210235
}
@@ -249,24 +274,26 @@ fn decode_model(
249274
}
250275

251276
/// Internal implementation of [`write_envelope`] to call with/without the zstd compression wrapper.
252-
fn write_impl(
277+
fn write_impl<'h>(
253278
writer: impl Write,
254-
package: &Package,
279+
hugrs: impl IntoIterator<Item = &'h Hugr>,
280+
extensions: &ExtensionRegistry,
255281
config: EnvelopeConfig,
256282
) -> Result<(), EnvelopeError> {
257283
match config.format {
258284
#[allow(deprecated)]
259-
EnvelopeFormat::PackageJson => package.to_json_writer(writer)?,
285+
EnvelopeFormat::PackageJson => package_json::to_json_writer(hugrs, extensions, writer)?,
260286
EnvelopeFormat::Model | EnvelopeFormat::ModelWithExtensions => {
261-
encode_model(writer, package, config.format)?
287+
encode_model(writer, hugrs, extensions, config.format)?
262288
}
263289
}
264290
Ok(())
265291
}
266292

267-
fn encode_model(
293+
fn encode_model<'h>(
268294
mut writer: impl Write,
269-
package: &Package,
295+
hugrs: impl IntoIterator<Item = &'h Hugr>,
296+
extensions: &ExtensionRegistry,
270297
format: EnvelopeFormat,
271298
) -> Result<(), EnvelopeError> {
272299
use hugr_model::v0::{binary::write_to_writer, bumpalo::Bump};
@@ -281,11 +308,11 @@ fn encode_model(
281308
}
282309

283310
let bump = Bump::default();
284-
let model_package = export_package(package, &bump);
311+
let model_package = export_package(hugrs, extensions, &bump);
285312
write_to_writer(&model_package, &mut writer)?;
286313

287314
if format.append_extensions() {
288-
serde_json::to_writer(writer, &package.extensions.iter().collect_vec())?;
315+
serde_json::to_writer(writer, &extensions.iter().collect_vec())?;
289316
}
290317

291318
Ok(())
+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
//! Encoding / decoding of Package json, used in the `PackageJson` envelope format.
2+
use derive_more::{Display, Error, From};
3+
use itertools::Itertools;
4+
use std::io;
5+
6+
use crate::extension::resolution::ExtensionResolutionError;
7+
use crate::extension::{ExtensionRegistry, PRELUDE_REGISTRY};
8+
use crate::hugr::ExtensionError;
9+
use crate::package::Package;
10+
use crate::{Extension, Hugr, HugrView};
11+
12+
/// Read a Package in json format from an io reader.
13+
pub(super) fn from_json_reader(
14+
reader: impl io::Read,
15+
extension_registry: &ExtensionRegistry,
16+
) -> Result<Package, PackageEncodingError> {
17+
let val: serde_json::Value = serde_json::from_reader(reader)?;
18+
19+
let PackageDeser {
20+
modules,
21+
extensions: pkg_extensions,
22+
} = serde_json::from_value::<PackageDeser>(val.clone())?;
23+
let mut modules = modules.into_iter().map(|h| h.0).collect_vec();
24+
25+
// TODO: We don't currently store transitive extension dependencies in the
26+
// package's extensions. For example, if we use a `collections.list` const
27+
// value but don't use anything in `prelude` we would not include `prelude` in
28+
// the package's extensions. But this would then fail when loading the
29+
// extensions, as we _need_ the prelude to load the `collections.list` op
30+
// definitions here.
31+
//
32+
// The current fix is to always include the prelude when decoding, but this
33+
// only works for transitive `prelude` dependencies.
34+
//
35+
// Chains of custom extensions will cause this to fail.
36+
let extension_registry = if PRELUDE_REGISTRY
37+
.iter()
38+
.any(|e| !extension_registry.contains(&e.name))
39+
{
40+
let mut reg_with_prelude = extension_registry.clone();
41+
reg_with_prelude.extend(PRELUDE_REGISTRY.iter().cloned());
42+
reg_with_prelude
43+
} else {
44+
extension_registry.clone()
45+
};
46+
47+
let mut pkg_extensions = ExtensionRegistry::new_with_extension_resolution(
48+
pkg_extensions,
49+
&(&extension_registry).into(),
50+
)?;
51+
52+
// Resolve the operations in the modules using the defined registries.
53+
let mut combined_registry = extension_registry.clone();
54+
combined_registry.extend(&pkg_extensions);
55+
56+
for module in &mut modules {
57+
module.resolve_extension_defs(&combined_registry)?;
58+
pkg_extensions.extend(module.extensions());
59+
}
60+
61+
Ok(Package {
62+
modules,
63+
extensions: pkg_extensions,
64+
})
65+
}
66+
67+
/// Write the Package in json format into an io writer.
68+
pub(super) fn to_json_writer<'h>(
69+
hugrs: impl IntoIterator<Item = &'h Hugr>,
70+
extensions: &ExtensionRegistry,
71+
writer: impl io::Write,
72+
) -> Result<(), PackageEncodingError> {
73+
let pkg_ser = PackageSer {
74+
modules: hugrs.into_iter().map(HugrSer).collect(),
75+
extensions: extensions.iter().map(|e| e.as_ref()).collect(),
76+
};
77+
serde_json::to_writer(writer, &pkg_ser)?;
78+
Ok(())
79+
}
80+
81+
/// Error raised while loading a package.
82+
#[derive(Debug, Display, Error, From)]
83+
#[non_exhaustive]
84+
pub enum PackageEncodingError {
85+
/// Error raised while parsing the package json.
86+
JsonEncoding(serde_json::Error),
87+
/// Error raised while reading from a file.
88+
IOError(io::Error),
89+
/// Could not resolve the extension needed to encode the hugr.
90+
ExtensionResolution(ExtensionResolutionError),
91+
/// Could not resolve the runtime extensions for the hugr.
92+
RuntimeExtensionResolution(ExtensionError),
93+
}
94+
95+
/// A private package structure implementing the serde traits.
96+
///
97+
/// We use this to avoid exposing a public implementation of Serialize/Deserialize,
98+
/// as the json definition is not stable, and should always be wrapped in an Envelope.
99+
#[derive(Debug, serde::Serialize)]
100+
struct PackageSer<'h> {
101+
pub modules: Vec<HugrSer<'h>>,
102+
pub extensions: Vec<&'h Extension>,
103+
}
104+
#[derive(Debug, serde::Serialize)]
105+
#[serde(transparent)]
106+
struct HugrSer<'h>(#[serde(serialize_with = "Hugr::serde_serialize")] pub &'h Hugr);
107+
108+
/// A private package structure implementing the serde traits.
109+
///
110+
/// We use this to avoid exposing a public implementation of Serialize/Deserialize,
111+
/// as the json definition is not stable, and should always be wrapped in an Envelope.
112+
#[derive(Debug, serde::Deserialize)]
113+
struct PackageDeser {
114+
pub modules: Vec<HugrDeser>,
115+
pub extensions: Vec<Extension>,
116+
}
117+
#[derive(Debug, serde::Deserialize)]
118+
#[serde(transparent)]
119+
struct HugrDeser(#[serde(deserialize_with = "Hugr::serde_deserialize")] pub Hugr);

0 commit comments

Comments
 (0)