-
Notifications
You must be signed in to change notification settings - Fork 7
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: Add CallGraph struct, and dead-function-removal pass #1796
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 #1796 +/- ##
==========================================
- Coverage 86.63% 86.60% -0.04%
==========================================
Files 191 192 +1
Lines 34738 34830 +92
Branches 31571 31663 +92
==========================================
+ Hits 30097 30166 +69
- Misses 2940 2961 +21
- Partials 1701 1703 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
can we deprecate remove_polyfuncs instead so we can include this in a non-breaking release? |
…and suppress warning on re-export
Yes, well, we can, and I have, but I admit I am not singing the praises of the tooling here - deprecate has a "since" tag for tools that might use it, but IIUC I have to guess what the next version number will be (i.e. the version in which it'll be deprecated); and the re-export causes problems (I have to suppress a warning there - I tried putting the |
31a7b44
to
08d9ebc
Compare
08d9ebc
to
e29ffa2
Compare
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.
Generally looks good to me, just one comment mainly for my understanding.
hugr-passes/src/call_graph.rs
Outdated
fn traverse( | ||
h: &impl HugrView, | ||
node: Node, | ||
enclosing: NodeIndex<u32>, |
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.
Just for clarification - is enclosing
the node in the call graph corresponding to the node node
in the hugr or something else?
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.
It's the (callgraph representation of) the nearest enclosing FuncDefn, so maybe the node's parent or an ancestor. I'll rename...
roots.extend(h.children(h.root()).filter(|n| { | ||
h.get_optype(*n) | ||
.as_func_defn() | ||
.is_some_and(|fd| fd.name == "main") | ||
})); | ||
assert_eq!(roots.len(), 1, "No entry_points for Module and no `main`"); |
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 think this should be the responsibility of the caller.
I think panicking here is a bit much, and that it would be better to just delete everything.
This means we panic on an empty module (e.g. Hugr::default() is an empty module). One can imagine that linker might invoke this pass at some point, when deleting everything would be fine.
/// * If the Hugr is non-Module-rooted and `entry_points` is non-empty | ||
/// * If the Hugr is Module-rooted, but does not declare `main`, and `entry_points` is empty | ||
/// * If the Hugr is Module-rooted, and `entry_points` is non-empty but contains nodes that | ||
/// are not [FuncDefn](OpType::FuncDefn)s |
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.
As above, I don't think the second should panic.
I think the interface would be cleaner if entry_points
, maybe with a different name: must be FuncDefn
or FuncDecl
nodes that are immediate children of the root.
Now the first panic goes away, and the third would be an error with the offending nodes.
} | ||
|
||
/// Runs the pass (see [remove_dead_funcs]) with this configuration | ||
pub fn run<H: HugrMut>(&self, hugr: &mut H) -> Result<(), ValidatePassError> { |
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 think it would be better to declare a new error type with [non_exhaustive]
so we have the freedom to add variants later without breaking the interface.
assert_eq!(roots.len(), 1, "No entry_points for Module and no `main`"); | ||
} | ||
let mut roots = roots.into_iter().map(|i| cg.node_to_g.get(&i).unwrap()); | ||
let mut b = Bfs::new(&cg.g, *roots.next().unwrap()); |
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.
Why Bfs? I think Dfs would be marginally better because more memory efficient
|
||
/// Adds new entry points for a Module-rooted Hugr, | ||
/// i.e., FuncDefns that are children of the root. | ||
pub fn with_module_entry_points( |
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.
If you accept my suggestions on the entry_points
interface this should get a new name
|
||
#[derive(Debug, Clone, Default)] | ||
/// A configuration for the Dead Function Removal pass. | ||
pub struct RemoveDeadFuncsPass { |
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 prefer for this to live in a new module. CallGraph is more general than this.
let reachable = reachable_funcs(&CallGraph::new(h), h, entry_points).collect::<HashSet<_>>(); | ||
let unreachable = h | ||
.nodes() | ||
.filter(|n| h.get_optype(*n).is_func_defn() && !reachable.contains(n)) |
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.
We should remove dead FuncDecl
s too
/// are not [FuncDefn](OpType::FuncDefn)s | ||
pub fn remove_dead_funcs(h: &mut impl HugrMut, entry_points: impl IntoIterator<Item = Node>) { | ||
let reachable = reachable_funcs(&CallGraph::new(h), h, entry_points).collect::<HashSet<_>>(); | ||
let unreachable = h |
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 think this definition of unreachable includes the root of a FuncDefn-rooted hugr? Surely worth a test case
h: &impl HugrView, | ||
entry_points: impl IntoIterator<Item = Node>, | ||
) -> impl Iterator<Item = Node> + 'a { | ||
let mut roots = entry_points.into_iter().collect_vec(); |
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.
It's a little thing, but if we didn't have to materialize this vec then the whole algorithm would stream. I think that would involve having to silently ignore any non-func entry_points, or at least you'd have to work hard to track them. probably not worth it.
/// A configuration for the Dead Function Removal pass. | ||
pub struct RemoveDeadFuncsPass { | ||
validation: ValidationLevel, | ||
entry_points: Vec<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.
Did you consider making this a HashSet? it would make with_module_entry_points
truly idempotent.
/// [FuncDefn]: OpType::FuncDefn | ||
/// [LoadFunction]: OpType::LoadFunction | ||
pub struct CallGraph { | ||
g: Graph<Node, CallGraphEdge>, |
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.
let's add a getter for this
Did you consider tracking in the Node weight whether it's a FuncDecl or a FuncDefn? |
Closes #1753.
remove_polyfuncs
in feat!: Addmonomorphization
pass #1733;remove_polyfuncs
is kept but deprecated and untested.CallGraph
struct has no public accessors/etc. yet. I propose to leave this until we have more uses, as I found it hard to expose suitable for using petgraph utilities such asBfs
.The longer-term plan is to integrate with #1672 by doing dataflow analysis and transforming to remove
Call
s that are locally/dynamically impossible, which will make more functions statically-unreachable and hence removable by this.