Skip to content
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

Open
wants to merge 13 commits into
base: main
Choose a base branch
from

Conversation

acl-cqc
Copy link
Contributor

@acl-cqc acl-cqc commented Dec 17, 2024

Closes #1753.

  • Tested by replacing use of remove_polyfuncs in feat!: Add monomorphization pass #1733; remove_polyfuncs is kept but deprecated and untested.
  • Note the 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 as Bfs.

The longer-term plan is to integrate with #1672 by doing dataflow analysis and transforming to remove Calls that are locally/dynamically impossible, which will make more functions statically-unreachable and hence removable by this.

Copy link

codecov bot commented Dec 17, 2024

Codecov Report

Attention: Patch coverage is 93.68421% with 6 lines in your changes missing coverage. Please review.

Project coverage is 86.60%. Comparing base (1f841ae) to head (f8008d9).

Files with missing lines Patch % Lines
hugr-passes/src/call_graph.rs 93.40% 4 Missing and 2 partials ⚠️
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     
Flag Coverage Δ
python 92.42% <ø> (ø)
rust 86.02% <93.68%> (-0.04%) ⬇️

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

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

@acl-cqc acl-cqc changed the title feat: [Slightly WIP] Add CallGraph struct, and dead-function-removal pass feat!: [Slightly WIP] Add CallGraph struct, and dead-function-removal pass Dec 17, 2024
@ss2165
Copy link
Member

ss2165 commented Dec 17, 2024

can we deprecate remove_polyfuncs instead so we can include this in a non-breaking release?

@acl-cqc
Copy link
Contributor Author

acl-cqc commented Dec 17, 2024

can we deprecate remove_polyfuncs instead so we can include this in a non-breaking release?

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 #[deprecated] only on the re-export but that did not generate a warning even when I used the function via the re-export). If I'm doing this wrong @ss2165 then please shout...

@acl-cqc acl-cqc changed the title feat!: [Slightly WIP] Add CallGraph struct, and dead-function-removal pass feat: [Slightly WIP] Add CallGraph struct, and dead-function-removal pass Dec 17, 2024
@acl-cqc acl-cqc changed the title feat: [Slightly WIP] Add CallGraph struct, and dead-function-removal pass feat: Add CallGraph struct, and dead-function-removal pass Dec 17, 2024
@acl-cqc acl-cqc force-pushed the acl/remove_dead_funcs branch from 31a7b44 to 08d9ebc Compare December 17, 2024 18:58
@acl-cqc acl-cqc force-pushed the acl/remove_dead_funcs branch from 08d9ebc to e29ffa2 Compare December 17, 2024 19:02
@acl-cqc acl-cqc marked this pull request as ready for review December 17, 2024 19:11
@acl-cqc acl-cqc requested a review from a team as a code owner December 17, 2024 19:11
@acl-cqc acl-cqc requested a review from tatiana-s December 17, 2024 19:11
Copy link
Contributor

@tatiana-s tatiana-s left a 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.

fn traverse(
h: &impl HugrView,
node: Node,
enclosing: NodeIndex<u32>,
Copy link
Contributor

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?

Copy link
Contributor Author

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

@tatiana-s tatiana-s requested a review from doug-q December 18, 2024 15:39
Comment on lines +87 to +92
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`");
Copy link
Collaborator

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.

Comment on lines +149 to +152
/// * 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
Copy link
Collaborator

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> {
Copy link
Collaborator

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());
Copy link
Collaborator

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(
Copy link
Collaborator

@doug-q doug-q Dec 19, 2024

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 {
Copy link
Collaborator

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))
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should remove dead FuncDecls 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
Copy link
Collaborator

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();
Copy link
Collaborator

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>,
Copy link
Collaborator

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>,
Copy link
Collaborator

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

@doug-q
Copy link
Collaborator

doug-q commented Dec 19, 2024

Did you consider tracking in the Node weight whether it's a FuncDecl or a FuncDefn?

@doug-q doug-q closed this Dec 19, 2024
@doug-q doug-q reopened this Dec 19, 2024
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.

Add a pass to remove "unused" functions
4 participants