1pub mod description;
38mod header;
39mod package_json;
40mod reader;
41use reader::EnvelopeReader;
42pub use reader::PayloadError;
43
44pub mod serde_with;
45
46pub use header::{EnvelopeConfig, EnvelopeFormat, EnvelopeHeader, MAGIC_NUMBERS, ZstdConfig};
47pub use package_json::PackageEncodingError;
48
49use crate::envelope::description::PackageDesc;
50use crate::envelope::header::HeaderError;
51use crate::extension::resolution::ExtensionResolutionError;
52use crate::{Hugr, HugrView};
53use crate::{
54 extension::{ExtensionRegistry, Version},
55 package::Package,
56};
57use std::io::BufRead;
58use std::io::Write;
59use thiserror::Error;
60
61#[allow(unused_imports)]
62use itertools::Itertools as _;
63
64use crate::import::ImportError;
65
66pub const GENERATOR_KEY: &str = "core.generator";
71pub const USED_EXTENSIONS_KEY: &str = "core.used_extensions";
73
74#[deprecated(since = "0.24.1", note = "Use PackageDesc::generator instead")]
80pub fn get_generator<H: HugrView>(modules: &[H]) -> Option<String> {
81 let mut desc: PackageDesc = PackageDesc::default();
82 desc.set_n_modules(modules.len());
83
84 for (module, d) in modules.iter().zip(desc.modules.iter_mut()) {
85 let d = d.get_or_insert_default();
86 d.load_generator(module);
87 }
88
89 desc.generator()
90}
91
92pub fn format_generator(json_val: &serde_json::Value) -> String {
94 match json_val {
95 serde_json::Value::String(s) => s.clone(),
96 serde_json::Value::Object(obj) => {
97 if let (Some(name), version) = (
98 obj.get("name").and_then(|v| v.as_str()),
99 obj.get("version").and_then(|v| v.as_str()),
100 ) {
101 if let Some(version) = version {
102 format!("{name}-v{version}")
104 } else {
105 name.to_string()
106 }
107 } else {
108 json_val.to_string()
110 }
111 }
112 _ => json_val.to_string(),
114 }
115}
116fn gen_str(generator: Option<&str>) -> String {
117 match generator {
118 Some(g) => format!("\ngenerated by {g}"),
119 None => String::new(),
120 }
121}
122
123#[derive(Error, Debug)]
125#[deprecated(since = "0.24.1", note = "Use PackageDesc instead")]
126#[error("{inner}{}", gen_str(self.generator().map(|s| s.as_str())))]
127pub struct WithGenerator<E: std::fmt::Display> {
128 inner: Box<E>,
129 generator: Option<String>,
131}
132
133#[expect(deprecated)]
134impl<E: std::fmt::Display> WithGenerator<E> {
135 pub fn inner(&self) -> &E {
137 &self.inner
138 }
139
140 pub fn generator(&self) -> Option<&String> {
142 self.generator.as_ref()
143 }
144}
145
146#[deprecated(since = "0.24.1", note = "Use read_described_envelope instead")]
155pub fn read_envelope(
156 reader: impl BufRead,
157 registry: &ExtensionRegistry,
158) -> Result<(EnvelopeConfig, Package), EnvelopeError> {
159 let reader = EnvelopeReader::new(reader, registry)?;
160 let config = reader.description().header.config();
161 let package = reader.read().1?;
162
163 Ok((config, package))
164}
165
166pub fn read_described_envelope(
182 reader: impl BufRead,
183 registry: &ExtensionRegistry,
184) -> Result<(PackageDesc, Package), ReadError> {
185 let reader = EnvelopeReader::new(reader, registry).map_err(Box::new)?;
186 let (desc, res) = reader.read();
187 match res {
188 Ok(pkg) => Ok((desc, pkg)),
189 Err(e) => Err(ReadError::Payload {
190 source: Box::new(e),
191 partial_description: desc,
192 }),
193 }
194}
195
196#[derive(Debug, Error)]
198pub enum ReadError {
199 #[error(transparent)]
201 EnvelopeHeader(#[from] Box<HeaderError>),
202 #[error("Error reading package payload in envelope.")]
204 Payload {
205 source: Box<PayloadError>,
207 partial_description: PackageDesc,
209 },
210}
211
212impl From<ReadError> for EnvelopeError {
213 fn from(err: ReadError) -> Self {
214 match err {
215 ReadError::EnvelopeHeader(e) => (*e).into(),
216 ReadError::Payload { source, .. } => (*source).into(),
217 }
218 }
219}
220
221pub fn write_envelope(
226 writer: impl Write,
227 package: &Package,
228 config: EnvelopeConfig,
229) -> Result<(), EnvelopeError> {
230 write_envelope_impl(writer, &package.modules, &package.extensions, config)
231}
232
233pub(crate) fn write_envelope_impl<'h>(
238 mut writer: impl Write,
239 hugrs: impl IntoIterator<Item = &'h Hugr>,
240 extensions: &ExtensionRegistry,
241 config: EnvelopeConfig,
242) -> Result<(), EnvelopeError> {
243 let header = config.make_header();
244 header.write(&mut writer)?;
245
246 match config.zstd {
247 #[cfg(feature = "zstd")]
248 Some(zstd) => {
249 let writer = zstd::Encoder::new(writer, zstd.level())?.auto_finish();
250 write_impl(writer, hugrs, extensions, config)?;
251 }
252 #[cfg(not(feature = "zstd"))]
253 Some(_) => return Err(EnvelopeError::ZstdUnsupported),
254 None => write_impl(writer, hugrs, extensions, config)?,
255 }
256
257 Ok(())
258}
259#[derive(Debug, Error)]
261#[non_exhaustive]
262pub enum EnvelopeError {
263 #[error(
265 "Bad magic number. expected 0x{:X} found 0x{:X}",
266 u64::from_be_bytes(*expected),
267 u64::from_be_bytes(*found)
268 )]
269 MagicNumber {
270 expected: [u8; 8],
274 found: [u8; 8],
276 },
277 #[error("Format descriptor {descriptor} is invalid.")]
279 InvalidFormatDescriptor {
280 descriptor: usize,
282 },
283 #[error("Payload format {format} is not supported.{}",
285 match feature {
286 Some(f) => format!(" This requires the '{f}' feature for `hugr`."),
287 None => String::new()
288 },
289 )]
290 FormatUnsupported {
291 format: EnvelopeFormat,
293 feature: Option<&'static str>,
295 },
296 #[error("Envelope format {format} cannot be represented as ASCII.")]
300 NonASCIIFormat {
301 format: EnvelopeFormat,
303 },
304 #[error("Zstd compression is not supported. This requires the 'zstd' feature for `hugr`.")]
306 ZstdUnsupported,
307 #[error("Expected an envelope containing a single hugr, but it contained {}.", if *count == 0 {
309 "none".to_string()
310 } else {
311 count.to_string()
312 })]
313 ExpectedSingleHugr {
314 count: usize,
316 },
317 #[error(transparent)]
319 SerdeError {
320 #[from]
322 source: serde_json::Error,
323 },
324 #[error(transparent)]
326 IO {
327 #[from]
329 source: std::io::Error,
330 },
331 #[error(transparent)]
333 PackageEncoding {
334 #[from]
336 source: PackageEncodingError,
337 },
338 #[error(transparent)]
340 ModelImport {
341 #[from]
343 source: ImportError,
344 },
346 #[error(transparent)]
348 ModelRead {
349 #[from]
351 source: hugr_model::v0::binary::ReadError,
352 },
353 #[error(transparent)]
355 ModelWrite {
356 #[from]
358 source: hugr_model::v0::binary::WriteError,
359 },
360 #[error("Model text parsing error")]
362 ModelTextRead {
363 #[from]
365 source: hugr_model::v0::ast::ParseError,
366 },
367 #[error(transparent)]
369 ModelTextResolve {
370 #[from]
372 source: hugr_model::v0::ast::ResolveError,
373 },
374 #[error(transparent)]
376 ExtensionLoad {
377 #[from]
379 source: crate::extension::ExtensionRegistryLoadError,
380 },
381 #[error(
383 "The envelope configuration has unknown {}. Please update your HUGR version.",
384 if flag_ids.len() == 1 {format!("flag #{}", flag_ids[0])} else {format!("flags {}", flag_ids.iter().join(", "))}
385 )]
386 FlagUnsupported {
387 flag_ids: Vec<usize>,
389 },
390 #[deprecated(since = "0.24.1")]
392 #[error(transparent)]
393 #[expect(deprecated)]
394 ExtensionVersion {
395 #[from]
397 source: WithGenerator<ExtensionBreakingError>,
398 },
399
400 #[error(transparent)]
403 ExtensionLoading(#[from] ExtensionResolutionError),
404}
405
406#[derive(Debug, Error)]
407#[error(
408 "The envelope format {format} is not supported.{}",
409 match feature {
410 Some(f) => format!(" This requires the '{f}' feature for `hugr`."),
411 None => String::new()
412 },
413)]
414struct FormatUnsupportedError {
415 format: EnvelopeFormat,
417 feature: Option<&'static str>,
419}
420
421fn check_model_version(format: EnvelopeFormat) -> Result<(), FormatUnsupportedError> {
422 if format.model_version() != Some(0) {
423 return Err(FormatUnsupportedError {
424 format,
425 feature: None,
426 });
427 }
428 Ok(())
429}
430
431fn write_impl<'h>(
433 writer: impl Write,
434 hugrs: impl IntoIterator<Item = &'h Hugr>,
435 extensions: &ExtensionRegistry,
436 config: EnvelopeConfig,
437) -> Result<(), EnvelopeError> {
438 match config.format {
439 EnvelopeFormat::PackageJson => package_json::to_json_writer(hugrs, extensions, writer)?,
440 EnvelopeFormat::Model
441 | EnvelopeFormat::ModelWithExtensions
442 | EnvelopeFormat::ModelText
443 | EnvelopeFormat::ModelTextWithExtensions => {
444 encode_model(writer, hugrs, extensions, config.format)?;
445 }
446 }
447 Ok(())
448}
449
450fn encode_model<'h>(
451 mut writer: impl Write,
452 hugrs: impl IntoIterator<Item = &'h Hugr>,
453 extensions: &ExtensionRegistry,
454 format: EnvelopeFormat,
455) -> Result<(), EnvelopeError> {
456 use hugr_model::v0::{binary::write_to_writer, bumpalo::Bump};
457
458 use crate::export::export_package;
459
460 if format.model_version() != Some(0) {
461 return Err(EnvelopeError::FormatUnsupported {
462 format,
463 feature: None,
464 });
465 }
466
467 if format == EnvelopeFormat::ModelTextWithExtensions {
469 serde_json::to_writer(&mut writer, &extensions.iter().collect_vec())?;
470 }
471
472 let bump = Bump::default();
473 let model_package = export_package(hugrs, extensions, &bump);
474
475 match format {
476 EnvelopeFormat::Model | EnvelopeFormat::ModelWithExtensions => {
477 write_to_writer(&model_package, &mut writer)?;
478 }
479 EnvelopeFormat::ModelText | EnvelopeFormat::ModelTextWithExtensions => {
480 let model_package = model_package.as_ast().unwrap();
481 writeln!(writer, "{model_package}")?;
482 }
483 _ => unreachable!(),
484 }
485
486 if format == EnvelopeFormat::ModelWithExtensions {
488 serde_json::to_writer(writer, &extensions.iter().collect_vec())?;
489 }
490
491 Ok(())
492}
493
494#[derive(Debug, Error)]
495#[error(
496 "Extension '{name}' version mismatch: registered version is {registered}, but used version is {used}"
497)]
498pub struct ExtensionVersionMismatch {
501 pub name: String,
503 pub registered: Version,
505 pub used: Version,
507}
508
509#[derive(Debug, Error)]
510#[non_exhaustive]
511pub enum ExtensionBreakingError {
513 #[error("{0}")]
515 ExtensionVersionMismatch(ExtensionVersionMismatch),
516
517 #[error("Failed to deserialize used extensions metadata")]
519 Deserialization(#[from] serde_json::Error),
520}
521
522fn check_breaking_extensions(
527 registry: &ExtensionRegistry,
528 used_exts: impl IntoIterator<Item = description::ExtensionDesc>,
529) -> Result<(), ExtensionBreakingError> {
530 for ext in used_exts {
531 let Some(registered) = registry.get(ext.name.as_str()) else {
532 continue; };
534 if !compatible_versions(registered.version(), &ext.version) {
535 return Err(ExtensionBreakingError::ExtensionVersionMismatch(
538 ExtensionVersionMismatch {
539 name: ext.name,
540 registered: registered.version().clone(),
541 used: ext.version,
542 },
543 ));
544 }
545 }
546
547 Ok(())
548}
549
550fn compatible_versions(registered: &Version, used: &Version) -> bool {
555 if used.major != registered.major {
556 return false;
557 }
558 if used.major == 0 && used.minor != registered.minor {
559 return false;
560 }
561
562 registered >= used
563}
564
565#[cfg(test)]
566pub(crate) mod test {
567 use super::*;
568 use cool_asserts::assert_matches;
569 use rstest::rstest;
570 use std::borrow::Cow;
571 use std::io::BufReader;
572
573 use crate::HugrView;
574 use crate::builder::test::{multi_module_package, simple_package};
575 use crate::extension::{Extension, ExtensionRegistry, Version};
576 use crate::extension::{ExtensionId, PRELUDE_REGISTRY};
577 use crate::hugr::HugrMut;
578 use crate::hugr::test::check_hugr_equality;
579 use crate::std_extensions::STD_REG;
580 use serde_json::json;
581 use std::sync::Arc;
582
583 fn join_extensions<'a>(
587 extensions: &'a ExtensionRegistry,
588 other: &ExtensionRegistry,
589 ) -> Cow<'a, ExtensionRegistry> {
590 if other.iter().all(|e| extensions.contains(e.name())) {
591 Cow::Borrowed(extensions)
592 } else {
593 let mut extensions = extensions.clone();
594 extensions.extend(other);
595 Cow::Owned(extensions)
596 }
597 }
598
599 pub(crate) fn check_hugr_roundtrip(hugr: &Hugr, config: EnvelopeConfig) -> Hugr {
608 let mut buffer = Vec::new();
609 hugr.store(&mut buffer, config).unwrap();
610
611 let extensions = join_extensions(&STD_REG, hugr.extensions());
612
613 let reader = BufReader::new(buffer.as_slice());
614 let extracted = Hugr::load(reader, Some(&extensions)).unwrap();
615
616 check_hugr_equality(&extracted, hugr);
617 extracted
618 }
619
620 #[rstest]
621 fn errors() {
622 let package = simple_package();
623 assert_matches!(
624 package.store_str(EnvelopeConfig::binary()),
625 Err(EnvelopeError::NonASCIIFormat { .. })
626 );
627 }
628
629 #[rstest]
630 #[case::empty(Package::default())]
631 #[case::simple(simple_package())]
632 #[case::multi(multi_module_package())]
633 fn text_roundtrip(#[case] package: Package) {
634 let envelope = package.store_str(EnvelopeConfig::text()).unwrap();
635 let new_package = Package::load_str(&envelope, None).unwrap();
636 assert_eq!(package, new_package);
637 }
638
639 #[rstest]
640 #[case::empty(Package::default())]
641 #[case::simple(simple_package())]
642 #[case::multi(multi_module_package())]
643 #[cfg_attr(all(miri, feature = "zstd"), ignore)] fn compressed_roundtrip(#[case] package: Package) {
645 let mut buffer = Vec::new();
646 let config = EnvelopeConfig {
647 format: EnvelopeFormat::PackageJson,
648 zstd: Some(ZstdConfig::default()),
649 };
650 let res = package.store(&mut buffer, config);
651
652 match cfg!(feature = "zstd") {
653 true => res.unwrap(),
654 false => {
655 assert_matches!(res, Err(EnvelopeError::ZstdUnsupported));
656 return;
657 }
658 }
659
660 let (desc, new_package) =
661 read_described_envelope(BufReader::new(buffer.as_slice()), &PRELUDE_REGISTRY).unwrap();
662 let decoded_config = desc.header.config();
663 assert_eq!(config.format, decoded_config.format);
664 assert_eq!(config.zstd.is_some(), decoded_config.zstd.is_some());
665 assert_eq!(package, new_package);
666 }
667
668 #[rstest]
669 #[case::empty_model(Package::default(), EnvelopeFormat::Model)]
671 #[case::empty_model_exts(Package::default(), EnvelopeFormat::ModelWithExtensions)]
672 #[case::empty_text(Package::default(), EnvelopeFormat::ModelText)]
673 #[case::empty_text_exts(Package::default(), EnvelopeFormat::ModelTextWithExtensions)]
674 #[case::simple_bin(simple_package(), EnvelopeFormat::Model)]
676 #[case::simple_bin_exts(simple_package(), EnvelopeFormat::ModelWithExtensions)]
677 #[case::simple_text(simple_package(), EnvelopeFormat::ModelText)]
678 #[case::simple_text_exts(simple_package(), EnvelopeFormat::ModelTextWithExtensions)]
679 #[case::multi_bin(multi_module_package(), EnvelopeFormat::Model)]
681 #[case::multi_bin_exts(multi_module_package(), EnvelopeFormat::ModelWithExtensions)]
682 #[case::multi_text(multi_module_package(), EnvelopeFormat::ModelText)]
683 #[case::multi_text_exts(multi_module_package(), EnvelopeFormat::ModelTextWithExtensions)]
684 fn model_roundtrip(#[case] package: Package, #[case] format: EnvelopeFormat) {
685 let mut buffer = Vec::new();
686 let config = EnvelopeConfig { format, zstd: None };
687 package.store(&mut buffer, config).unwrap();
688
689 let (desc, new_package) =
690 read_described_envelope(BufReader::new(buffer.as_slice()), &PRELUDE_REGISTRY).unwrap();
691 let decoded_config = desc.header.config();
692
693 assert_eq!(config.format, decoded_config.format);
694 assert_eq!(config.zstd.is_some(), decoded_config.zstd.is_some());
695
696 assert_eq!(package, new_package);
697 }
698
699 fn check(hugr: &Hugr, registry: &ExtensionRegistry) -> Result<(), ExtensionBreakingError> {
701 let mut desc = description::ModuleDesc::default();
702 desc.load_used_extensions_generator(&hugr)?;
703 let Some(used_exts) = desc.used_extensions_generator else {
704 return Ok(());
705 };
706 check_breaking_extensions(registry, used_exts)
707 }
708
709 #[rstest]
710 #[case::simple(simple_package())]
711 fn test_check_breaking_extensions(#[case] mut package: Package) {
712 let test_ext_v0 =
714 Extension::new(ExtensionId::new_unchecked("test-v0"), Version::new(0, 2, 3));
715 let test_ext_v1 =
717 Extension::new(ExtensionId::new_unchecked("test-v1"), Version::new(1, 2, 3));
718
719 let registry =
721 ExtensionRegistry::new([Arc::new(test_ext_v0.clone()), Arc::new(test_ext_v1.clone())]);
722 let mut hugr = package.modules.remove(0);
723
724 assert_matches!(check(&hugr, ®istry), Ok(()));
726
727 let used_exts = json!([{ "name": "test-v0", "version": "0.2.3" }]);
729 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
730 assert_matches!(check(&hugr, ®istry), Ok(()));
731
732 let used_exts = json!([{ "name": "test-v0", "version": "0.2.2" }]);
734 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
735 assert_matches!(check(&hugr, ®istry), Ok(()));
736
737 let used_exts = json!([{ "name": "test-v0", "version": "0.3.3" }]);
739 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
740 assert_matches!(
741 check(&hugr, ®istry),
742 Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
743 name,
744 registered,
745 used
746 })) if name == "test-v0" && registered == Version::new(0, 2, 3) && used == Version::new(0, 3, 3)
747 );
748
749 assert!(
750 check(&hugr, hugr.extensions()).is_ok(),
751 "Extension is not actually used in the HUGR, should be ignored by full check"
752 );
753
754 let used_exts = json!([{ "name": "test-v0", "version": "1.2.3" }]);
756 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
757 assert_matches!(
758 check(&hugr, ®istry),
759 Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
760 name,
761 registered,
762 used
763 })) if name == "test-v0" && registered == Version::new(0, 2, 3) && used == Version::new(1, 2, 3)
764 );
765
766 let used_exts = json!([{ "name": "test-v0", "version": "0.2.4" }]);
768 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
769 assert_matches!(
770 check(&hugr, ®istry),
771 Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
772 name,
773 registered,
774 used
775 })) if name == "test-v0" && registered == Version::new(0, 2, 3) && used == Version::new(0, 2, 4)
776 );
777
778 let used_exts = json!([{ "name": "test-v1", "version": "1.2.3" }]);
780 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
781 assert_matches!(check(&hugr, ®istry), Ok(()));
782
783 let used_exts = json!([{ "name": "test-v1", "version": "1.1.0" }]);
785 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
786 assert_matches!(check(&hugr, ®istry), Ok(()));
787
788 let used_exts = json!([{ "name": "test-v1", "version": "1.2.2" }]);
790 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
791 assert_matches!(check(&hugr, ®istry), Ok(()));
792
793 let used_exts = json!([{ "name": "test-v1", "version": "2.2.3" }]);
795 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
796 assert_matches!(
797 check(&hugr, ®istry),
798 Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
799 name,
800 registered,
801 used
802 })) if name == "test-v1" && registered == Version::new(1, 2, 3) && used == Version::new(2, 2, 3)
803 );
804
805 let used_exts = json!([{ "name": "test-v1", "version": "1.3.0" }]);
807 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
808 assert_matches!(
809 check(&hugr, ®istry),
810 Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
811 name,
812 registered,
813 used
814 })) if name == "test-v1" && registered == Version::new(1, 2, 3) && used == Version::new(1, 3, 0)
815 );
816
817 let used_exts = json!([{ "name": "test-v1", "version": "1.2.4" }]);
819 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
820 assert_matches!(
821 check(&hugr, ®istry),
822 Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
823 name,
824 registered,
825 used
826 })) if name == "test-v1" && registered == Version::new(1, 2, 3) && used == Version::new(1, 2, 4)
827 );
828
829 let used_exts = json!([{ "name": "unknown", "version": "1.0.0" }]);
831 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
832 assert_matches!(check(&hugr, ®istry), Ok(()));
833
834 let used_exts = json!([
836 { "name": "unknown", "version": "1.0.0" },
837 { "name": "test-v1", "version": "2.0.0" }
838 ]);
839 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
840 assert_matches!(
841 check(&hugr, ®istry),
842 Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
843 name,
844 registered,
845 used
846 })) if name == "test-v1" && registered == Version::new(1, 2, 3) && used == Version::new(2, 0, 0)
847 );
848
849 hugr.set_metadata(
851 hugr.module_root(),
852 USED_EXTENSIONS_KEY,
853 json!("not an array"),
854 );
855 assert_matches!(
856 check(&hugr, ®istry),
857 Err(ExtensionBreakingError::Deserialization(_))
858 );
859
860 let used_exts = json!([
862 { "name": "test-v0", "version": "0.2.2" },
863 { "name": "test-v1", "version": "1.1.9" }
864 ]);
865 hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
866 assert_matches!(check(&hugr, ®istry), Ok(()));
867 }
868}