From 6ac89f1dcd30ffd5635ded732492ddfc2585b20b Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Wed, 1 Apr 2026 19:35:54 -0400 Subject: [PATCH] fix: reject multiply-instantiated modules in resolver (defense-in-depth) The merger already rejects multiply-instantiated modules, but the resolver runs first and silently builds a corrupt module_to_instance HashMap (last-write-wins). Add early detection in resolve_via_instances before the map is constructed, so the error is caught at the earliest pipeline stage. Closes #24. Co-Authored-By: Claude Opus 4.6 (1M context) --- meld-core/src/resolver.rs | 79 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/meld-core/src/resolver.rs b/meld-core/src/resolver.rs index f6335e8..232036c 100644 --- a/meld-core/src/resolver.rs +++ b/meld-core/src/resolver.rs @@ -1581,6 +1581,20 @@ impl Resolver { } } + // Reject multiply-instantiated modules before building the + // module→instance map, which would silently drop duplicates. + { + let mut seen_modules = HashSet::new(); + for info in instantiate_infos.values() { + if !seen_modules.insert(info.module_idx) { + return Err(Error::DuplicateModuleInstantiation { + component_idx: comp_idx, + module_idx: info.module_idx, + }); + } + } + } + // Step 2: Build module → instance map (which instance instantiated which module). let mut module_to_instance: HashMap = HashMap::new(); for (inst_idx, info) in &instantiate_infos { @@ -3774,4 +3788,69 @@ mod tests { "SR-17: scalar-only results should produce zero copy layouts" ); } + + // --------------------------------------------------------------- + // Multiply-instantiated module rejection (issue #24) + // --------------------------------------------------------------- + + use crate::parser::{ComponentInstance, InstanceKind}; + + #[test] + fn test_multiply_instantiated_module_rejected() { + let mut comp = empty_parsed_component(); + comp.instances = vec![ + ComponentInstance { + index: 0, + kind: InstanceKind::Instantiate { + module_idx: 0, + args: vec![], + }, + }, + ComponentInstance { + index: 1, + kind: InstanceKind::Instantiate { + module_idx: 0, // duplicate — same module + args: vec![], + }, + }, + ]; + + let resolver = Resolver::new(); + let result = resolver.resolve(&[comp]); + assert!( + result.is_err(), + "should reject multiply-instantiated module" + ); + let err_msg = format!("{}", result.unwrap_err()); + assert!( + err_msg.contains("instantiates core module 0 more than once"), + "error should identify the duplicate module: {}", + err_msg + ); + } + + #[test] + fn test_distinct_module_instantiations_accepted() { + let mut comp = empty_parsed_component(); + comp.instances = vec![ + ComponentInstance { + index: 0, + kind: InstanceKind::Instantiate { + module_idx: 0, + args: vec![], + }, + }, + ComponentInstance { + index: 1, + kind: InstanceKind::Instantiate { + module_idx: 1, + args: vec![], + }, + }, + ]; + + let resolver = Resolver::new(); + let result = resolver.resolve(&[comp]); + assert!(result.is_ok(), "distinct modules should be accepted"); + } }