-
Notifications
You must be signed in to change notification settings - Fork 8
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
feat: Rewrite for inlining a single Call #1934
base: main
Are you sure you want to change the base?
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1934 +/- ##
========================================
Coverage 83.71% 83.72%
========================================
Files 196 197 +1
Lines 37533 37738 +205
Branches 34346 34551 +205
========================================
+ Hits 31422 31597 +175
- Misses 4328 4338 +10
- Partials 1783 1803 +20
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
@@ -448,6 +456,50 @@ impl<T: RootTagged<RootHandle = Node> + AsMut<Hugr>> HugrMut for T { | |||
} | |||
translate_indices(node_map) | |||
} | |||
|
|||
fn copy_descendants(&mut self, root: Node, new_parent: Node) -> HashMap<Node, Node> { | |||
// TODO should we check that we will not invalidate the Hugr? |
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.
My general feeling is that this is too hard, at the least, it involves at least O(size of subtree)
work (probably * O(depth)
), never mind Dom edges; and it'd only be meaningful if the new location was already in its final resting place in the hierarchy anyway. But it does leave a bit of a bad taste that a "valid" Rewrite could make a valid Hugr invalidate without warning or any method of detection....
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.
Any linear-typed edge incoming to
root
I don't think this a problem on its face. the ports of root have nothing to do with this. The copied Input node will have a linear outgoing edge, but it will be connected exactly once. A bigger problem is that new_parent
will now have two input nodes, unless you carefully arrange that it is initially empty
Non-local edges can't happen for FuncDefns, so that's nice.
Other methods on HugrMut
can invalidate the hugr, e.g. disconnect
. @aborgna-q What do you think? Should this method be on HugrMutInternals
?.
I would prefer this comment(well, at least the non-local edges bit) to be a doc on the definition in the trait.
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.
Yeah, I think the linear-typed edge comment only applied to when I was copying subtrees not descendants, so I deleted that and left the rest....
Your example of disconnect
is good, so if the best we can do is to make this a doc comment (and raise an issue for better handling/detection of "nonlocal edges incoming to a subtree", if we don't have one already...hmmm), then let's do that....right?
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 would say yes. I tagged Agustin because he asked me to move a "dangerous" function, and he may disagree.
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.
@doug-q's comment seems reasonable imho.
We should describe how this may break the structure in the description, including the doubled i/o nodes.
Coverage looks poor because of use of |
@@ -146,6 +146,14 @@ pub trait HugrMut: HugrMutInternals { | |||
self.hugr_mut().remove_node(node); | |||
} | |||
|
|||
/// Copies the strict descendants of `root` to under the `new_parent`. | |||
/// (That is, the immediate children of root, are copied to make children of `new_parent`). | |||
fn copy_descendants(&mut self, root: Node, new_parent: Node) -> HashMap<Node, 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.
could a test be refactored to use this function so it is covered?
// Copy the optypes, metadata, and hierarchy | ||
for (&node, &new_node) in node_map.iter() { | ||
for ch in self.children(node).collect::<Vec<_>>() { | ||
self.set_parent(*node_map.get(&ch).unwrap(), 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.
looks like the hierarchy part isn't covered, do you need a test with more nesting?
let [i] = fb.input_wires_arr(); | ||
let add = fb.add_dataflow_op(IntOpDef::iadd.with_log_width(5), [i, cst1])?; | ||
let call = fb.call( | ||
&FuncID::<true>::from(fb.container_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.
not for this PR but this recursive handle should probably in the function builder API
} | ||
assert_eq!(ancestors.next(), Some(main.node())); | ||
assert_eq!(ancestors.next(), Some(hugr.root())); | ||
assert_eq!(ancestors.next(), None); |
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.
nice
@@ -146,6 +146,14 @@ pub trait HugrMut: HugrMutInternals { | |||
self.hugr_mut().remove_node(node); | |||
} | |||
|
|||
/// Copies the strict descendants of `root` to under the `new_parent`. | |||
/// (That is, the immediate children of root, are copied to make children of `new_parent`). | |||
fn copy_descendants(&mut self, root: Node, new_parent: Node) -> HashMap<Node, 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.
Consider returning a BTreeMap
over a HashMap
here, as a small help for determinism.
hugr-core/src/hugr/hugrmut.rs
Outdated
}*/ | ||
let mut nodes = Vec::new(); | ||
let mut q = VecDeque::from_iter(self.children(root)); | ||
while let Some(n) = q.pop_front() { |
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.
Can you use DescendentsView
here? This is such a common operation I think we should provide a helper for it. I have code golfed
iter::successors(Some(vec![root]), |nodes| {
let children = nodes.iter().flat_map(|&n| hugr.children(n)).collect_vec();
(!children.is_empty()).then_some(children)
})
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 originally avoided this because I didn't want to "build a whole DescendantsGraph" but now I realize that a DescendantsGraph isn't much more than the collection of nodes that we want.
However, in fact there is a method in Hierarchy
that does pretty much exactly what we want. So I've used that. Do you (still) think it's worth adding a helper to HugrView??
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.
In general yes, for you here, no. Hierarchy is not available out-of-crate.
// Copy the optypes, metadata, and hierarchy | ||
for (&node, &new_node) in node_map.iter() { | ||
for ch in self.children(node).collect::<Vec<_>>() { | ||
self.set_parent(*node_map.get(&ch).unwrap(), 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.
This is nondeterministic because you are iterating a HashMap. the order of children in their parent will depend on iteration order. It's a little out of scope for this PR, but would you consider changing translate_indices
to return a BTreeMap
?
EDIT: no I'm wrong. You mutate the nodes in a nondeterministic order, but I don't think this is actually nondeterministic. for each parent you set_parent it's children in a deterministic order.
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.
Yes, ISTR this hit me just getting Input/Output in the wrong order, so I had to make sure it did it "right"...
@@ -0,0 +1,234 @@ | |||
//! Rewrite to inline a Call to a FuncDefn by copying the body of the function | |||
//! into a DFG which replaces the Call node. | |||
use thiserror::Error; |
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.
Can you use derive_more::Error
? One day we will be able to delete the thiserror dependency
Ok(()) | ||
} | ||
|
||
fn apply(self, h: &mut impl HugrMut) -> Result<(), Self::Error> { |
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 don't think you are considering the case of polymorphic functions here? I think you need to substitute the type args of the call into the signature and all descendents.
I fo think it's ok to reject polymorphic functions for now
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.
Oh dang that's a good point. Knew some things would get forgotten what with this sleeping over hack fortnight...
So I could do this later, but it's not much new code - you just need to iterate through the same nodes
and apply the substitution. However, this does involve cloning the OpType in copy_descendants
followed by applying the substitution (which is kinda a copy itself). So we could
- decide this bit of extra copying isn't important
- make substitution destructive (mutating). This would be perhaps the biggest change.
- pass a
impl Fn(&OpType) -> OpType
intocopy_descendants
. Seems a little bit arbitrary (shouldn't we also have callbacks to copy metadata, etc....) but I think it's reasonable to argue that we should have this one. What do you think?
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.
In fact on the third I am tempted to make copy_descendants
take an Option<Substitution>
rather than a generic callback. Substitution is pretty fundamental and it avoids the argument that we should also do the same thing for e.g. metadata ;)
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 don't think the copying is important.
I do like passing an optional substitution, this is very nice.
It seems like a bit of a weird inter-module dependency, this is very low level, substitution pretty high level. But this is just a vibe.
Including adding a new method for copying descendants of another node (preserving edges incoming to that subtree).
The choice of copying descendants rather than a subtree and its root may seem a little strange. The minor difference is that in most usecases (others are peeling tail loops, and monomorphization which can be refactored to use this too) we're gonna update the root node optype too, so we might as well just make the new optype first rather than copy+overwrite. The bigger differences are
closes #1833, #1886