Skip to main content

rucc_opt/
frontier.rs

1//! Where a dominator stops dominating, forwards and backwards.
2//!
3//! Design: `spec/optimizer/06-cfg-and-dominators.md` section 6.3, and document 17 for the
4//! consumer of the backwards half.
5//!
6//! The dominance frontier of a block is where its influence ends. Block `a` is in the frontier
7//! of block `b` when `b` dominates a predecessor of `a` and does not strictly dominate `a`
8//! itself, which is to say `a` is the first place control can arrive without having gone
9//! through `b`. Run the same definition on the graph with every arrow turned around and it
10//! answers a different question: the post-dominance frontier of a block is the set of branches
11//! that decide whether the block runs at all. That is the control dependence relation, and
12//! [`ControlDependence`] is the name it goes by, because that is what a pass asking for it
13//! wants and nobody wants the frontier of a reversed graph for its own sake.
14//!
15//! # Why both are here and why they are one algorithm
16//!
17//! Section 6.3 says the forward frontier is probably not needed, and the reason is sound: the
18//! classical consumer is Cytron's SSA construction, and rucc builds SSA during lowering, so that
19//! consumer does not exist here. The backwards one is needed, by aggressive dead code
20//! elimination in document 17 and by if-conversion in document 22, and it cannot be written
21//! without writing the forward one, because they are the same six lines over two graphs. Given
22//! that, the forward one costs a wrapper and a doc comment, and it buys a test: the two are
23//! checked against a direct reading of the definition, and a mistake in the shared walk shows
24//! up twice rather than once.
25//!
26//! What is deliberately not here is Cytron's iterated frontier. It exists to place phi nodes and
27//! nothing else in this compiler places phi nodes.
28//!
29//! # The invented edges
30//!
31//! [`PostDominators`] adds an edge to the exit from every block that has no path to one, which
32//! is how an infinite loop gets a post-dominator at all. Those edges are in this relation too,
33//! so a block at the far end of an infinite loop can come out control dependent on a branch it
34//! is not really control dependent on. That is the safe direction for every consumer there is:
35//! a pass that keeps something because it might matter is slow, and one that removes an infinite
36//! loop because nothing after it runs is wrong. [`PostDominators::fake_exits`] is public so a
37//! pass that wants to know can ask.
38
39use rucc_ir::Block;
40
41use crate::{Cfg, Dominators, PostDominators};
42
43/// Where each block stops dominating, by block number.
44///
45/// The lists are sorted by block number and hold no duplicates, so two of these compare equal
46/// when they say the same thing, which is what the analysis cache needs of them.
47#[derive(Clone, Debug, Default, PartialEq, Eq)]
48pub struct Frontiers {
49    of: Vec<Vec<Block>>,
50}
51
52impl Frontiers {
53    /// Builds the frontier of every block.
54    ///
55    /// The cost is the size of the answer plus the size of the graph, because the walk from a
56    /// predecessor stops at the immediate dominator of the block it started for, and every step
57    /// it takes writes one entry.
58    #[must_use]
59    pub fn new(cfg: &Cfg, doms: &Dominators) -> Self {
60        let mut of = vec![Vec::new(); cfg.capacity()];
61        for &block in cfg.postorder() {
62            // A block with one way in is not a place two paths meet, and the entry is, because
63            // control also arrives there from outside the function. Leaving that out is the one
64            // mistake in this algorithm that a small test does not catch, since it only shows up
65            // on a loop whose back edge goes to the entry itself.
66            //
67            // A predecessor control never reaches is not a way in. Section 6.5 says such a block
68            // is invisible to every analysis, and counting one here would put a frontier on a
69            // block that has no dominators for the walk to climb.
70            let arriving = || cfg.predecessors(block).iter().copied().filter(|&p| cfg.reaches(p));
71            let arrivals = arriving().count() + usize::from(Some(block) == cfg.entry());
72            if arrivals < 2 {
73                continue;
74            }
75            let stop = doms.immediate_dominator(block);
76            for pred in arriving() {
77                let mut runner = pred;
78                while Some(runner) != stop {
79                    of[runner.index()].push(block);
80                    match doms.immediate_dominator(runner) {
81                        Some(next) => runner = next,
82                        // The entry, which happens when `stop` is `None` because `block` is the
83                        // entry as well. The entry is in its own frontier there and that is the
84                        // right answer, since it does not strictly dominate itself.
85                        None => break,
86                    }
87                }
88            }
89        }
90        for list in &mut of {
91            list.sort_unstable_by_key(|b: &Block| b.index());
92            list.dedup();
93        }
94        Self { of }
95    }
96
97    /// The blocks control can first arrive at without having gone through this one.
98    ///
99    /// Empty for a block the entry does not reach, and for one whose influence covers everything
100    /// below it, which is every block on a path with no branches.
101    #[must_use]
102    pub fn of(&self, block: Block) -> &[Block] {
103        self.of.get(block.index()).map_or(&[][..], Vec::as_slice)
104    }
105}
106
107/// Which branches decide whether a block runs.
108///
109/// This is the post-dominance frontier under the name a pass would look for. A block is control
110/// dependent on a branch when one arm of the branch always reaches it and the other does not
111/// have to, so the branch is what a pass has to keep in order to keep the block meaningful.
112#[derive(Clone, Debug, Default, PartialEq, Eq)]
113pub struct ControlDependence {
114    on: Vec<Vec<Block>>,
115}
116
117impl ControlDependence {
118    /// Builds the relation for every block.
119    #[must_use]
120    pub fn new(cfg: &Cfg, post: &PostDominators) -> Self {
121        let mut on = vec![Vec::new(); cfg.capacity()];
122        for &block in cfg.postorder() {
123            // The same join test as above, on the reversed graph, where what arrives at a block
124            // is what the block branches to. A block with two successors is a branch, and a
125            // block with an invented edge to the exit counts that edge, which is what puts an
126            // infinite loop in this relation at all.
127            let invented = post.fake_exits().contains(&block) || cfg.successors(block).is_empty();
128            let arrivals = cfg.successors(block).len() + usize::from(invented);
129            if arrivals < 2 {
130                continue;
131            }
132            let stop = post.immediate_post_dominator(block);
133            for &succ in cfg.successors(block) {
134                let mut runner = succ;
135                while Some(runner) != stop {
136                    on[runner.index()].push(block);
137                    match post.immediate_post_dominator(runner) {
138                        Some(next) => runner = next,
139                        // The walk arrived at the exit. Nothing above the exit is a block, so
140                        // there is nothing further to record whatever `stop` was.
141                        None => break,
142                    }
143                }
144            }
145        }
146        for list in &mut on {
147            list.sort_unstable_by_key(|b: &Block| b.index());
148            list.dedup();
149        }
150        Self { on }
151    }
152
153    /// The blocks whose terminator decides whether this one runs.
154    ///
155    /// Empty for a block that runs whenever the function runs, which is the entry and every
156    /// block that post-dominates it.
157    #[must_use]
158    pub fn on(&self, block: Block) -> &[Block] {
159        self.on.get(block.index()).map_or(&[][..], Vec::as_slice)
160    }
161
162    /// Whether this block runs whatever any branch decides.
163    #[must_use]
164    pub fn unconditional(&self, block: Block) -> bool {
165        self.on(block).is_empty()
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::{ControlDependence, Frontiers};
172    use crate::testing::graph;
173    use crate::{Cfg, Dominators, PostDominators};
174    use rucc_ir::{Block, Signature};
175
176    /// The frontier of every block, read straight off the definition.
177    ///
178    /// Quadratic in the number of blocks and cubic with the predecessor walk, which is why it is
179    /// only here. It is the oracle: the algorithm above is an optimization of this and the tests
180    /// hold it to exactly this answer.
181    fn by_definition(cfg: &Cfg, doms: &Dominators) -> Vec<Vec<Block>> {
182        let mut out = vec![Vec::new(); cfg.capacity()];
183        for &of in cfg.postorder() {
184            for &block in cfg.postorder() {
185                // `dominates` is already false for a predecessor nothing reaches, so the filter
186                // the algorithm needs is not written again here.
187                let reaches_a_pred =
188                    cfg.predecessors(block).iter().any(|&pred| doms.dominates(of, pred));
189                if reaches_a_pred && !doms.strictly_dominates(of, block) {
190                    out[of.index()].push(block);
191                }
192            }
193        }
194        for list in &mut out {
195            list.sort_unstable_by_key(|b: &Block| b.index());
196        }
197        out
198    }
199
200    /// The same, for post-dominance, which is the control dependence relation with the two ends
201    /// the other way round.
202    fn control_by_definition(cfg: &Cfg, post: &PostDominators) -> Vec<Vec<Block>> {
203        let mut out = vec![Vec::new(); cfg.capacity()];
204        for &of in cfg.postorder() {
205            for &block in cfg.postorder() {
206                let reaches_a_succ =
207                    cfg.successors(block).iter().any(|&succ| post.post_dominates(of, succ));
208                if reaches_a_succ && !post.strictly_post_dominates(of, block) {
209                    out[of.index()].push(block);
210                }
211            }
212        }
213        for list in &mut out {
214            list.sort_unstable_by_key(|b: &Block| b.index());
215        }
216        out
217    }
218
219    fn frontiers(edges: &[&[usize]]) -> Vec<Vec<usize>> {
220        let func = graph(edges);
221        let cfg = Cfg::new(&func);
222        let doms = Dominators::new(&cfg);
223        let built = Frontiers::new(&cfg, &doms);
224        (0..edges.len())
225            .map(|index| built.of(Block::from_usize(index)).iter().map(|b| b.index()).collect())
226            .collect()
227    }
228
229    fn depends(edges: &[&[usize]]) -> Vec<Vec<usize>> {
230        let func = graph(edges);
231        let cfg = Cfg::new(&func);
232        let post = PostDominators::new(&cfg);
233        let built = ControlDependence::new(&cfg, &post);
234        (0..edges.len())
235            .map(|index| built.on(Block::from_usize(index)).iter().map(|b| b.index()).collect())
236            .collect()
237    }
238
239    #[test]
240    fn a_chain_of_blocks_has_no_frontier_anywhere() {
241        // Nothing joins, so no block ever stops dominating what comes after it.
242        assert_eq!(frontiers(&[&[1], &[2], &[]]), vec![vec![], vec![], vec![]]);
243    }
244
245    #[test]
246    fn the_two_arms_of_a_branch_meet_at_the_block_after_it() {
247        // 0 branches to 1 and 2, both go to 3. Each arm stops mattering at 3 and the branch
248        // itself dominates all of it.
249        let df = frontiers(&[&[1, 2], &[3], &[3], &[]]);
250        assert_eq!(df, vec![vec![], vec![3], vec![3], vec![]]);
251    }
252
253    #[test]
254    fn a_loop_header_is_in_its_own_frontier() {
255        // 0 -> 1, 1 branches to 1 and 2. The body of the loop stops dominating at the header,
256        // which is what makes the header the place a value defined in the body needs a
257        // parameter.
258        let df = frontiers(&[&[1], &[1, 2], &[]]);
259        assert_eq!(df, vec![vec![], vec![1], vec![]]);
260    }
261
262    #[test]
263    fn a_back_edge_to_the_entry_puts_the_entry_in_its_own_frontier() {
264        // The case the arrivals count is written for. Block 0 has one predecessor and is still
265        // a join, because control also arrives from outside the function.
266        let df = frontiers(&[&[1, 2], &[0], &[]]);
267        assert_eq!(df, vec![vec![0], vec![0], vec![]]);
268    }
269
270    #[test]
271    fn the_frontier_is_what_the_definition_says_on_every_graph_the_design_names() {
272        for edges in shapes() {
273            let func = graph(edges);
274            let cfg = Cfg::new(&func);
275            let doms = Dominators::new(&cfg);
276            let built = Frontiers::new(&cfg, &doms);
277            let wanted = by_definition(&cfg, &doms);
278            for &block in cfg.postorder() {
279                assert_eq!(
280                    built.of(block),
281                    wanted[block.index()].as_slice(),
282                    "block {} of {edges:?}",
283                    block.index()
284                );
285            }
286        }
287    }
288
289    #[test]
290    fn control_dependence_is_what_the_definition_says_on_every_graph_the_design_names() {
291        for edges in shapes() {
292            let func = graph(edges);
293            let cfg = Cfg::new(&func);
294            let post = PostDominators::new(&cfg);
295            let built = ControlDependence::new(&cfg, &post);
296            let wanted = control_by_definition(&cfg, &post);
297            for &block in cfg.postorder() {
298                assert_eq!(
299                    built.on(block),
300                    wanted[block.index()].as_slice(),
301                    "block {} of {edges:?}",
302                    block.index()
303                );
304            }
305        }
306    }
307
308    #[test]
309    fn only_the_arms_of_a_branch_depend_on_it() {
310        // 0 branches to 1 and 2, both go to 3. The arms run because of the branch and 3 runs
311        // whatever the branch decided.
312        let cd = depends(&[&[1, 2], &[3], &[3], &[]]);
313        assert_eq!(cd, vec![vec![], vec![0], vec![0], vec![]]);
314    }
315
316    #[test]
317    fn a_block_that_always_runs_depends_on_nothing() {
318        let func = graph(&[&[1], &[2], &[]]);
319        let cfg = Cfg::new(&func);
320        let post = PostDominators::new(&cfg);
321        let cd = ControlDependence::new(&cfg, &post);
322        for index in 0..3 {
323            assert!(cd.unconditional(Block::from_usize(index)), "block {index}");
324        }
325    }
326
327    #[test]
328    fn a_loop_body_depends_on_the_test_that_ends_the_loop() {
329        // 0 -> 1, 1 branches to 2 and 3, 2 -> 1, 3 returns. The body and the latch run because
330        // the test said so, and the test itself is in the loop, so it depends on itself.
331        let cd = depends(&[&[1], &[2, 3], &[1], &[]]);
332        assert_eq!(cd, vec![vec![], vec![1], vec![1], vec![]]);
333    }
334
335    #[test]
336    fn one_arm_falling_through_still_depends_on_the_branch() {
337        // 0 branches to 1 and 2, 1 goes to 2, 2 returns. Nothing joins on the other side, so 1
338        // is the only block the branch decides.
339        let cd = depends(&[&[1, 2], &[2], &[]]);
340        assert_eq!(cd, vec![vec![], vec![0], vec![]]);
341    }
342
343    #[test]
344    fn nothing_after_an_infinite_loop_is_forgotten() {
345        // 0 branches to 1 and 2, 1 loops on itself forever, 2 returns. The loop has no path to
346        // the exit, so post-dominance is only defined for it through an invented edge, and the
347        // relation still has to come out of the walk rather than out of a panic.
348        //
349        // Block 1 depends on itself as well as on the branch, which is the invented edge
350        // showing through: in the reversed graph the loop is a place two ways in meet. That is
351        // the conservative direction and it is what keeps a pass from deleting the loop.
352        let func = graph(&[&[1, 2], &[1], &[]]);
353        let cfg = Cfg::new(&func);
354        let post = PostDominators::new(&cfg);
355        assert_eq!(post.fake_exits(), [Block::from_usize(1)]);
356        assert_eq!(depends(&[&[1, 2], &[1], &[]]), vec![vec![], vec![0, 1], vec![0]]);
357    }
358
359    #[test]
360    fn a_switch_puts_every_arm_on_the_block_that_chose_it() {
361        // Four ways out of block 0, all meeting at 4.
362        let cd = depends(&[&[1, 2, 3, 4], &[4], &[4], &[4], &[]]);
363        assert_eq!(cd, vec![vec![], vec![0], vec![0], vec![0], vec![]]);
364    }
365
366    #[test]
367    fn a_declaration_has_a_frontier_like_anything_else() {
368        // No blocks, so no answers, and no panic on the way to saying so.
369        let func = rucc_ir::Func::new(rucc_base::Interner::new().intern("f"), Signature::new());
370        let cfg = Cfg::new(&func);
371        let doms = Dominators::new(&cfg);
372        let post = PostDominators::new(&cfg);
373        assert!(Frontiers::new(&cfg, &doms).of(Block::from_usize(0)).is_empty());
374        assert!(ControlDependence::new(&cfg, &post).on(Block::from_usize(0)).is_empty());
375    }
376
377    /// Shapes that between them have a branch, a join, a loop, a switch, an irreducible region,
378    /// a back edge to the entry, an infinite loop and an unreachable block.
379    ///
380    /// The point of the list is that the two exhaustive tests above run over all of it, so a
381    /// shape added here is a shape both relations are checked on.
382    fn shapes() -> Vec<&'static [&'static [usize]]> {
383        vec![
384            &[&[1], &[2], &[]],
385            &[&[1, 2], &[3], &[3], &[]],
386            &[&[1], &[1, 2], &[]],
387            &[&[1, 2], &[0], &[]],
388            &[&[1, 2], &[2], &[]],
389            &[&[1, 2, 3, 4], &[4], &[4], &[4], &[]],
390            &[&[1], &[2, 3], &[1], &[]],
391            // Irreducible: two ways into a two block cycle, so neither block of it dominates
392            // the other.
393            &[&[1, 2], &[2], &[1, 3], &[]],
394            // An unreachable block, which every relation here has to ignore rather than trip
395            // over.
396            &[&[1], &[], &[1]],
397            // Nested branches meeting at two different places.
398            &[&[1, 4], &[2, 3], &[4], &[4], &[]],
399            // An infinite loop, which only has post-dominators through an invented edge.
400            &[&[1, 2], &[1], &[]],
401            // Two of them, so the walk meets more than one invented edge.
402            &[&[1, 2], &[1], &[3, 4], &[3], &[]],
403        ]
404    }
405}