rucc_codegen/layout.rs
1//! Putting the blocks in an order, and turning the edges between them into jumps.
2//!
3//! Design: `spec/10-backend.md` section 10.6.
4//!
5//! Up to here a function is a set of blocks and a set of edges, and nothing has said which block
6//! comes first in memory. A machine has no such thing: it runs the instruction after the one it
7//! just ran, so an order is not a presentation detail but the last piece of what the function
8//! means. This is what chooses one, and then writes the jumps that make the edges the order did
9//! not put next to each other still go where they went.
10//!
11//! # What the order is
12//!
13//! Reverse postorder over the CFG, with each block's successors walked in reverse, and anything
14//! unreachable put at the end in block order.
15//!
16//! That is the `-O0` order `spec/10-backend.md` section 10.3 asks for, and it is not arbitrary.
17//! Walking the successors in reverse is what makes the first arm of a branch come out first,
18//! because a depth-first walk finishes its last child first and reverse postorder then puts that
19//! child last. So an `if` with no `else` falls through into its body, and a loop comes out as its
20//! header, its body and then whatever follows it, which is the shape where the back edge is the
21//! only jump in it. The chain construction weighted by block frequency that section 10.6
22//! describes is what replaces this above `-O0`, and it is not written yet.
23//!
24//! Unreachable blocks are laid out rather than deleted. Deleting one is a decision about what the
25//! program does and this pass has no business making it, and a block nothing reaches costs the
26//! bytes it occupies and nothing else.
27//!
28//! # What a block looks like afterwards
29//!
30//! A block still holds where it goes, and it still holds every arm, which is what keeps the
31//! control flow graph readable after this has run. What changes is that the order the arms are in
32//! now means something it did not mean before:
33//!
34//! ```text
35//! no arms it returns
36//! one arm it falls into that block if that block is next, and jumps to it if not
37//! two arms a test and a conditional jump to the first, and the second is always next
38//! ```
39//!
40//! So a jump target is a block without an instruction growing a field for one.
41//! `rucc_mir::InstData` is twenty four bytes by assertion and a block reference does not fit in
42//! it, and every pass over the graph already reads the arms, so putting the target where the
43//! graph already is costs nothing and keeps the two from disagreeing.
44//!
45//! Which arm is which is no longer which way the condition went, because a block that falls into
46//! the arm the condition is true for is a block whose jump has to be taken when it is false. That
47//! is what the two conditional jumps in [`BranchInsts`] are for, and it is why the arms may come
48//! out swapped: what the condition meant is in the opcode afterwards, and what the arms mean is
49//! where the jump goes and what comes next.
50//!
51//! # The block a branch sometimes needs
52//!
53//! A branch whose second arm cannot be laid out next, because both its arms are blocks the walk
54//! has already been to, would need two jumps in one block. Rather than write one, this makes the
55//! block it needs: an empty one on the second edge, laid out immediately after the branch, that
56//! jumps where the edge went. That is exactly the critical edge splitting in [`crate::split`],
57//! done for a different reason, and it costs the same jump the second jump would have cost while
58//! leaving every block with at most one.
59//!
60//! # Why it runs last
61//!
62//! [`crate::finish`] finds the blocks a function returns from by looking for the ones that go
63//! nowhere. Nothing here creates one of those, but everything here reads and writes the arms, and
64//! a pass that reorders them is one nothing before it should be looking at. Running the layout
65//! after the prologue and the epilogue are in is also what makes the epilogue something it can
66//! lay out around rather than something it has to leave room for.
67
68use rucc_base::Interner;
69use rucc_mir as mir;
70use rucc_target::BranchInsts;
71
72/// Puts a function's blocks in an order and writes the jumps that order needs.
73///
74/// Run last, after [`crate::finish`].
75///
76/// # Panics
77///
78/// Panics on a block with more than two successors, which nothing lowers to yet, and on a block
79/// with two whose last instruction is not the conditional branch the target named. Both are a
80/// function that was built wrongly somewhere earlier, and both are worth finding here rather than
81/// as a jump to the wrong place.
82pub fn blocks(func: &mut mir::Func, insts: &BranchInsts, names: &mut Interner) {
83 let mut order = order(func);
84 let mut writer = Writer { func, insts, names };
85 let mut at = 0;
86 while at < order.len() {
87 // A branch that can fall into neither arm asks for a block to put the second jump in, and
88 // that block goes immediately after it, which is where the loop reaches it next.
89 if let Some(bridge) = writer.edges(order[at], order.get(at + 1).copied()) {
90 order.insert(at + 1, bridge);
91 }
92 at += 1;
93 }
94 func.set_block_order(&order);
95}
96
97/// The order the blocks are laid out in, which is every block the function has exactly once.
98fn order(func: &mir::Func) -> Vec<mir::Block> {
99 let mut order = Vec::with_capacity(func.block_count());
100 let mut seen = vec![false; func.block_count()];
101 if let Some(entry) = func.entry() {
102 seen[entry.index()] = true;
103 // The walk is explicit rather than recursive because a function with a hundred thousand
104 // blocks in it is a function somebody generated, and it should compile rather than run out
105 // of stack. Each entry is a block and how many of its arms have been started.
106 let mut stack = vec![(entry, 0usize)];
107 while let Some((block, next)) = stack.pop() {
108 let succs = &func[block].succs;
109 let Some(arm) = succs.len().checked_sub(next + 1) else {
110 order.push(block);
111 continue;
112 };
113 stack.push((block, next + 1));
114 let to = succs[arm].block;
115 if !std::mem::replace(&mut seen[to.index()], true) {
116 stack.push((to, 0));
117 }
118 }
119 order.reverse();
120 }
121 // Whatever the walk did not reach, in the order the blocks were made, which is the only order
122 // there is anything to be said for when nothing goes to any of them.
123 order.extend(func.blocks().filter(|block| !seen[block.index()]));
124 order
125}
126
127/// The one thing that writes an instruction here, over the function it writes into.
128struct Writer<'a> {
129 func: &'a mut mir::Func,
130 insts: &'a BranchInsts,
131 names: &'a mut Interner,
132}
133
134impl Writer<'_> {
135 /// Writes the jumps one block needs, given the block laid out after it, and gives back the
136 /// block that has to go between the two when the branch needed one.
137 fn edges(&mut self, block: mir::Block, next: Option<mir::Block>) -> Option<mir::Block> {
138 match self.func[block].succs.len() {
139 0 => None,
140 1 => {
141 self.one(block, next);
142 None
143 }
144 2 => self.two(block, next),
145 arms => panic!("a block with {arms} arms, and nothing lowers to one"),
146 }
147 }
148
149 /// A block that goes to one place, which either follows it or has to be jumped to.
150 fn one(&mut self, block: mir::Block, next: Option<mir::Block>) {
151 if Some(self.func[block].succs[0].block) == next {
152 return;
153 }
154 let opcode = self.opcode(self.insts.jump);
155 self.func.build(block, opcode).finish();
156 }
157
158 /// A block that goes to two places, which is a test and a jump to one of them.
159 ///
160 /// The condition is read off the branch the rules selected and the branch is taken out, so the
161 /// register the test reads is the one the branch read and no new value is made. That is what
162 /// makes this safe to run after allocation: it writes no register that was not already
163 /// written and it asks for none that was not already asked for.
164 fn two(&mut self, block: mir::Block, next: Option<mir::Block>) -> Option<mir::Block> {
165 let condition = self.take(block);
166
167 // Whichever arm is laid out next is the one the block falls into, and the jump is then
168 // the one taken when the condition sends it the other way. Falling into the arm the
169 // condition is false for leaves the jump taken when it holds, and falling into the arm it
170 // is true for leaves the other jump and the arms the other way round.
171 let arms: Vec<mir::Block> = self.func[block].succs.iter().map(|arm| arm.block).collect();
172 let (name, bridge) = if next == Some(arms[1]) {
173 (self.insts.if_true, None)
174 } else if next == Some(arms[0]) {
175 self.func.succs_mut(block).swap(0, 1);
176 (self.insts.if_false, None)
177 } else {
178 (self.insts.if_true, Some(self.bridge(block)))
179 };
180
181 let opcode = self.opcode(self.insts.test);
182 self.func.build(block, opcode).operand(condition).finish();
183 let opcode = self.opcode(name);
184 self.func.build(block, opcode).finish();
185 bridge
186 }
187
188 /// Takes the conditional branch off the end of a block and gives back what it read.
189 fn take(&mut self, block: mir::Block) -> mir::Operand {
190 let branch = self.func.terminator(block).expect("a block with two arms has a branch");
191 let cond = self.opcode(self.insts.cond);
192 assert_eq!(
193 self.func[branch].opcode, cond,
194 "a block with two arms whose last instruction is not the branch"
195 );
196 let operands = self.func[branch].operands;
197 let condition = self.func[operands][0];
198 self.func.remove_inst(branch);
199 condition
200 }
201
202 /// Puts an empty block on a branch's second edge, so that the branch has something to fall
203 /// into and the jump the edge really needs is in a block of its own.
204 fn bridge(&mut self, block: mir::Block) -> mir::Block {
205 let bridge = self.func.create_block();
206 let edge = self.func[block].succs[1].clone();
207 *self.func.succs_mut(bridge) = vec![edge];
208 self.func.succs_mut(block)[1] = mir::BlockCall::to(bridge);
209 bridge
210 }
211
212 /// The opcode of that name on this target, which is the name with the target's prefix in
213 /// front of it.
214 fn opcode(&mut self, name: &str) -> mir::Opcode {
215 mir::Opcode::new(self.names.intern(&format!("{}{name}", self.insts.prefix)))
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use rucc_mir::{BlockCall, Opcode, Operand, Reg};
222 use rucc_target::x86_64::{BRANCH, GPR, RAX, REGS};
223
224 use super::*;
225
226 /// A function with that many blocks, none of which goes anywhere yet.
227 fn blank(count: usize) -> (Interner, mir::Func, Vec<mir::Block>) {
228 let mut names = Interner::new();
229 let mut func = mir::Func::new(names.intern("f"));
230 let blocks = (0..count).map(|_| func.create_block()).collect();
231 (names, func, blocks)
232 }
233
234 /// Puts a conditional branch at the end of a block, on a register that is already physical
235 /// the way one is by the time this pass runs.
236 fn branch(func: &mut mir::Func, names: &mut Interner, block: mir::Block, arms: &[mir::Block]) {
237 let opcode = Opcode::new(names.intern("x64.br_cond_8"));
238 func.build(block, opcode).operand(Operand::read(Reg::physical(RAX), GPR)).finish();
239 *func.succs_mut(block) = arms.iter().map(|&arm| BlockCall::to(arm)).collect();
240 }
241
242 /// Laying the blocks out for the one machine this crate has, and the dump of what came out.
243 ///
244 /// The dump rather than the function, because where a jump goes is on the block and the dump
245 /// is the one place the instruction and the arm are put back together. A test that read the
246 /// two separately would pass on a function whose jump and whose edge disagreed, which is the
247 /// mistake this pass is most able to make.
248 ///
249 /// A block is named in the dump by where it is in the layout rather than by the number it was
250 /// made with, which is why every expectation below reads that way and why the order is worth
251 /// asserting on its own.
252 fn laid_out(func: &mut mir::Func, names: &mut Interner) -> Vec<String> {
253 blocks(func, &BRANCH, names);
254 mir::print_func(func, names, ®S)
255 .lines()
256 .filter(|line| !line.trim().is_empty() && !line.starts_with("mfunc") && *line != "}")
257 .map(|line| line.trim().to_string())
258 .collect()
259 }
260
261 /// The blocks in layout order, by the number each was made with.
262 fn order_of(func: &mir::Func) -> Vec<usize> {
263 func.blocks().map(mir::Block::index).collect()
264 }
265
266 #[test]
267 fn a_block_that_falls_into_the_next_one_gets_no_jump_at_all() {
268 let (mut names, mut func, made) = blank(2);
269 *func.succs_mut(made[0]) = vec![BlockCall::to(made[1])];
270
271 let text = laid_out(&mut func, &mut names);
272
273 // The arm is still on the block, because the graph is still worth reading, and there is
274 // no instruction on it because the block it goes to is the one that runs next anyway.
275 assert_eq!(text, ["block0:", "block1", "block1:"]);
276 }
277
278 #[test]
279 fn a_block_that_goes_somewhere_that_is_not_next_gets_a_jump() {
280 let (mut names, mut func, made) = blank(2);
281 // A loop with nothing in it and no way out, which is the smallest function there is with
282 // an edge that runs backwards. Every layout puts the two blocks in this order, so the
283 // second one has nothing after it and its edge has to be a jump.
284 *func.succs_mut(made[0]) = vec![BlockCall::to(made[1])];
285 *func.succs_mut(made[1]) = vec![BlockCall::to(made[0])];
286
287 let text = laid_out(&mut func, &mut names);
288
289 assert_eq!(text, ["block0:", "block1", "block1:", "x64.jmp block0"]);
290 }
291
292 #[test]
293 fn a_branch_that_falls_into_its_false_arm_jumps_when_the_condition_holds() {
294 let (mut names, mut func, made) = blank(3);
295 // A loop whose body is the block it came from: the arm taken when the condition holds is
296 // a block the walk has already been to, so the other arm is what comes next.
297 *func.succs_mut(made[0]) = vec![BlockCall::to(made[1])];
298 branch(&mut func, &mut names, made[1], &[made[0], made[2]]);
299
300 let text = laid_out(&mut func, &mut names);
301
302 assert_eq!(order_of(&func), [0, 1, 2]);
303 assert_eq!(
304 text,
305 [
306 "block0:",
307 "block1",
308 "block1:",
309 "x64.test_rr_8 $rax",
310 "x64.jcc_ne block0, block2",
311 "block2:",
312 ]
313 );
314 }
315
316 #[test]
317 fn a_branch_that_falls_into_its_true_arm_jumps_when_the_condition_does_not_hold() {
318 let (mut names, mut func, made) = blank(3);
319 branch(&mut func, &mut names, made[0], &[made[1], made[2]]);
320
321 let text = laid_out(&mut func, &mut names);
322
323 // The arms come out swapped, because after this the first is where the jump goes and the
324 // second is what runs next, and the jump is the one taken when the condition failed.
325 assert_eq!(order_of(&func), [0, 1, 2]);
326 assert_eq!(
327 text,
328 ["block0:", "x64.test_rr_8 $rax", "x64.jcc_e block2, block1", "block1:", "block2:"]
329 );
330 }
331
332 #[test]
333 fn a_branch_that_can_fall_into_neither_arm_is_given_a_block_to_jump_from() {
334 let (mut names, mut func, made) = blank(2);
335 // A loop that goes back to the top or round again, so both arms are blocks the walk has
336 // already been to and nothing is left to lay out after it.
337 *func.succs_mut(made[0]) = vec![BlockCall::to(made[1])];
338 branch(&mut func, &mut names, made[1], &[made[0], made[1]]);
339
340 let text = laid_out(&mut func, &mut names);
341
342 // Block two is the one this made. It is empty, it is laid out where the branch falls into
343 // it, and the jump the second arm needed is in it rather than being a second jump in the
344 // block above.
345 assert_eq!(order_of(&func), [0, 1, 2]);
346 assert_eq!(
347 text,
348 [
349 "block0:",
350 "block1",
351 "block1:",
352 "x64.test_rr_8 $rax",
353 "x64.jcc_ne block0, block2",
354 "block2:",
355 "x64.jmp block1",
356 ]
357 );
358 }
359
360 #[test]
361 fn the_test_reads_the_register_the_branch_read() {
362 let (mut names, mut func, made) = blank(3);
363 branch(&mut func, &mut names, made[0], &[made[1], made[2]]);
364
365 blocks(&mut func, &BRANCH, &mut names);
366
367 let test = func.insts(made[0]).next().expect("a test");
368 let operands = func[test].operands;
369 assert_eq!(func[operands], [Operand::read(Reg::physical(RAX), GPR)]);
370 }
371
372 #[test]
373 fn a_block_nothing_reaches_is_laid_out_at_the_end_rather_than_deleted() {
374 let (mut names, mut func, made) = blank(4);
375 *func.succs_mut(made[0]) = vec![BlockCall::to(made[3])];
376
377 blocks(&mut func, &BRANCH, &mut names);
378
379 // Blocks one and two are reached by nothing, so they go last, in the order they were
380 // made. Deleting one would be a decision about what the program does, and this pass has
381 // no business making it.
382 assert_eq!(order_of(&func), [0, 3, 1, 2]);
383 }
384
385 #[test]
386 fn a_function_with_no_blocks_is_left_alone() {
387 let mut names = Interner::new();
388 let mut func = mir::Func::new(names.intern("f"));
389
390 blocks(&mut func, &BRANCH, &mut names);
391
392 assert_eq!(func.block_count(), 0);
393 }
394
395 #[test]
396 #[should_panic(expected = "a block with 3 arms")]
397 fn a_block_with_three_arms_is_refused_rather_than_laid_out_wrongly() {
398 let (mut names, mut func, made) = blank(4);
399 branch(&mut func, &mut names, made[0], &[made[1], made[2], made[3]]);
400
401 blocks(&mut func, &BRANCH, &mut names);
402 }
403
404 #[test]
405 #[should_panic(expected = "whose last instruction is not the branch")]
406 fn a_block_with_two_arms_and_no_branch_in_it_is_refused() {
407 let (mut names, mut func, made) = blank(3);
408 let opcode = Opcode::new(names.intern("x64.nop"));
409 func.build(made[0], opcode).finish();
410 *func.succs_mut(made[0]) = vec![BlockCall::to(made[1]), BlockCall::to(made[2])];
411
412 blocks(&mut func, &BRANCH, &mut names);
413 }
414}