Skip to content

Commit 86564e5

Browse files
authored
feat: add hugr-core StaticGraph, deprecate hugr-passes CallGraph (#2698)
Respin of #2531, marking the node/edge enums in the new StaticGraph as `non-exhaustive` and adding Consts + LoadConstants. (Lukas originally planned to make something like this to reflect references from Terms to functions, and non-exhaustive allows extending this StaticGraph for that purpose - if we do go that way, however, I have a different plan, can describe if relevant. Rather I think non-exhaustive is just a reasonable allowance for future extension to new use-cases hopefully without damaging existing use-cases.) This is taken from #2555, i.e. which uses it (although there it is still called CallGraph in hugr-core). Naming: could be ModuleGraph ?? Other ideas welcome. I will add a test with some Consts + use of `out_edges` (tho will be supplied in #2555 !)
1 parent cad2f55 commit 86564e5

File tree

6 files changed

+237
-30
lines changed

6 files changed

+237
-30
lines changed

hugr-core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub mod extension;
1717
pub mod hugr;
1818
pub mod import;
1919
pub mod macros;
20+
pub mod module_graph;
2021
pub mod ops;
2122
pub mod package;
2223
pub mod std_extensions;

hugr-core/src/module_graph.rs

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
//! Data structure summarizing static nodes of a Hugr and their uses
2+
use std::collections::HashMap;
3+
4+
use crate::{HugrView, Node, core::HugrNode, ops::OpType};
5+
use petgraph::{Graph, visit::EdgeRef};
6+
7+
/// Weight for an edge in a [`ModuleGraph`]
8+
#[derive(Clone, Debug, PartialEq, Eq)]
9+
#[non_exhaustive]
10+
pub enum StaticEdge<N = Node> {
11+
/// Edge corresponds to a [Call](OpType::Call) node (specified) in the Hugr
12+
Call(N),
13+
/// Edge corresponds to a [`LoadFunction`](OpType::LoadFunction) node (specified) in the Hugr
14+
LoadFunction(N),
15+
/// Edge corresponds to a [LoadConstant](OpType::LoadConstant) node (specified) in the Hugr
16+
LoadConstant(N),
17+
}
18+
19+
/// Weight for a petgraph-node in a [`ModuleGraph`]
20+
#[derive(Clone, Debug, PartialEq, Eq)]
21+
#[non_exhaustive]
22+
pub enum StaticNode<N = Node> {
23+
/// petgraph-node corresponds to a [`FuncDecl`](OpType::FuncDecl) node (specified) in the Hugr
24+
FuncDecl(N),
25+
/// petgraph-node corresponds to a [`FuncDefn`](OpType::FuncDefn) node (specified) in the Hugr
26+
FuncDefn(N),
27+
/// petgraph-node corresponds to the [HugrView::entrypoint], that is not
28+
/// a [`FuncDefn`](OpType::FuncDefn). Note that it will not be a [Module](OpType::Module)
29+
/// either, as such a node could not have edges, so is not represented in the petgraph.
30+
NonFuncEntrypoint,
31+
/// petgraph-node corresponds to a constant; will have no outgoing edges, and incoming
32+
/// edges will be [StaticEdge::LoadConstant]
33+
Const(N),
34+
}
35+
36+
/// Details the [`FuncDefn`]s, [`FuncDecl`]s and module-level [`Const`]s in a Hugr,
37+
/// in a Hugr, along with the [`Call`]s, [`LoadFunction`]s, and [`LoadConstant`]s connecting them.
38+
///
39+
/// Each node in the `ModuleGraph` corresponds to a module-level function or const;
40+
/// each edge corresponds to a use of the target contained in the edge's source.
41+
///
42+
/// For Hugrs whose entrypoint is neither a [Module](OpType::Module) nor a [`FuncDefn`],
43+
/// the static graph will have an additional [`StaticNode::NonFuncEntrypoint`]
44+
/// corresponding to the Hugr's entrypoint, with no incoming edges.
45+
///
46+
/// [`Call`]: OpType::Call
47+
/// [`Const`]: OpType::Const
48+
/// [`FuncDecl`]: OpType::FuncDecl
49+
/// [`FuncDefn`]: OpType::FuncDefn
50+
/// [`LoadConstant`]: OpType::LoadConstant
51+
/// [`LoadFunction`]: OpType::LoadFunction
52+
pub struct ModuleGraph<N = Node> {
53+
g: Graph<StaticNode<N>, StaticEdge<N>>,
54+
node_to_g: HashMap<N, petgraph::graph::NodeIndex<u32>>,
55+
}
56+
57+
impl<N: HugrNode> ModuleGraph<N> {
58+
/// Makes a new `ModuleGraph` for a Hugr.
59+
pub fn new(hugr: &impl HugrView<Node = N>) -> Self {
60+
let mut g = Graph::default();
61+
let mut node_to_g = hugr
62+
.children(hugr.module_root())
63+
.filter_map(|n| {
64+
let weight = match hugr.get_optype(n) {
65+
OpType::FuncDecl(_) => StaticNode::FuncDecl(n),
66+
OpType::FuncDefn(_) => StaticNode::FuncDefn(n),
67+
OpType::Const(_) => StaticNode::Const(n),
68+
_ => return None,
69+
};
70+
Some((n, g.add_node(weight)))
71+
})
72+
.collect::<HashMap<_, _>>();
73+
if !hugr.entrypoint_optype().is_module() && !node_to_g.contains_key(&hugr.entrypoint()) {
74+
node_to_g.insert(hugr.entrypoint(), g.add_node(StaticNode::NonFuncEntrypoint));
75+
}
76+
for (func, cg_node) in &node_to_g {
77+
traverse(hugr, *cg_node, *func, &mut g, &node_to_g);
78+
}
79+
fn traverse<N: HugrNode>(
80+
h: &impl HugrView<Node = N>,
81+
enclosing_func: petgraph::graph::NodeIndex<u32>,
82+
node: N, // Nonstrict-descendant of `enclosing_func``
83+
g: &mut Graph<StaticNode<N>, StaticEdge<N>>,
84+
node_to_g: &HashMap<N, petgraph::graph::NodeIndex<u32>>,
85+
) {
86+
for ch in h.children(node) {
87+
traverse(h, enclosing_func, ch, g, node_to_g);
88+
let weight = match h.get_optype(ch) {
89+
OpType::Call(_) => StaticEdge::Call(ch),
90+
OpType::LoadFunction(_) => StaticEdge::LoadFunction(ch),
91+
OpType::LoadConstant(_) => StaticEdge::LoadConstant(ch),
92+
_ => continue,
93+
};
94+
if let Some(target) = h.static_source(ch) {
95+
if h.get_parent(target) == Some(h.module_root()) {
96+
g.add_edge(enclosing_func, node_to_g[&target], weight);
97+
} else {
98+
assert!(!node_to_g.contains_key(&target));
99+
assert!(h.get_optype(ch).is_load_constant());
100+
assert!(h.get_optype(target).is_const());
101+
}
102+
}
103+
}
104+
}
105+
ModuleGraph { g, node_to_g }
106+
}
107+
108+
/// Allows access to the petgraph
109+
#[must_use]
110+
pub fn graph(&self) -> &Graph<StaticNode<N>, StaticEdge<N>> {
111+
&self.g
112+
}
113+
114+
/// Convert a Hugr [Node] into a petgraph node index.
115+
/// Result will be `None` if `n` is not a [`FuncDefn`](OpType::FuncDefn),
116+
/// [`FuncDecl`](OpType::FuncDecl) or the [HugrView::entrypoint].
117+
pub fn node_index(&self, n: N) -> Option<petgraph::graph::NodeIndex<u32>> {
118+
self.node_to_g.get(&n).copied()
119+
}
120+
121+
/// Returns an iterator over the out-edges from the given Node, i.e.
122+
/// edges to the functions/constants called/loaded by it.
123+
///
124+
/// If the node is not recognised as a function or the entrypoint,
125+
/// for example if it is a [`Const`](OpType::Const), the iterator will be empty.
126+
pub fn out_edges(&self, n: N) -> impl Iterator<Item = (&StaticEdge<N>, &StaticNode<N>)> {
127+
let g = self.graph();
128+
self.node_index(n).into_iter().flat_map(move |n| {
129+
self.graph().edges(n).map(|e| {
130+
(
131+
g.edge_weight(e.id()).unwrap(),
132+
g.node_weight(e.target()).unwrap(),
133+
)
134+
})
135+
})
136+
}
137+
138+
/// Returns an iterator over the in-edges to the given Node, i.e.
139+
/// edges from the (necessarily) functions that call/load it.
140+
///
141+
/// If the node is not recognised as a function or constant,
142+
/// for example if it is a non-function entrypoint, the iterator will be empty.
143+
pub fn in_edges(&self, n: N) -> impl Iterator<Item = (&StaticNode<N>, &StaticEdge<N>)> {
144+
let g = self.graph();
145+
self.node_index(n).into_iter().flat_map(move |n| {
146+
self.graph()
147+
.edges_directed(n, petgraph::Direction::Incoming)
148+
.map(|e| {
149+
(
150+
g.node_weight(e.source()).unwrap(),
151+
g.edge_weight(e.id()).unwrap(),
152+
)
153+
})
154+
})
155+
}
156+
}
157+
158+
#[cfg(test)]
159+
mod test {
160+
use itertools::Itertools as _;
161+
162+
use crate::builder::{
163+
Container, Dataflow, DataflowSubContainer, HugrBuilder, ModuleBuilder, endo_sig, inout_sig,
164+
};
165+
use crate::extension::prelude::{ConstUsize, usize_t};
166+
use crate::ops::{Value, handle::NodeHandle};
167+
168+
use super::*;
169+
170+
#[test]
171+
fn edges() {
172+
let mut mb = ModuleBuilder::new();
173+
let cst = mb.add_constant(Value::from(ConstUsize::new(42)));
174+
let callee = mb.define_function("callee", endo_sig(usize_t())).unwrap();
175+
let ins = callee.input_wires();
176+
let callee = callee.finish_with_outputs(ins).unwrap();
177+
let mut caller = mb
178+
.define_function("caller", inout_sig(vec![], usize_t()))
179+
.unwrap();
180+
let val = caller.load_const(&cst);
181+
let call = caller.call(callee.handle(), &[], vec![val]).unwrap();
182+
let caller = caller.finish_with_outputs(call.outputs()).unwrap();
183+
let h = mb.finish_hugr().unwrap();
184+
185+
let mg = ModuleGraph::new(&h);
186+
let call_edge = StaticEdge::Call(call.node());
187+
let load_const_edge = StaticEdge::LoadConstant(val.node());
188+
189+
assert_eq!(mg.out_edges(callee.node()).next(), None);
190+
assert_eq!(
191+
mg.in_edges(callee.node()).collect_vec(),
192+
[(&StaticNode::FuncDefn(caller.node()), &call_edge,)]
193+
);
194+
195+
assert_eq!(
196+
mg.out_edges(caller.node()).collect_vec(),
197+
[
198+
(&call_edge, &StaticNode::FuncDefn(callee.node()),),
199+
(&load_const_edge, &StaticNode::Const(cst.node()),)
200+
]
201+
);
202+
assert_eq!(mg.in_edges(caller.node()).next(), None);
203+
204+
assert_eq!(mg.out_edges(cst.node()).next(), None);
205+
assert_eq!(
206+
mg.in_edges(cst.node()).collect_vec(),
207+
[(&StaticNode::FuncDefn(caller.node()), &load_const_edge,)]
208+
);
209+
}
210+
}

hugr-passes/src/dead_code.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ pub enum PreserveNode {
6767
impl PreserveNode {
6868
/// A conservative default for a given node. Just examines the node's [`OpType`]:
6969
/// * Assumes all Calls must be preserved. (One could scan the called `FuncDefn`, but would
70-
/// also need to check for cycles in the [`CallGraph`](super::call_graph::CallGraph).)
70+
/// also need to check for cycles in the [`ModuleGraph`](hugr_core::module_graph::ModuleGraph).)
7171
/// * Assumes all CFGs must be preserved. (One could, for example, allow acyclic
7272
/// CFGs to be removed.)
7373
/// * Assumes all `TailLoops` must be preserved. (One could, for example, use dataflow

hugr-passes/src/dead_funcs.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::collections::HashSet;
55
use hugr_core::{
66
HugrView, Node,
77
hugr::hugrmut::HugrMut,
8+
module_graph::{ModuleGraph, StaticNode},
89
ops::{OpTag, OpTrait},
910
};
1011
use petgraph::visit::{Dfs, Walker};
@@ -14,8 +15,6 @@ use crate::{
1415
composable::{ValidatePassError, validate_if_test},
1516
};
1617

17-
use super::call_graph::{CallGraph, CallGraphNode};
18-
1918
#[derive(Debug, thiserror::Error)]
2019
#[non_exhaustive]
2120
/// Errors produced by [`RemoveDeadFuncsPass`].
@@ -31,7 +30,7 @@ pub enum RemoveDeadFuncsError<N = Node> {
3130
}
3231

3332
fn reachable_funcs<'a, H: HugrView>(
34-
cg: &'a CallGraph<H::Node>,
33+
cg: &'a ModuleGraph<H::Node>,
3534
h: &'a H,
3635
entry_points: impl IntoIterator<Item = H::Node>,
3736
) -> impl Iterator<Item = H::Node> + 'a {
@@ -41,9 +40,11 @@ fn reachable_funcs<'a, H: HugrView>(
4140
for n in entry_points {
4241
d.stack.push(cg.node_index(n).unwrap());
4342
}
44-
d.iter(g).map(|i| match g.node_weight(i).unwrap() {
45-
CallGraphNode::FuncDefn(n) | CallGraphNode::FuncDecl(n) => *n,
46-
CallGraphNode::NonFuncRoot => h.entrypoint(),
43+
d.iter(g).filter_map(|i| match g.node_weight(i).unwrap() {
44+
StaticNode::FuncDefn(n) | StaticNode::FuncDecl(n) => Some(*n),
45+
StaticNode::NonFuncEntrypoint => Some(h.entrypoint()),
46+
StaticNode::Const(_) => None,
47+
_ => unreachable!(),
4748
})
4849
}
4950

@@ -85,7 +86,7 @@ impl<H: HugrMut<Node = Node>> ComposablePass<H> for RemoveDeadFuncsPass {
8586
}
8687

8788
let mut reachable =
88-
reachable_funcs(&CallGraph::new(hugr), hugr, entry_points).collect::<HashSet<_>>();
89+
reachable_funcs(&ModuleGraph::new(hugr), hugr, entry_points).collect::<HashSet<_>>();
8990
// Also prevent removing the entrypoint itself
9091
let mut n = Some(hugr.entrypoint());
9192
while let Some(n2) = n {

hugr-passes/src/inline_funcs.rs

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
//! Contains a pass to inline calls to selected functions in a Hugr.
22
use std::collections::{HashSet, VecDeque};
33

4-
use hugr_core::hugr::hugrmut::HugrMut;
5-
use hugr_core::hugr::patch::inline_call::InlineCall;
64
use itertools::Itertools;
75
use petgraph::algo::tarjan_scc;
86

9-
use crate::call_graph::{CallGraph, CallGraphNode};
7+
use hugr_core::hugr::{hugrmut::HugrMut, patch::inline_call::InlineCall};
8+
use hugr_core::module_graph::{ModuleGraph, StaticNode};
109

1110
/// Error raised by [inline_acyclic]
1211
#[derive(Clone, Debug, thiserror::Error, PartialEq)]
@@ -26,7 +25,7 @@ pub fn inline_acyclic<H: HugrMut>(
2625
h: &mut H,
2726
call_predicate: impl Fn(&H, H::Node) -> bool,
2827
) -> Result<(), InlineFuncsError> {
29-
let cg = CallGraph::new(&*h);
28+
let cg = ModuleGraph::new(&*h);
3029
let g = cg.graph();
3130
let all_funcs_in_cycles = tarjan_scc(g)
3231
.into_iter()
@@ -37,7 +36,7 @@ pub fn inline_acyclic<H: HugrMut>(
3736
}
3837
}
3938
ns.into_iter().map(|n| {
40-
let CallGraphNode::FuncDefn(fd) = g.node_weight(n).unwrap() else {
39+
let StaticNode::FuncDefn(fd) = g.node_weight(n).unwrap() else {
4140
panic!("Expected only FuncDefns in sccs")
4241
};
4342
*fd
@@ -68,18 +67,17 @@ pub fn inline_acyclic<H: HugrMut>(
6867
mod test {
6968
use std::collections::HashSet;
7069

71-
use hugr_core::core::HugrNode;
72-
use hugr_core::ops::OpType;
7370
use itertools::Itertools;
74-
use petgraph::visit::EdgeRef;
71+
use rstest::rstest;
7572

7673
use hugr_core::HugrView;
7774
use hugr_core::builder::{Dataflow, DataflowSubContainer, HugrBuilder, ModuleBuilder};
75+
use hugr_core::core::HugrNode;
76+
use hugr_core::module_graph::{ModuleGraph, StaticNode};
77+
use hugr_core::ops::OpType;
7878
use hugr_core::{Hugr, extension::prelude::qb_t, types::Signature};
79-
use rstest::rstest;
8079

81-
use crate::call_graph::{CallGraph, CallGraphNode};
82-
use crate::inline_funcs::inline_acyclic;
80+
use super::inline_acyclic;
8381

8482
/// /->-\
8583
/// main -> f g -> b -> c
@@ -156,7 +154,7 @@ mod test {
156154
target_funcs.contains(&tgt)
157155
})
158156
.unwrap();
159-
let cg = CallGraph::new(&h);
157+
let cg = ModuleGraph::new(&h);
160158
for fname in check_not_called {
161159
let fnode = find_func(&h, fname);
162160
let fnode = cg.node_index(fnode).unwrap();
@@ -180,12 +178,8 @@ mod test {
180178
}
181179
}
182180

183-
fn outgoing_calls<N: HugrNode>(cg: &CallGraph<N>, src: N) -> Vec<N> {
184-
let src = cg.node_index(src).unwrap();
185-
cg.graph()
186-
.edges_directed(src, petgraph::Direction::Outgoing)
187-
.map(|e| func_node(cg.graph().node_weight(e.target()).unwrap()))
188-
.collect()
181+
fn outgoing_calls<N: HugrNode>(cg: &ModuleGraph<N>, src: N) -> Vec<N> {
182+
cg.out_edges(src).map(|(_, tgt)| func_node(tgt)).collect()
189183
}
190184

191185
#[test]
@@ -205,17 +199,17 @@ mod test {
205199
}
206200
})
207201
.unwrap();
208-
let cg = CallGraph::new(&h);
202+
let cg = ModuleGraph::new(&h);
209203
// b and then c should have been inlined into g, leaving only cyclic call to f
210204
assert_eq!(outgoing_calls(&cg, g), [find_func(&h, "f")]);
211205
// But c should not have been inlined into b:
212206
assert_eq!(outgoing_calls(&cg, b), [c]);
213207
}
214208

215-
fn func_node<N: Copy>(cgn: &CallGraphNode<N>) -> N {
209+
fn func_node<N: Copy>(cgn: &StaticNode<N>) -> N {
216210
match cgn {
217-
CallGraphNode::FuncDecl(n) | CallGraphNode::FuncDefn(n) => *n,
218-
CallGraphNode::NonFuncRoot => panic!(),
211+
StaticNode::FuncDecl(n) | StaticNode::FuncDefn(n) => *n,
212+
_ => panic!(),
219213
}
220214
}
221215

hugr-passes/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Compilation passes acting on the HUGR program representation.
22
3+
#[deprecated(note = "Use hugr-core::module_graph::ModuleGraph", since = "0.24.1")]
34
pub mod call_graph;
45
pub mod composable;
56
pub use composable::ComposablePass;

0 commit comments

Comments
 (0)