Skip to main content

hugr_passes/
inline_funcs.rs

1//! Contains a pass to inline calls to selected functions in a Hugr.
2use std::collections::{HashSet, VecDeque};
3
4use itertools::Itertools;
5use petgraph::algo::tarjan_scc;
6
7use hugr_core::hugr::{hugrmut::HugrMut, patch::inline_call::InlineCall};
8use hugr_core::module_graph::{ModuleGraph, StaticNode};
9
10/// Error raised by [inline_acyclic]
11#[derive(Clone, Debug, thiserror::Error, PartialEq)]
12#[non_exhaustive]
13#[deprecated(
14    note = "`hugr-passes` is deprecated. Use tket::passes instead",
15    since = "0.26.2"
16)]
17pub enum InlineFuncsError {}
18
19/// Inline (a subset of) [Call]s whose target [FuncDefn]s are not in cycles of the call
20/// graph.
21///
22/// The function `call_predicate` is passed each such [Call] node and can return
23/// `false` to prevent that Call from being inlined. (Note the [Call] may be created as
24/// a result of previous inlinings so may not have existed in the original Hugr).
25///
26/// [Call]: hugr_core::ops::Call
27/// [FuncDefn]: hugr_core::ops::FuncDefn
28#[deprecated(
29    note = "`hugr-passes` is deprecated. Use tket::passes instead",
30    since = "0.26.2"
31)]
32pub fn inline_acyclic<H: HugrMut>(
33    h: &mut H,
34    call_predicate: impl Fn(&H, H::Node) -> bool,
35) -> Result<(), InlineFuncsError> {
36    let cg = ModuleGraph::new(&*h);
37    let g = cg.graph();
38    let all_funcs_in_cycles = tarjan_scc(g)
39        .into_iter()
40        .flat_map(|mut ns| {
41            if let Ok(n) = ns.iter().exactly_one()
42                && g.edges_connecting(*n, *n).next().is_none()
43            {
44                ns.clear(); // Single-node SCC has no self edge, so discard
45            }
46            ns.into_iter().map(|n| {
47                let StaticNode::FuncDefn(fd) = g.node_weight(n).unwrap() else {
48                    panic!("Expected only FuncDefns in sccs")
49                };
50                *fd
51            })
52        })
53        .collect::<HashSet<_>>();
54    let target_funcs: HashSet<H::Node> = h
55        .children(h.module_root())
56        .filter(|n| h.get_optype(*n).is_func_defn() && !all_funcs_in_cycles.contains(n))
57        .collect();
58    let mut q = VecDeque::from([h.entrypoint()]);
59    while let Some(n) = q.pop_front() {
60        if h.get_optype(n).is_call()
61            && let Some(t) = h.static_source(n)
62            && target_funcs.contains(&t)
63            && call_predicate(h, n)
64        {
65            // We've already checked all error conditions
66            h.apply_patch(InlineCall::new(n)).unwrap();
67        }
68        // Traverse children - including any resulting from turning Call into DFG
69        q.extend(h.children(n));
70    }
71    Ok(())
72}
73
74#[cfg(test)]
75mod test {
76    use std::collections::HashSet;
77
78    use itertools::Itertools;
79    use rstest::rstest;
80
81    use hugr_core::HugrView;
82    use hugr_core::builder::{Dataflow, DataflowSubContainer, HugrBuilder, ModuleBuilder};
83    use hugr_core::core::HugrNode;
84    use hugr_core::module_graph::{ModuleGraph, StaticNode};
85    use hugr_core::ops::OpType;
86    use hugr_core::{Hugr, extension::prelude::qb_t, types::Signature};
87
88    use super::inline_acyclic;
89
90    ///          /->-\
91    /// main -> f     g -> b -> c
92    ///        / \-<-/
93    ///       /
94    ///       \-> a -> x
95    fn make_test_hugr() -> Hugr {
96        let sig = || Signature::new_endo([qb_t()]);
97        let mut mb = ModuleBuilder::new();
98        let x = mb.declare("x", sig().into()).unwrap();
99        let a = {
100            let mut fb = mb.define_function("a", sig()).unwrap();
101            let ins = fb.input_wires();
102            let res = fb.call(&x, &[], ins).unwrap();
103            fb.finish_with_outputs(res.outputs()).unwrap()
104        };
105        let c = {
106            let fb = mb.define_function("c", sig()).unwrap();
107            let ins = fb.input_wires();
108            fb.finish_with_outputs(ins).unwrap()
109        };
110        let b = {
111            let mut fb = mb.define_function("b", sig()).unwrap();
112            let ins = fb.input_wires();
113            let res = fb.call(c.handle(), &[], ins).unwrap().outputs();
114            fb.finish_with_outputs(res).unwrap()
115        };
116        let f = mb.declare("f", sig().into()).unwrap();
117        let g = {
118            let mut fb = mb.define_function("g", sig()).unwrap();
119            let ins = fb.input_wires();
120            let c1 = fb.call(&f, &[], ins).unwrap();
121            let c2 = fb.call(b.handle(), &[], c1.outputs()).unwrap();
122            fb.finish_with_outputs(c2.outputs()).unwrap()
123        };
124        let _f = {
125            let mut fb = mb.define_declaration(&f).unwrap();
126            let ins = fb.input_wires();
127            let c1 = fb.call(g.handle(), &[], ins).unwrap();
128            let c2 = fb.call(a.handle(), &[], c1.outputs()).unwrap();
129            fb.finish_with_outputs(c2.outputs()).unwrap()
130        };
131        mb.finish_hugr().unwrap()
132    }
133
134    fn find_func<H: HugrView>(h: &H, name: &str) -> H::Node {
135        h.children(h.module_root())
136            .find(|n| {
137                h.get_optype(*n)
138                    .as_func_defn()
139                    .is_some_and(|fd| fd.func_name() == name)
140            })
141            .unwrap()
142    }
143
144    #[rstest]
145    #[case(["a", "b", "c"], ["a", "b", "c"], [vec!["g", "x"], vec!["f"], vec!["x"], vec![], vec![]])]
146    #[case(["a", "b"], ["a", "b"], [vec!["g", "x"], vec!["f", "c"], vec!["x"], vec!["c"], vec![]])]
147    #[case(["c"], ["c"], [vec!["g", "a"], vec!("f", "b"), vec!["x"], vec![], vec![]])]
148    fn test_inline(
149        #[case] req: impl IntoIterator<Item = &'static str>,
150        #[case] check_not_called: impl IntoIterator<Item = &'static str>,
151        #[case] calls_fgabc: [Vec<&'static str>; 5],
152    ) {
153        let mut h = make_test_hugr();
154        let target_funcs = req
155            .into_iter()
156            .map(|name| find_func(&h, name))
157            .collect::<HashSet<_>>();
158        inline_acyclic(&mut h, |h, call| {
159            let tgt = h.static_source(call).unwrap();
160            // Check the callback is never asked about an impossible inlining
161            assert!(["a", "b", "c"].contains(&func_name(h, tgt).as_str()));
162            target_funcs.contains(&tgt)
163        })
164        .unwrap();
165        let cg = ModuleGraph::new(&h);
166        for fname in check_not_called {
167            let fnode = find_func(&h, fname);
168            let fnode = cg.node_index(fnode).unwrap();
169            assert_eq!(
170                None,
171                cg.graph()
172                    .edges_directed(fnode, petgraph::Direction::Incoming)
173                    .next()
174            );
175        }
176        for (fname, tgts) in ["f", "g", "a", "b", "c"].into_iter().zip_eq(calls_fgabc) {
177            let fnode = find_func(&h, fname);
178            assert_eq!(
179                outgoing_calls(&cg, fnode)
180                    .into_iter()
181                    .map(|n| func_name(&h, n).as_str())
182                    .collect::<HashSet<_>>(),
183                HashSet::from_iter(tgts),
184                "Calls from {fname}"
185            );
186        }
187    }
188
189    fn outgoing_calls<N: HugrNode>(cg: &ModuleGraph<N>, src: N) -> Vec<N> {
190        cg.out_edges(src).map(|(_, tgt)| func_node(tgt)).collect()
191    }
192
193    #[test]
194    fn test_filter_caller() {
195        let mut h = make_test_hugr();
196        let [g, b, c] = ["g", "b", "c"].map(|n| find_func(&h, n));
197        // Inline calls contained within `g`
198        inline_acyclic(&mut h, |h, mut call| {
199            loop {
200                if call == g {
201                    return true;
202                };
203                let Some(parent) = h.get_parent(call) else {
204                    return false;
205                };
206                call = parent;
207            }
208        })
209        .unwrap();
210        let cg = ModuleGraph::new(&h);
211        // b and then c should have been inlined into g, leaving only cyclic call to f
212        assert_eq!(outgoing_calls(&cg, g), [find_func(&h, "f")]);
213        // But c should not have been inlined into b:
214        assert_eq!(outgoing_calls(&cg, b), [c]);
215    }
216
217    fn func_node<N: Copy>(cgn: &StaticNode<N>) -> N {
218        match cgn {
219            StaticNode::FuncDecl(n) | StaticNode::FuncDefn(n) => *n,
220            _ => panic!(),
221        }
222    }
223
224    fn func_name<H: HugrView>(h: &H, n: H::Node) -> &String {
225        match h.get_optype(n) {
226            OpType::FuncDecl(fd) => fd.func_name(),
227            OpType::FuncDefn(fd) => fd.func_name(),
228            _ => panic!(),
229        }
230    }
231}