-
Notifications
You must be signed in to change notification settings - Fork 18
fix: Replace non-deterministic iterations on hash maps #3055
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
aadb41d
88ec14d
4d9c088
edf2337
bbc5f04
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| //! Implementation of the `Replace` operation. | ||
|
|
||
| use std::collections::{HashMap, HashSet, VecDeque}; | ||
| use std::collections::{BTreeSet, HashMap, HashSet, VecDeque}; | ||
|
|
||
| use itertools::Itertools; | ||
| use thiserror::Error; | ||
|
|
@@ -71,12 +71,16 @@ pub struct Replacement<HostNode = Node> { | |
| pub replacement: Hugr, | ||
| /// Describes how parts of the Hugr that would otherwise be removed should | ||
| /// instead be preserved but with new parents amongst the newly-inserted | ||
| /// nodes. This is a Map from container nodes in [`Self::replacement`] | ||
| /// nodes. | ||
| /// | ||
| /// This is a Map from container nodes in [`Self::replacement`] | ||
| /// that have no children, to container nodes that are descended from | ||
| /// [`Self::removal`]. The keys are the new parents for the children of | ||
| /// the values. Note no value may be ancestor or descendant of another. | ||
| /// This is "B" in the spec; "R" is the set of descendants of | ||
| /// [`Self::removal`] that are not descendants of values here. | ||
| /// | ||
| /// The nodes in the values must be distinct descendants of the removed nodes. | ||
| pub adoptions: HashMap<Node, HostNode>, | ||
| /// Edges from nodes in the existing Hugr that are not removed | ||
| /// ([`NewEdgeSpec::src`] in Gamma\R) to inserted nodes | ||
|
|
@@ -146,7 +150,7 @@ impl<HostNode: HugrNode, N: Clone> NewEdgeSpec<N, HostNode> { | |
| fn check_existing_edge( | ||
| &self, | ||
| h: &impl HugrView<Node = HostNode>, | ||
| legal_src_ancestors: &HashSet<HostNode>, | ||
| legal_src_ancestors: &BTreeSet<HostNode>, | ||
| err_edge: impl Fn(Self) -> WhichEdgeSpec<HostNode>, | ||
| ) -> Result<(), ReplaceError<HostNode>> { | ||
| if let NewEdgeKind::Static { tgt_pos, .. } | NewEdgeKind::Value { tgt_pos, .. } = self.kind | ||
|
|
@@ -202,7 +206,7 @@ impl<HostNode: HugrNode> Replacement<HostNode> { | |
| fn get_removed_nodes( | ||
| &self, | ||
| h: &impl HugrView<Node = HostNode>, | ||
| ) -> Result<HashSet<HostNode>, ReplaceError<HostNode>> { | ||
| ) -> Result<BTreeSet<HostNode>, ReplaceError<HostNode>> { | ||
| // Check the keys of the transfer map too, the values we'll use imminently | ||
| self.adoptions.keys().try_for_each(|&n| { | ||
| (self.replacement.contains_node(n) | ||
|
|
@@ -222,7 +226,7 @@ impl<HostNode: HugrNode> Replacement<HostNode> { | |
| )); | ||
| } | ||
|
|
||
| let mut removed = HashSet::new(); | ||
| let mut removed = BTreeSet::new(); | ||
| let mut queue = VecDeque::from_iter(self.removal.iter().copied()); | ||
| while let Some(n) = queue.pop_front() { | ||
| let new = removed.insert(n); | ||
|
|
@@ -400,9 +404,18 @@ impl<HostNode: HugrNode> PatchHugrMut for Replacement<HostNode> { | |
| h.remove_node(inserted_entrypoint); | ||
|
|
||
| // 6. Transfer to keys of `transfers` children of the corresponding values. | ||
| #[expect( | ||
| clippy::iter_over_hash_type, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Of course, if the same value (old_parent) appeared twice in the map, we'd get completely different results - they'd get moved to the first new_parent (and then there'd to nothing to move to whichever new_parent came second)....
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ouch, good catch. Repeated values should cause a hard error. |
||
| reason = "adoptions move disjoint child sets, so order cannot affect the result. Repeated values return an error." | ||
| )] | ||
| for (new_parent, &old_parent) in &self.adoptions { | ||
| let new_parent = node_map.get(new_parent).unwrap(); | ||
| debug_assert!(h.children(old_parent).next().is_some()); | ||
|
|
||
| if h.children(old_parent).next().is_none() { | ||
| // `old_parent` appeared more than once on the right-hand side of `adoptions`. | ||
| return Err(ReplaceError::RepeatedAdoptee { node: old_parent }); | ||
| } | ||
|
|
||
| while let Some(ch) = h.first_child(old_parent) { | ||
| h.set_parent(ch, *new_parent); | ||
| } | ||
|
|
@@ -422,7 +435,7 @@ fn transfer_edges<'a, SrcNode, TgtNode, HostNode>( | |
| trans_src: impl Fn(SrcNode) -> Option<HostNode>, | ||
| trans_tgt: impl Fn(TgtNode) -> Option<HostNode>, | ||
| err_spec: impl Fn(NewEdgeSpec<SrcNode, TgtNode>) -> WhichEdgeSpec<HostNode>, | ||
| legal_src_ancestors: Option<&HashSet<HostNode>>, | ||
| legal_src_ancestors: Option<&BTreeSet<HostNode>>, | ||
| ) -> Result<(), ReplaceError<HostNode>> | ||
| where | ||
| SrcNode: 'a + HugrNode, | ||
|
|
@@ -509,6 +522,12 @@ pub enum ReplaceError<HostNode = Node> { | |
| /// The [`NewEdgeKind`] was not applicable for the source/target node(s) | ||
| #[error("The edge kind was not applicable to the {0:?} node: {1:?}")] | ||
| BadEdgeKind(Direction, WhichEdgeSpec<HostNode>), | ||
| /// Some value in [`Replacement::adoptions`] was repeated for multiple parents. The nodes are indicated on a best-effort basis. | ||
| #[error("Node {node:?} adopted by multiple new parents")] | ||
| RepeatedAdoptee { | ||
| /// The node that was repeated | ||
| node: HostNode, | ||
| }, | ||
| } | ||
|
|
||
| /// The three kinds of [`NewEdgeSpec`] that may appear in a [`ReplaceError`] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ use hugr_core::{ | |
| Hugr, HugrView, Node, | ||
| hugr::patch::{Patch, simple_replace}, | ||
| }; | ||
| use indexmap::IndexMap; | ||
| use itertools::Itertools; | ||
| use relrc::HistoryGraph; | ||
|
|
||
|
|
@@ -187,13 +188,10 @@ impl PersistentHugr { | |
| ) -> Result<CommitId, InvalidCommit> { | ||
| // Check that `replacement` does not conflict with siblings at any of its | ||
| // parents | ||
| let new_invalid_nodes = replacement | ||
| .subgraph() | ||
| .nodes() | ||
| .iter() | ||
| .map(|&PatchNode(id, node)| (id, node)) | ||
| .into_grouping_map() | ||
| .collect::<BTreeSet<_>>(); | ||
| let mut new_invalid_nodes = IndexMap::<_, BTreeSet<_>>::new(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This feels independent/driveby? Map on LHS is BTreeMap not HashMap?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The groups themselves are stored using hashes. |
||
| for &PatchNode(id, node) in replacement.subgraph().nodes() { | ||
| new_invalid_nodes.entry(id).or_default().insert(node); | ||
| } | ||
| for (parent, new_invalid_nodes) in new_invalid_nodes { | ||
| let invalidation_set = self.deleted_nodes(parent).collect(); | ||
| if let Some(&node) = new_invalid_nodes.intersection(&invalidation_set).next() { | ||
|
|
@@ -394,6 +392,10 @@ impl PersistentHugr { | |
| hugr.mermaid_string() | ||
| ); | ||
|
|
||
| #[expect( | ||
| clippy::iter_over_hash_type, | ||
| reason = "inserting independent node mappings is order-independent" | ||
| )] | ||
| for (old_node, new_node) in new_node_map { | ||
| let old_patch_node = PatchNode(commit_id, old_node); | ||
| node_map.insert(old_patch_node, new_node); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could move the
roots.insertout of the loop usingextend, which avoids the lint (whether because it really knows that the keys must all be unique, or it just doesn't understand extend, I'm not sure..). But yeah, removing the lint requirescollecting all the children-whose-grandchildren-to-remove into a list, which again, makes me doubt that the linter is actually all that clever...