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}