-
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 2 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 |
|---|---|---|
|
|
@@ -91,6 +91,10 @@ pub trait HugrLinking: HugrMut { | |
| roots.insert(other.entrypoint(), parent); | ||
| other.set_parent(other.entrypoint(), other.module_root()); | ||
| }; | ||
| #[expect( | ||
|
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. You could move the |
||
| clippy::iter_over_hash_type, | ||
| reason = "roots are stored in a BTreeMap and child pruning is independent" | ||
| )] | ||
| for (ch, dirv) in children.iter() { | ||
| roots.insert(*ch, self.module_root()); | ||
| if matches!(dirv, NodeLinkingDirective::UseExisting(_)) { | ||
|
|
@@ -759,10 +763,22 @@ fn check_directives<SRC: HugrView, TN: HugrNode>( | |
| parent: Option<TN>, | ||
| children: &NodeLinkingDirectives<SRC::Node, TN>, | ||
| ) -> Result<Transfers<SRC::Node, TN>, NodeLinkingError<SRC::Node, TN>> { | ||
| /// Returns the minimum key in `children` for which `pred` returns true, or | ||
| /// `None` if there is no such key. | ||
| /// | ||
| /// Helper function to avoid nondeterminism in error reporting from iterating | ||
| /// over `children` (a `HashMap`). | ||
| fn min_directive_key<SN: HugrNode, TN>( | ||
| children: &NodeLinkingDirectives<SN, TN>, | ||
| mut pred: impl FnMut(SN) -> bool, | ||
| ) -> Option<SN> { | ||
| children.keys().copied().filter(|&n| pred(n)).min() | ||
| } | ||
|
|
||
| if parent.is_some() { | ||
| if other.entrypoint() == other.module_root() { | ||
| if let Some(c) = children.keys().next() { | ||
| return Err(NodeLinkingError::ChildOfEntrypoint(*c)); | ||
| if let Some(c) = min_directive_key(children, |_| true) { | ||
| return Err(NodeLinkingError::ChildOfEntrypoint(c)); | ||
| } | ||
| } else { | ||
| let mut n = other.entrypoint(); | ||
|
|
@@ -787,10 +803,15 @@ fn check_directives<SRC: HugrView, TN: HugrNode>( | |
| replace: HashMap::default(), | ||
| use_existing: HashMap::default(), | ||
| }; | ||
| for (&sn, dirv) in children { | ||
| if other.get_parent(sn) != Some(other.module_root()) { | ||
| return Err(NodeLinkingError::NotChildOfRoot(sn)); | ||
| } | ||
| if let Some(sn) = min_directive_key(children, |sn| { | ||
| other.get_parent(sn) != Some(other.module_root()) | ||
| }) { | ||
| return Err(NodeLinkingError::NotChildOfRoot(sn)); | ||
| } | ||
| for sn in other.children(other.module_root()) { | ||
| let Some(dirv) = children.get(&sn) else { | ||
|
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. Eeek! This means we're now going through Nondeterminism only affects the error reported (NodeMultiplyReplaced), but to prevent that, you are probably better off copying
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. Rolled back, and added some
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. Have you? You're still going through |
||
| continue; | ||
| }; | ||
| match dirv { | ||
| NodeLinkingDirective::Add { replace } => { | ||
| for &r in replace { | ||
|
|
@@ -814,12 +835,20 @@ fn link_by_node<SN: HugrNode, TGT: HugrLinking + ?Sized>( | |
| ) { | ||
| // Resolve `use_existing` first in case the existing node is also replaced by | ||
| // a new node (which we know will not be in RHS of any entry in `replace`). | ||
| #[expect( | ||
| clippy::iter_over_hash_type, | ||
| reason = "each source is replaced independently, so order cannot affect the result" | ||
| )] | ||
| for (sn, tn) in transfers.use_existing { | ||
| let copy = node_map.remove(&sn).unwrap(); | ||
| // Because of `UseExisting` we avoided adding `sn`s descendants | ||
| debug_assert_eq!(hugr.children(copy).next(), None); | ||
| replace_static_src(hugr, copy, tn); | ||
| } | ||
| #[expect( | ||
| clippy::iter_over_hash_type, | ||
| reason = "each source is replaced independently, so order cannot affect the result" | ||
| )] | ||
| for (tn, sn) in transfers.replace { | ||
| let new_node = *node_map.get(&sn).unwrap(); | ||
| replace_static_src(hugr, tn, new_node); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| //! Rewrite for inserting a CFG-node into the hierarchy containing a subsection | ||
| //! of an existing CFG | ||
| use std::collections::HashSet; | ||
| use std::collections::BTreeSet; | ||
|
|
||
| use itertools::Itertools; | ||
| use thiserror::Error; | ||
|
|
@@ -19,14 +19,14 @@ use super::{PatchHugrMut, PatchVerification}; | |
| /// Moves some of the blocks in a Control-flow region into a new CFG-node that | ||
| /// is the only child of a new Basic Block in the original region. | ||
| pub struct OutlineCfg { | ||
| blocks: HashSet<Node>, | ||
| blocks: BTreeSet<Node>, | ||
| } | ||
|
|
||
| impl OutlineCfg { | ||
| /// Create a new `OutlineCfg` rewrite that will move the provided blocks. | ||
| pub fn new(blocks: impl IntoIterator<Item = Node>) -> Self { | ||
| Self { | ||
| blocks: HashSet::from_iter(blocks), | ||
| blocks: BTreeSet::from_iter(blocks), | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -236,7 +236,7 @@ pub enum OutlineCfgError { | |
|
|
||
| #[cfg(test)] | ||
| mod test { | ||
| use std::collections::HashSet; | ||
| use std::collections::{BTreeSet, HashSet}; | ||
|
|
||
| use crate::builder::{ | ||
| BlockBuilder, BuildError, CFGBuilder, Container, Dataflow, DataflowSubContainer, | ||
|
|
@@ -481,7 +481,7 @@ mod test { | |
| cfg: Node, | ||
| blocks: Vec<Node>, | ||
| ) -> (Node, Node, Node) { | ||
| let mut other_blocks = h.children(cfg).collect::<HashSet<_>>(); | ||
| let mut other_blocks = h.children(cfg).collect::<BTreeSet<_>>(); | ||
|
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. Not necessary - we only iterate through this to do an |
||
| assert!(blocks.iter().all(|b| other_blocks.remove(b))); | ||
| let [new_block, new_cfg] = h.apply_patch(OutlineCfg::new(blocks.clone())).unwrap(); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -400,6 +400,10 @@ 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" | ||
| )] | ||
| 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()); | ||
|
|
@@ -409,6 +413,10 @@ impl<HostNode: HugrNode> PatchHugrMut for Replacement<HostNode> { | |
| } | ||
|
|
||
| // 7. Remove remaining nodes | ||
| #[expect( | ||
|
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. Agreed this is harmless. But we could remove the
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. done |
||
| clippy::iter_over_hash_type, | ||
| reason = "all nodes in this set are removed, so removal order does not affect the result" | ||
| )] | ||
| for n in to_remove { | ||
| h.remove_node(n); | ||
| } | ||
|
|
||
| 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.
I'm not convinced there's any actual nondeterminism here; processing each element of
node_childrenseems independent.However,
node_childrenseems like an enormous structure. Why not just merge the loops (move theself.hierarchystuff in the second loop, into the first)? You'd need to dofor node in self.nodes().collect::<Vec<_>>()but that vec would be much smaller thannode_childrenafter all!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.
Done! We're only mutating the hierarchy, so the borrow checker is happy if we iterate over
self.graph's nodes instead.