Skip to content

Commit d568010

Browse files
committed
feat: record used extension when resolution fails
1 parent 9cbbfd0 commit d568010

File tree

2 files changed

+106
-2
lines changed

2 files changed

+106
-2
lines changed

hugr-core/src/envelope/reader.rs

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use hugr_model::v0::table;
55
use itertools::{Either, Itertools as _};
66

77
use crate::HugrView as _;
8-
use crate::envelope::description::PackageDesc;
8+
use crate::envelope::description::{ExtensionDesc, ModuleDesc, PackageDesc};
99
use crate::envelope::header::{EnvelopeFormat, HeaderError};
1010
use crate::envelope::{
1111
EnvelopeError, EnvelopeHeader, ExtensionBreakingError, FormatUnsupportedError,
@@ -103,6 +103,32 @@ impl<R: BufRead> EnvelopeReader<R> {
103103
self.registry.extend(extensions);
104104
}
105105

106+
/// Handle extension resolution errors by recording missing extensions in the description.
107+
///
108+
/// This function inspects the error and adds any missing extensions to the module description
109+
/// with a default version of 0.0.0.
110+
fn handle_resolution_error(desc: &mut ModuleDesc, err: &ExtensionResolutionError) {
111+
match err {
112+
ExtensionResolutionError::MissingOpExtension {
113+
missing_extension, ..
114+
}
115+
| ExtensionResolutionError::MissingTypeExtension {
116+
missing_extension, ..
117+
} => desc.extend_used_extensions_resolved([ExtensionDesc::new(
118+
missing_extension,
119+
crate::extension::Version::new(0, 0, 0),
120+
)]),
121+
ExtensionResolutionError::InvalidConstTypes {
122+
missing_extensions, ..
123+
} => desc.extend_used_extensions_resolved(
124+
missing_extensions
125+
.iter()
126+
.map(|ext| ExtensionDesc::new(ext, crate::extension::Version::new(0, 0, 0))),
127+
),
128+
_ => {}
129+
}
130+
}
131+
106132
fn read_impl(&mut self) -> Result<Package, PayloadError> {
107133
let mut package = match self.header().format {
108134
EnvelopeFormat::PackageJson => self.decode_json()?,
@@ -121,7 +147,10 @@ impl<R: BufRead> EnvelopeReader<R> {
121147
check_breaking_extensions(module.extensions(), used_exts.drain(..))?;
122148
}
123149

124-
module.resolve_extension_defs(&self.registry)?;
150+
module
151+
.resolve_extension_defs(&self.registry)
152+
.inspect_err(|err| Self::handle_resolution_error(desc, err))?;
153+
125154
// overwrite the description with the actual module read,
126155
// cheap so ok to repeat.
127156
desc.load_from_hugr(&module);
@@ -415,4 +444,77 @@ mod test {
415444
assert_eq!(description.header, header);
416445
assert_eq!(description.n_modules(), 0); // No valid modules should be set
417446
}
447+
448+
#[test]
449+
fn test_handle_resolution_error() {
450+
use crate::extension::ExtensionId;
451+
use crate::ops::{OpName, constant::ValueName};
452+
use crate::types::TypeName;
453+
454+
let mut desc = ModuleDesc::default();
455+
let handle_error = |d: &mut ModuleDesc, err: &ExtensionResolutionError| {
456+
EnvelopeReader::<Cursor<Vec<u8>>>::handle_resolution_error(d, err)
457+
};
458+
let assert_extensions = |d: &ModuleDesc, expected_ids: &[&ExtensionId]| {
459+
let resolved = d.used_extensions_resolved.as_ref().unwrap();
460+
assert_eq!(resolved.len(), expected_ids.len());
461+
let names: Vec<_> = resolved.iter().map(|e| &e.name).collect();
462+
for ext_id in expected_ids {
463+
assert!(names.contains(&&ext_id.to_string()));
464+
}
465+
assert!(
466+
resolved
467+
.iter()
468+
.all(|e| e.version == crate::extension::Version::new(0, 0, 0))
469+
);
470+
};
471+
472+
// Test MissingOpExtension
473+
let ext_id = ExtensionId::new("test.extension").unwrap();
474+
let error = ExtensionResolutionError::MissingOpExtension {
475+
node: None,
476+
op: OpName::new("test.op"),
477+
missing_extension: ext_id.clone(),
478+
available_extensions: vec![],
479+
};
480+
handle_error(&mut desc, &error);
481+
assert_extensions(&desc, &[&ext_id]);
482+
483+
// Test MissingTypeExtension
484+
desc.used_extensions_resolved = None;
485+
let ext_id2 = ExtensionId::new("test.extension2").unwrap();
486+
let error = ExtensionResolutionError::MissingTypeExtension {
487+
node: None,
488+
ty: TypeName::new("test.type"),
489+
missing_extension: ext_id2.clone(),
490+
available_extensions: vec![],
491+
};
492+
handle_error(&mut desc, &error);
493+
assert_extensions(&desc, &[&ext_id2]);
494+
495+
// Test InvalidConstTypes with multiple extensions
496+
desc.used_extensions_resolved = None;
497+
let ext_id3 = ExtensionId::new("test.extension3").unwrap();
498+
let ext_id4 = ExtensionId::new("test.extension4").unwrap();
499+
let mut missing_exts = crate::extension::ExtensionSet::new();
500+
missing_exts.insert(ext_id3.clone());
501+
missing_exts.insert(ext_id4.clone());
502+
503+
let error = ExtensionResolutionError::InvalidConstTypes {
504+
value: ValueName::new("test.value"),
505+
missing_extensions: missing_exts,
506+
};
507+
handle_error(&mut desc, &error);
508+
assert_extensions(&desc, &[&ext_id3, &ext_id4]);
509+
510+
// Test other error variant (should not add anything)
511+
desc.used_extensions_resolved = None;
512+
let error = ExtensionResolutionError::WrongTypeDefExtension {
513+
extension: ExtensionId::new("ext1").unwrap(),
514+
def: TypeName::new("def"),
515+
wrong_extension: ExtensionId::new("ext2").unwrap(),
516+
};
517+
handle_error(&mut desc, &error);
518+
assert!(desc.used_extensions_resolved.is_none());
519+
}
418520
}

hugr-py/tests/test_cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,5 @@ def test_failed_describe(hugr_using_ext):
167167
assert mod.num_nodes == 8 # computed before error
168168
assert isinstance(desc.error, str)
169169
assert "requires extension ext" in desc.error
170+
171+
assert desc.uses_extension("ext")

0 commit comments

Comments
 (0)