Skip to content

fix: Replace non-deterministic iterations on hash maps#3055

Open
aborgna-q wants to merge 5 commits into
mainfrom
ab/iter-over-hash-type
Open

fix: Replace non-deterministic iterations on hash maps#3055
aborgna-q wants to merge 5 commits into
mainfrom
ab/iter-over-hash-type

Conversation

@aborgna-q

Copy link
Copy Markdown
Collaborator

Enables the iter_over_hash_type clippy lint, and fixes all cases of iteration over hashmaps and hashsets that could cause non-deterministic outputs.

@aborgna-q aborgna-q requested a review from a team as a code owner May 12, 2026 10:21
@aborgna-q aborgna-q requested a review from mark-koch May 12, 2026 10:21
@codecov

codecov Bot commented May 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.75000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 81.29%. Comparing base (90a165e) to head (bbc5f04).
⚠️ Report is 33 commits behind head on main.

Files with missing lines Patch % Lines
hugr-core/src/hugr/patch/replace.rs 85.71% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3055      +/-   ##
==========================================
+ Coverage   81.09%   81.29%   +0.19%     
==========================================
  Files         241      242       +1     
  Lines       45054    46823    +1769     
  Branches    38822    40506    +1684     
==========================================
+ Hits        36536    38064    +1528     
- Misses       6542     6768     +226     
- Partials     1976     1991      +15     
Flag Coverage Δ
python 89.47% <ø> (+0.57%) ⬆️
rust 80.01% <93.75%> (+0.17%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread hugr-core/src/hugr/patch/outline_cfg.rs Outdated
blocks: Vec<Node>,
) -> (Node, Node, Node) {
let mut other_blocks = h.children(cfg).collect::<HashSet<_>>();
let mut other_blocks = h.children(cfg).collect::<BTreeSet<_>>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessary - we only iterate through this to do an assert (line 489, five lines down) - you could either expect on the loop or rewrite to an .all (if that helps?)

Comment thread hugr-core/src/hugr/linking.rs Outdated
Comment thread hugr-core/src/hugr/linking.rs Outdated
return Err(NodeLinkingError::NotChildOfRoot(sn));
}
for sn in other.children(other.module_root()) {
let Some(dirv) = children.get(&sn) else {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eeek! This means we're now going through other.children which might be much bigger than children.

Nondeterminism only affects the error reported (NodeMultiplyReplaced), but to prevent that, you are probably better off copying children into a BTreeSet (avoiding the need for min_directive_key and so on)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rolled back, and added some expects.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you? You're still going through other.children, I mean I guess it's not terrible (you'll copy them all when linking happens)...you decided not to just put children into a BTreeThing

Comment thread hugr-core/src/hugr.rs Outdated
/// sibling graph (since these must be the input and output nodes).
fn order_siblings_by_node_index(&mut self) {
let mut node_children: HashMap<Node, Vec<Node>> = HashMap::default();
let mut node_children = Vec::new();

Copy link
Copy Markdown
Contributor

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_children seems independent.

However, node_children seems like an enormous structure. Why not just merge the loops (move the self.hierarchy stuff in the second loop, into the first)? You'd need to do for node in self.nodes().collect::<Vec<_>>() but that vec would be much smaller than node_children after all!

Copy link
Copy Markdown
Collaborator Author

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.

.map(|&PatchNode(id, node)| (id, node))
.into_grouping_map()
.collect::<BTreeSet<_>>();
let mut new_invalid_nodes = IndexMap::<_, BTreeSet<_>>::new();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels independent/driveby? Map on LHS is BTreeMap not HashMap?

@aborgna-q aborgna-q Jun 4, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The groups themselves are stored using hashes.
.into_grouping_map().collect<T>() produces a HashMap<T>.

@acl-cqc acl-cqc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love it, thanks @aborgna-q - a few suggestions but happy to approve unless you'd rather @mark-koch had a final look over?

aborgna-q and others added 2 commits May 20, 2026 17:03
Co-authored-by: Alan Lawrence <alan.lawrence@quantinuum.com>
@aborgna-q aborgna-q requested a review from acl-cqc June 4, 2026 14:42

// 6. Transfer to keys of `transfers` children of the corresponding values.
#[expect(
clippy::iter_over_hash_type,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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)....

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ouch, good catch.

Repeated values should cause a hard error.
I added the check (previously it was only a debug_assert), and documented it in Replacement::adoptions.

Comment thread hugr-core/src/hugr/patch/replace.rs Outdated
}

// 7. Remove remaining nodes
#[expect(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed this is harmless. But we could remove the expect by changing private fn get_removed_nodes to return a BTreeSet....

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

roots.insert(other.entrypoint(), parent);
other.set_parent(other.entrypoint(), other.module_root());
};
#[expect(

Copy link
Copy Markdown
Contributor

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.insert out of the loop using extend, 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 requires collecting all the children-whose-grandchildren-to-remove into a list, which again, makes me doubt that the linter is actually all that clever...

@aborgna-q aborgna-q requested a review from acl-cqc June 5, 2026 14:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants