Skip to main content

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, &REGS)
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}