@@ -361,7 +361,7 @@ fn read_impl(
361361 header : EnvelopeHeader ,
362362 registry : & ExtensionRegistry ,
363363) -> Result < Package , EnvelopeError > {
364- let ( package, combined_registry ) = match header. format {
364+ let package = match header. format {
365365 #[ allow( deprecated) ]
366366 EnvelopeFormat :: PackageJson => Ok ( package_json:: from_json_reader ( payload, registry) ?) ,
367367 EnvelopeFormat :: Model | EnvelopeFormat :: ModelWithExtensions => {
@@ -373,8 +373,7 @@ fn read_impl(
373373 } ?;
374374
375375 package. modules . iter ( ) . try_for_each ( |module| {
376- check_breaking_extensions ( module, & combined_registry)
377- . map_err ( |err| WithGenerator :: new ( err, & package. modules ) )
376+ check_breaking_extensions ( module) . map_err ( |err| WithGenerator :: new ( err, & package. modules ) )
378377 } ) ?;
379378 Ok ( package)
380379}
@@ -393,7 +392,7 @@ fn decode_model(
393392 mut stream : impl BufRead ,
394393 extension_registry : & ExtensionRegistry ,
395394 format : EnvelopeFormat ,
396- ) -> Result < ( Package , ExtensionRegistry ) , EnvelopeError > {
395+ ) -> Result < Package , EnvelopeError > {
397396 check_model_version ( format) ?;
398397 let bump = Bump :: default ( ) ;
399398 let model_package = hugr_model:: v0:: binary:: read_from_reader ( & mut stream, & bump) ?;
@@ -406,7 +405,7 @@ fn decode_model(
406405
407406 let package = import_package ( & model_package, & extension_registry) ?;
408407
409- Ok ( ( package, extension_registry ) )
408+ Ok ( package)
410409}
411410
412411fn check_model_version ( format : EnvelopeFormat ) -> Result < ( ) , EnvelopeError > {
@@ -430,7 +429,7 @@ fn decode_model_ast(
430429 mut stream : impl BufRead ,
431430 extension_registry : & ExtensionRegistry ,
432431 format : EnvelopeFormat ,
433- ) -> Result < ( Package , ExtensionRegistry ) , EnvelopeError > {
432+ ) -> Result < Package , EnvelopeError > {
434433 check_model_version ( format) ?;
435434
436435 let mut extension_registry = extension_registry. clone ( ) ;
@@ -458,7 +457,7 @@ fn decode_model_ast(
458457
459458 let package = import_package ( & model_package, & extension_registry) ?;
460459
461- Ok ( ( package, extension_registry ) )
460+ Ok ( package)
462461}
463462
464463/// Internal implementation of [`write_envelope`] to call with/without the zstd compression wrapper.
@@ -558,12 +557,20 @@ pub enum ExtensionBreakingError {
558557 #[ error( "Failed to deserialize used extensions metadata" ) ]
559558 Deserialization ( #[ from] serde_json:: Error ) ,
560559}
560+ /// If HUGR metadata contains a list of used extensions, under the key [`USED_EXTENSIONS_KEY`],
561+ /// and extension is resolved in the HUGR, check that the
562+ /// version of the extension in the metadata matches the resolved version.
563+ /// Version compatibility is defined by [`compatible_versions`].
564+ fn check_breaking_extensions ( hugr : impl crate :: HugrView ) -> Result < ( ) , ExtensionBreakingError > {
565+ check_breaking_extensions_against_registry ( & hugr, hugr. extensions ( ) )
566+ }
567+
561568/// If HUGR metadata contains a list of used extensions, under the key [`USED_EXTENSIONS_KEY`],
562569/// and extension is registered in the given registry, check that the
563- /// version of the extension in the metadata matches the registered version (up to
564- /// MAJOR.MINOR) .
565- fn check_breaking_extensions (
566- hugr : impl crate :: HugrView ,
570+ /// version of the extension in the metadata matches the registered version.
571+ /// Version compatibility is defined by [`compatible_versions`] .
572+ fn check_breaking_extensions_against_registry (
573+ hugr : & impl crate :: HugrView ,
567574 registry : & ExtensionRegistry ,
568575) -> Result < ( ) , ExtensionBreakingError > {
569576 let Some ( exts) = hugr. get_metadata ( hugr. module_root ( ) , USED_EXTENSIONS_KEY ) else {
@@ -594,17 +601,16 @@ fn check_breaking_extensions(
594601/// Check if two versions are compatible according to:
595602/// - Major version must match.
596603/// - If major version is 0, minor version must match.
597- fn compatible_versions ( v1 : & Version , v2 : & Version ) -> bool {
598- if v1. major != v2. major {
599- return false ; // Major version mismatch
604+ /// - The registered version must be greater than or equal to the used version.
605+ fn compatible_versions ( registered : & Version , used : & Version ) -> bool {
606+ if used. major != registered. major {
607+ return false ;
600608 }
601-
602- if v1. major == 0 {
603- // For major version 0, we only allow minor version matches
604- return v1. minor == v2. minor ;
609+ if used. major == 0 && used. minor != registered. minor {
610+ return false ;
605611 }
606612
607- true
613+ registered >= used
608614}
609615
610616#[ cfg( test) ]
@@ -740,6 +746,11 @@ pub(crate) mod test {
740746 assert_eq ! ( package, new_package) ;
741747 }
742748
749+ /// Test helper to call `check_breaking_extensions_against_registry`
750+ fn check ( hugr : & Hugr , registry : & ExtensionRegistry ) -> Result < ( ) , ExtensionBreakingError > {
751+ check_breaking_extensions_against_registry ( hugr, registry)
752+ }
753+
743754 #[ rstest]
744755 #[ case:: simple( simple_package( ) ) ]
745756 fn test_check_breaking_extensions ( #[ case] mut package : Package ) {
@@ -756,73 +767,114 @@ pub(crate) mod test {
756767 let mut hugr = package. modules . remove ( 0 ) ;
757768
758769 // No metadata - should pass
759- assert_matches ! ( check_breaking_extensions ( & hugr, & registry) , Ok ( ( ) ) ) ;
770+ assert_matches ! ( check ( & hugr, & registry) , Ok ( ( ) ) ) ;
760771
761772 // Matching version for v0 - should pass
762773 let used_exts = json ! ( [ { "name" : "test-v0" , "version" : "0.2.3" } ] ) ;
763774 hugr. set_metadata ( hugr. module_root ( ) , USED_EXTENSIONS_KEY , used_exts) ;
764- assert_matches ! ( check_breaking_extensions ( & hugr, & registry) , Ok ( ( ) ) ) ;
775+ assert_matches ! ( check ( & hugr, & registry) , Ok ( ( ) ) ) ;
765776
766- // Matching major/minor but different patch for v0 - should pass
767- let used_exts = json ! ( [ { "name" : "test-v0" , "version" : "0.2.4 " } ] ) ;
777+ // Matching major/minor but lower patch for v0 - should pass
778+ let used_exts = json ! ( [ { "name" : "test-v0" , "version" : "0.2.2 " } ] ) ;
768779 hugr. set_metadata ( hugr. module_root ( ) , USED_EXTENSIONS_KEY , used_exts) ;
769- assert_matches ! ( check_breaking_extensions ( & hugr, & registry) , Ok ( ( ) ) ) ;
780+ assert_matches ! ( check ( & hugr, & registry) , Ok ( ( ) ) ) ;
770781
771782 //Different minor version for v0 - should fail
772783 let used_exts = json ! ( [ { "name" : "test-v0" , "version" : "0.3.3" } ] ) ;
773784 hugr. set_metadata ( hugr. module_root ( ) , USED_EXTENSIONS_KEY , used_exts) ;
774785 assert_matches ! (
775- check_breaking_extensions ( & hugr, & registry) ,
786+ check ( & hugr, & registry) ,
776787 Err ( ExtensionBreakingError :: ExtensionVersionMismatch ( ExtensionVersionMismatch {
777788 name,
778789 registered,
779790 used
780791 } ) ) if name == "test-v0" && registered == Version :: new( 0 , 2 , 3 ) && used == Version :: new( 0 , 3 , 3 )
781792 ) ;
782793
794+ assert ! (
795+ check_breaking_extensions( & hugr) . is_ok( ) ,
796+ "Extension is not actually used in the HUGR, should be ignored by full check"
797+ ) ;
798+
783799 // Different major version for v0 - should fail
784800 let used_exts = json ! ( [ { "name" : "test-v0" , "version" : "1.2.3" } ] ) ;
785801 hugr. set_metadata ( hugr. module_root ( ) , USED_EXTENSIONS_KEY , used_exts) ;
786802 assert_matches ! (
787- check_breaking_extensions ( & hugr, & registry) ,
803+ check ( & hugr, & registry) ,
788804 Err ( ExtensionBreakingError :: ExtensionVersionMismatch ( ExtensionVersionMismatch {
789805 name,
790806 registered,
791807 used
792808 } ) ) if name == "test-v0" && registered == Version :: new( 0 , 2 , 3 ) && used == Version :: new( 1 , 2 , 3 )
793809 ) ;
794810
811+ // Higher patch version for v0 - should fail
812+ let used_exts = json ! ( [ { "name" : "test-v0" , "version" : "0.2.4" } ] ) ;
813+ hugr. set_metadata ( hugr. module_root ( ) , USED_EXTENSIONS_KEY , used_exts) ;
814+ assert_matches ! (
815+ check( & hugr, & registry) ,
816+ Err ( ExtensionBreakingError :: ExtensionVersionMismatch ( ExtensionVersionMismatch {
817+ name,
818+ registered,
819+ used
820+ } ) ) if name == "test-v0" && registered == Version :: new( 0 , 2 , 3 ) && used == Version :: new( 0 , 2 , 4 )
821+ ) ;
822+
795823 // Matching version for v1 - should pass
796824 let used_exts = json ! ( [ { "name" : "test-v1" , "version" : "1.2.3" } ] ) ;
797825 hugr. set_metadata ( hugr. module_root ( ) , USED_EXTENSIONS_KEY , used_exts) ;
798- assert_matches ! ( check_breaking_extensions ( & hugr, & registry) , Ok ( ( ) ) ) ;
826+ assert_matches ! ( check ( & hugr, & registry) , Ok ( ( ) ) ) ;
799827
800- // Different minor version for v1 - should pass
801- let used_exts = json ! ( [ { "name" : "test-v1" , "version" : "1.3 .0" } ] ) ;
828+ // Lower minor version for v1 - should pass
829+ let used_exts = json ! ( [ { "name" : "test-v1" , "version" : "1.1 .0" } ] ) ;
802830 hugr. set_metadata ( hugr. module_root ( ) , USED_EXTENSIONS_KEY , used_exts) ;
803- assert_matches ! ( check_breaking_extensions ( & hugr, & registry) , Ok ( ( ) ) ) ;
831+ assert_matches ! ( check ( & hugr, & registry) , Ok ( ( ) ) ) ;
804832
805- // Different patch for v1 - should pass
806- let used_exts = json ! ( [ { "name" : "test-v1" , "version" : "1.2.4 " } ] ) ;
833+ // Lower patch for v1 - should pass
834+ let used_exts = json ! ( [ { "name" : "test-v1" , "version" : "1.2.2 " } ] ) ;
807835 hugr. set_metadata ( hugr. module_root ( ) , USED_EXTENSIONS_KEY , used_exts) ;
808- assert_matches ! ( check_breaking_extensions ( & hugr, & registry) , Ok ( ( ) ) ) ;
836+ assert_matches ! ( check ( & hugr, & registry) , Ok ( ( ) ) ) ;
809837
810838 // Different major version for v1 - should fail
811839 let used_exts = json ! ( [ { "name" : "test-v1" , "version" : "2.2.3" } ] ) ;
812840 hugr. set_metadata ( hugr. module_root ( ) , USED_EXTENSIONS_KEY , used_exts) ;
813841 assert_matches ! (
814- check_breaking_extensions ( & hugr, & registry) ,
842+ check ( & hugr, & registry) ,
815843 Err ( ExtensionBreakingError :: ExtensionVersionMismatch ( ExtensionVersionMismatch {
816844 name,
817845 registered,
818846 used
819847 } ) ) if name == "test-v1" && registered == Version :: new( 1 , 2 , 3 ) && used == Version :: new( 2 , 2 , 3 )
820848 ) ;
821849
850+ // Higher minor version for v1 - should fail
851+ let used_exts = json ! ( [ { "name" : "test-v1" , "version" : "1.3.0" } ] ) ;
852+ hugr. set_metadata ( hugr. module_root ( ) , USED_EXTENSIONS_KEY , used_exts) ;
853+ assert_matches ! (
854+ check( & hugr, & registry) ,
855+ Err ( ExtensionBreakingError :: ExtensionVersionMismatch ( ExtensionVersionMismatch {
856+ name,
857+ registered,
858+ used
859+ } ) ) if name == "test-v1" && registered == Version :: new( 1 , 2 , 3 ) && used == Version :: new( 1 , 3 , 0 )
860+ ) ;
861+
862+ // Higher patch version for v1 - should fail
863+ let used_exts = json ! ( [ { "name" : "test-v1" , "version" : "1.2.4" } ] ) ;
864+ hugr. set_metadata ( hugr. module_root ( ) , USED_EXTENSIONS_KEY , used_exts) ;
865+ assert_matches ! (
866+ check( & hugr, & registry) ,
867+ Err ( ExtensionBreakingError :: ExtensionVersionMismatch ( ExtensionVersionMismatch {
868+ name,
869+ registered,
870+ used
871+ } ) ) if name == "test-v1" && registered == Version :: new( 1 , 2 , 3 ) && used == Version :: new( 1 , 2 , 4 )
872+ ) ;
873+
822874 // Non-registered extension - should pass
823875 let used_exts = json ! ( [ { "name" : "unknown" , "version" : "1.0.0" } ] ) ;
824876 hugr. set_metadata ( hugr. module_root ( ) , USED_EXTENSIONS_KEY , used_exts) ;
825- assert_matches ! ( check_breaking_extensions ( & hugr, & registry) , Ok ( ( ) ) ) ;
877+ assert_matches ! ( check ( & hugr, & registry) , Ok ( ( ) ) ) ;
826878
827879 // Multiple extensions - one mismatch should fail
828880 let used_exts = json ! ( [
@@ -831,7 +883,7 @@ pub(crate) mod test {
831883 ] ) ;
832884 hugr. set_metadata ( hugr. module_root ( ) , USED_EXTENSIONS_KEY , used_exts) ;
833885 assert_matches ! (
834- check_breaking_extensions ( & hugr, & registry) ,
886+ check ( & hugr, & registry) ,
835887 Err ( ExtensionBreakingError :: ExtensionVersionMismatch ( ExtensionVersionMismatch {
836888 name,
837889 registered,
@@ -846,17 +898,17 @@ pub(crate) mod test {
846898 json ! ( "not an array" ) ,
847899 ) ;
848900 assert_matches ! (
849- check_breaking_extensions ( & hugr, & registry) ,
901+ check ( & hugr, & registry) ,
850902 Err ( ExtensionBreakingError :: Deserialization ( _) )
851903 ) ;
852904
853905 // Multiple extensions with all compatible versions - should pass
854906 let used_exts = json ! ( [
855- { "name" : "test-v0" , "version" : "0.2.5 " } ,
856- { "name" : "test-v1" , "version" : "1.9 .9" }
907+ { "name" : "test-v0" , "version" : "0.2.2 " } ,
908+ { "name" : "test-v1" , "version" : "1.1 .9" }
857909 ] ) ;
858910 hugr. set_metadata ( hugr. module_root ( ) , USED_EXTENSIONS_KEY , used_exts) ;
859- assert_matches ! ( check_breaking_extensions ( & hugr, & registry) , Ok ( ( ) ) ) ;
911+ assert_matches ! ( check ( & hugr, & registry) , Ok ( ( ) ) ) ;
860912 }
861913
862914 #[ test]
@@ -875,7 +927,7 @@ pub(crate) mod test {
875927 hugr. set_metadata ( hugr. module_root ( ) , USED_EXTENSIONS_KEY , used_exts) ;
876928
877929 // Create the error and wrap it with WithGenerator
878- let err = check_breaking_extensions ( & hugr, & registry) . unwrap_err ( ) ;
930+ let err = check_breaking_extensions_against_registry ( & hugr, & registry) . unwrap_err ( ) ;
879931 let with_gen = WithGenerator :: new ( err, & [ & hugr] ) ;
880932
881933 let err_msg = with_gen. to_string ( ) ;
0 commit comments