Skip to main content

rucc_codegen/
split.rs

1//! Splitting critical edges, so that every edge that carries values has somewhere to put them.
2//!
3//! Design: `spec/10-backend.md` section 10.4.
4//!
5//! An edge carries values when the block it goes to takes parameters, and giving a parameter its
6//! value is a move. The move has to happen on the edge and not before it or after it, because
7//! before it is a block that goes somewhere else too and after it is a block that is arrived at
8//! from somewhere else too, and in either case the move would run on a path it was not written
9//! for. An edge out of a block with one successor can put its moves at the end of that block,
10//! since every path through it takes the edge. An edge into a block with one predecessor can put
11//! them at the start of that block, for the same reason the other way round. An edge that is
12//! neither, which is what a critical edge is, has neither place, and the allocator says so:
13//! `rucc_regalloc` asserts that it never sees one.
14//!
15//! So one is turned into two. A block with nothing in it goes on the edge, the arguments move on
16//! to the second half, and both halves are now uncritical: the first goes to a block with one
17//! predecessor and the second leaves a block with one successor. Which of the two the moves end
18//! up in is the allocator's answer and not this one's, and either is correct.
19//!
20//! # What it leaves behind
21//!
22//! An empty block, which is a jump to the next thing unless the layout puts it where it falls
23//! through. That is a cost, and it is why an edge with nothing to carry is left alone: there are
24//! no moves to find a place for, so splitting it would buy a jump and nothing else.
25//!
26//! # The other edge with nowhere to put a move
27//!
28//! A computed `goto` leaves its block through a register, and the moves an edge out of it carries
29//! would have to be written somewhere the jump has already gone past. So there is a second pass
30//! here, [`indirect`], which takes the values off those edges and puts them in a block of their
31//! own in front of each label. It runs first, and what it leaves behind is edges the splitting
32//! below then has nothing to do about.
33//!
34//! [`pads`] is here for the same reason and not for a reason of its own: the blocks those labels
35//! begin at are addresses an indirect branch arrives at, and a machine that checks the forward edge
36//! wants a landing pad at every one of them. Which block an address names is settled by the pass
37//! above, so the pad is written after it and not where the prologue's own pad is written.
38
39use std::collections::HashMap;
40
41use rucc_base::Interner;
42use rucc_mir as mir;
43use rucc_target::{BranchInsts, FrameInsts};
44
45/// Splits every critical edge that carries values, and gives back how many it split.
46///
47/// Run after lowering and before allocation. Running it twice is running it once, because the
48/// blocks it adds have one successor each and are never the source of a critical edge.
49pub fn critical(func: &mut mir::Func) -> usize {
50    let preds = preds(func);
51    let blocks: Vec<mir::Block> = func.blocks().collect();
52    let mut split = 0;
53    for block in blocks {
54        if func[block].succs.len() < 2 {
55            continue;
56        }
57        for index in 0..func[block].succs.len() {
58            let call = func[block].succs[index].clone();
59            if call.args.is_empty() || preds[call.block.index()] < 2 {
60                continue;
61            }
62            // The new block is at the end of the layout, which is where a block that is a jump
63            // and nothing else does the least harm before the layout pass has an opinion.
64            //
65            // It runs exactly as often as the edge it sits on is taken, and both halves of that
66            // edge are now that edge, which is why the weight is copied onto all three rather
67            // than left at what a block nobody told anything runs. A block on a cold edge that
68            // claimed to run once per call would be one the layout put in the middle of the hot
69            // path.
70            let weight = call.weight;
71            let half = func.create_block();
72            func.set_weight(half, weight);
73            *func.succs_mut(half) = vec![call];
74            func.succs_mut(block)[index] = mir::BlockCall::to(half).taken(weight);
75            split += 1;
76        }
77    }
78    split
79}
80
81/// Takes the values off every edge out of a computed `goto`, and gives back how many blocks it
82/// made to hold them.
83///
84/// Run after lowering and before [`critical`], which then sees edges with nothing on them and
85/// leaves them alone. Running it twice is running it once, for the reason the splitting above is:
86/// the blocks it adds end in a jump rather than in a branch through a register.
87///
88/// # What is wrong with the edge it takes the values off
89///
90/// Every other edge in the function is out of a block whose last instruction the layout writes, so
91/// an edge that is the only way out of its block can put its moves at the end of that block and
92/// they land in front of the jump. A block that leaves through a register already ends in the jump
93/// when the allocator runs, because where it goes is a value and a value is something selection
94/// reads rather than something the layout knows. Moves at the end of that block would be written
95/// after the jump, where nothing runs them, and moves in front of it would be written across the
96/// register the jump reads, which the allocator believes is dead from the jump onwards and is free
97/// to hand to one of the moves.
98///
99/// So the moves go somewhere else. Each label an indirect branch reaches gets a block in front of
100/// it that carries the values, the branch goes to that block with nothing on the edge, and the
101/// address the `&&label` produces is the address of that block rather than of the label's own. The
102/// new block is arrived at one way and leaves one way, so its own edge has both of the places the
103/// splitting above talks about and the allocator is content.
104///
105/// # One label, one address, and two branches that disagree
106///
107/// A label has one address, so two computed `goto`s that reach it both arrive at whatever block
108/// that address names, and the values they carry are not the same values. One block in front of
109/// the label cannot move two different sets of registers.
110///
111/// So they are made to agree first. Each parameter of the label gets a register of its own, every
112/// branch writes that register in front of its jump, and the block in front of the label carries
113/// those registers and nothing else. That is what gcc does about the same problem, which it calls
114/// coalescing across an abnormal edge, done here rather than while the values are still the
115/// optimizer's.
116///
117/// Writing them in front of the jump is safe, which is not obvious, since a branch that goes five
118/// ways writes the registers of one of those ways on the path to all five. What makes it safe is
119/// that nothing reads those registers except the block in front of the label, and the only way to
120/// reach that block is an edge out of a branch, which writes them on the way. So a value written
121/// here and not used is a value overwritten before anything looks, whichever way the jump went.
122///
123/// # Panics
124///
125/// Panics on a class of register the machine named no move for, which is a function carrying a
126/// value of a kind the target never said how to copy, and on a branch that has lost the terminator
127/// it was found by, which nothing between the finding and the use of it can do. Both are a target
128/// description or a function that was built wrongly, and both are worth finding here rather than as
129/// a value that arrives somewhere it was never written.
130pub fn indirect(
131    func: &mut mir::Func,
132    branch: &BranchInsts,
133    frame: &FrameInsts,
134    names: &mut Interner,
135) -> usize {
136    let jump = mir::Opcode::new(names.intern(&format!("{}{}", branch.prefix, branch.indirect)));
137    let branches: Vec<mir::Block> = func
138        .blocks()
139        .filter(|&block| func.terminator(block).is_some_and(|last| func[last].opcode == jump))
140        .collect();
141    // Nothing at all in almost every function, and the walk at the bottom is over every instruction
142    // in it, so the answer is arrived at here rather than paid for everywhere.
143    if branches.is_empty() {
144        return 0;
145    }
146    // In the order the branches name them rather than in whatever order a hash gives, so that two
147    // runs of the compiler over one program write the same blocks.
148    let mut targets: Vec<mir::Block> = Vec::new();
149    for &block in &branches {
150        for call in &func[block].succs {
151            if !call.args.is_empty() && !targets.contains(&call.block) {
152                targets.push(call.block);
153            }
154        }
155    }
156
157    let mut entries: HashMap<mir::Block, mir::Block> = HashMap::new();
158    for target in targets {
159        let params = func[target].params.clone();
160        let homes: Vec<mir::Reg> = params.iter().map(|param| func.new_vreg(param.class)).collect();
161        let entry = func.create_block();
162        let mut total = mir::Weight::NEVER;
163        for &block in &branches {
164            for index in 0..func[block].succs.len() {
165                if func[block].succs[index].block != target {
166                    continue;
167                }
168                let call = func[block].succs[index].clone();
169                let last = func.terminator(block).expect("a block that ends in a jump");
170                for (home, (arg, param)) in homes.iter().zip(call.args.iter().zip(&params)) {
171                    let name = frame.moves(param.class).expect("a class this machine can move").mov;
172                    let opcode = mir::Opcode::new(names.intern(&format!("{}{name}", frame.prefix)));
173                    let inst = func
174                        .build_loose(opcode)
175                        .def(*home, param.class)
176                        .uses(*arg, param.class)
177                        .finish();
178                    func.insert_before(last, inst);
179                }
180                // The block in front of the label runs as often as every branch that reaches it,
181                // which is the same sum the weight of a block with that many edges into it would
182                // be.
183                total = mir::Weight::parts(total.raw().saturating_add(call.weight.raw()));
184                func.succs_mut(block)[index] = mir::BlockCall::to(entry).taken(call.weight);
185            }
186        }
187        func.set_weight(entry, total);
188        *func.succs_mut(entry) = vec![mir::BlockCall::with(target, homes).taken(total)];
189        entries.insert(target, entry);
190    }
191
192    // And the addresses, which is the half of this that is not about edges. Every `&&label` in the
193    // function names a block, and a label with a block in front of it now begins at that block, so
194    // an address left pointing at the label's own block would be a jump past the moves.
195    let mut addresses: Vec<mir::MemRef> = Vec::new();
196    for block in func.blocks() {
197        for inst in func.insts(block) {
198            if let Some(mem) = func[inst].mem {
199                addresses.push(mem);
200            }
201        }
202    }
203    for mem in addresses {
204        if let Some(named) = func[mem].block {
205            if let Some(&entry) = entries.get(&named) {
206                func[mem].block = Some(entry);
207            }
208        }
209    }
210    // And the names, for the same reason. A block an image points at is one a `goto *p` arrives at,
211    // so a name left on the label's own block would be an address in a table that skips the moves,
212    // which is the one way into the block that would not have made them.
213    for (block, _) in &mut func.labels {
214        if let Some(&entry) = entries.get(block) {
215            *block = entry;
216        }
217    }
218    entries.len()
219}
220
221/// Puts a landing pad at the front of every block whose address is taken, and gives back how many
222/// it wrote.
223///
224/// Run after [`indirect`], because the block an address names is not settled until that has moved
225/// the addresses on to the blocks it made, and only when the command line asked for the forward
226/// edge to be checked. Nothing is written otherwise, which is why the name comes in as an option
227/// and why a target with no such instruction is a target this does nothing on.
228///
229/// The pad a prologue opens with is written elsewhere, in `crate::finish`, because the address it
230/// makes reachable is the address of the function rather than a place inside it. These are the
231/// other addresses an indirect branch may arrive at, and a machine that checks the forward edge
232/// faults on one that has no pad, so a computed `goto` compiled without this would be a program
233/// that ran everywhere except on the hardware the flag was turned on for.
234pub fn pads(
235    func: &mut mir::Func,
236    frame: &FrameInsts,
237    landing: Option<&'static str>,
238    names: &mut Interner,
239) -> usize {
240    let Some(name) = landing else { return 0 };
241    let opcode = mir::Opcode::new(names.intern(&format!("{}{name}", frame.prefix)));
242    let mut addressed: Vec<mir::Block> = Vec::new();
243    for block in func.blocks() {
244        for inst in func.insts(block) {
245            if let Some(mem) = func[inst].mem {
246                if let Some(named) = func[mem].block {
247                    if !addressed.contains(&named) {
248                        addressed.push(named);
249                    }
250                }
251            }
252        }
253    }
254    for &block in &addressed {
255        let inst = func.build_loose(opcode).finish();
256        func.prepend_inst(block, inst);
257    }
258    addressed.len()
259}
260
261/// How many edges arrive at each block, counted by index rather than in layout order so that a
262/// block added while splitting can be looked up in the same table.
263fn preds(func: &mir::Func) -> Vec<usize> {
264    let mut counts = vec![0; func.block_count()];
265    for block in func.blocks() {
266        for call in &func[block].succs {
267            counts[call.block.index()] += 1;
268        }
269    }
270    counts
271}
272
273#[cfg(test)]
274mod tests {
275    use rucc_base::Interner;
276    use rucc_target::x86_64::{BRANCH, FRAME, GPR, REGS};
277
278    use super::*;
279
280    /// A diamond: one block that goes two ways and one block both ways arrive at, with as many
281    /// parameters on the block they arrive at as the test asks for.
282    fn diamond(params: usize) -> (Interner, mir::Func, [mir::Block; 4]) {
283        let mut names = Interner::new();
284        let mut func = mir::Func::new(names.intern("f"));
285        let head = func.create_block();
286        let left = func.create_block();
287        let right = func.create_block();
288        let join = func.create_block();
289        // The values arrive in the head, so that they have somewhere to be defined and the
290        // printer has a name for them. Nothing here runs an allocator, which is the one thing
291        // that would object to a first block with parameters.
292        let args: Vec<mir::Reg> = (0..params).map(|_| func.append_param(head, GPR)).collect();
293        for _ in 0..params {
294            func.append_param(join, GPR);
295        }
296        *func.succs_mut(head) = vec![mir::BlockCall::to(left), mir::BlockCall::to(right)];
297        *func.succs_mut(left) = vec![mir::BlockCall::with(join, args.clone())];
298        *func.succs_mut(right) = vec![mir::BlockCall::with(join, args)];
299        (names, func, [head, left, right, join])
300    }
301
302    /// Where each block goes, which is the whole of what this changes.
303    fn edges(func: &mir::Func) -> Vec<Vec<usize>> {
304        func.blocks()
305            .map(|block| func[block].succs.iter().map(|call| call.block.index()).collect())
306            .collect()
307    }
308
309    #[test]
310    fn an_edge_that_is_the_only_way_out_is_left_alone() {
311        let (_, mut func, _) = diamond(1);
312        // The two edges into the join carry a value each and neither is critical, because the
313        // block each leaves goes nowhere else.
314        assert_eq!(critical(&mut func), 0);
315        assert_eq!(edges(&func), vec![vec![1, 2], vec![3], vec![3], vec![]]);
316    }
317
318    #[test]
319    fn a_critical_edge_carrying_a_value_is_split_in_two() {
320        let (_, mut func, [head, _, _, join]) = diamond(1);
321        // Now the head goes straight to the join as well, so both of its arms are critical: it
322        // has two ways out and the join has three ways in.
323        let arg = func.append_param(head, GPR);
324        func.succs_mut(head).push(mir::BlockCall::with(join, vec![arg]));
325        func.succs_mut(head).swap(1, 2);
326
327        assert_eq!(critical(&mut func), 1);
328        assert_eq!(
329            edges(&func),
330            // The head's second arm is the new block and the new block goes to the join. The
331            // other two arms are untouched, because each goes to a block with one way in.
332            vec![vec![1, 4, 2], vec![3], vec![3], vec![], vec![3]]
333        );
334    }
335
336    #[test]
337    fn a_critical_edge_carrying_nothing_is_left_alone() {
338        let (_, mut func, [head, _, _, join]) = diamond(0);
339        func.succs_mut(head).push(mir::BlockCall::to(join));
340
341        // Critical and not split, because there is no move to find a place for and a block that
342        // is a jump and nothing else is worth more than nothing.
343        assert_eq!(critical(&mut func), 0);
344    }
345
346    #[test]
347    fn the_arguments_move_on_to_the_half_that_arrives() {
348        let (names, mut func, [head, _, _, join]) = diamond(1);
349        let arg = func.append_param(head, GPR);
350        func.succs_mut(head).push(mir::BlockCall::with(join, vec![arg]));
351
352        assert_eq!(critical(&mut func), 1);
353        // What the first half carries is nothing, since the block it goes to asks for nothing,
354        // and what the second half carries is what the whole edge used to.
355        let half = func.blocks().last().expect("the block the split added");
356        assert_eq!(func[head].succs[2].args, Vec::new());
357        assert_eq!(func[half].succs[0].args, vec![arg]);
358        assert_eq!(
359            mir::print_func(&func, &names, &REGS),
360            "mfunc @f {\nblock0(%0:gpr, %1:gpr):\n    block1, block2, block4\n\n\
361             block1:\n    block3(%0)\n\nblock2:\n    block3(%0)\n\n\
362             block3(%2:gpr):\n\nblock4:\n    block3(%1)\n}\n"
363        );
364    }
365
366    #[test]
367    fn splitting_twice_is_splitting_once() {
368        let (_, mut func, [head, _, _, join]) = diamond(1);
369        let arg = func.append_param(head, GPR);
370        func.succs_mut(head).push(mir::BlockCall::with(join, vec![arg]));
371
372        assert_eq!(critical(&mut func), 1);
373        assert_eq!(critical(&mut func), 0);
374    }
375
376    /// A function with one label whose address is taken and as many blocks leaving through that
377    /// address as the test asks for, each carrying as many values to the label as it asks for.
378    fn computed(branches: usize, params: usize) -> (Interner, mir::Func) {
379        let mut names = Interner::new();
380        let mut func = mir::Func::new(names.intern("f"));
381        let head = func.create_block();
382        let label = func.create_block();
383        for _ in 0..params {
384            func.append_param(label, GPR);
385        }
386        let lea = mir::Opcode::new(names.intern("x64.lea_64"));
387        let jump = mir::Opcode::new(names.intern("x64.jmp_reg"));
388        for _ in 0..branches {
389            // Every branch works the address out for itself, which is what a program that takes
390            // the address of a label twice looks like once the values are in registers.
391            let args: Vec<mir::Reg> = (0..params).map(|_| func.append_param(head, GPR)).collect();
392            let address = func.new_vreg(GPR);
393            let at = if branches == 1 { head } else { func.create_block() };
394            func.build(at, lea).def(address, GPR).mem(mir::Mem::block(label)).finish();
395            func.build(at, jump).operand(mir::Operand::read(address, GPR)).finish();
396            *func.succs_mut(at) = vec![mir::BlockCall::with(label, args)];
397        }
398        (names, func)
399    }
400
401    /// Which block each address in the function names, in the order the instructions are in.
402    fn addressed(func: &mir::Func) -> Vec<usize> {
403        func.blocks()
404            .flat_map(|block| func.insts(block).collect::<Vec<_>>())
405            .filter_map(|inst| func[inst].mem)
406            .filter_map(|mem| func[mem].block)
407            .map(mir::Block::index)
408            .collect()
409    }
410
411    #[test]
412    fn the_values_a_computed_goto_carries_move_into_a_block_in_front_of_the_label() {
413        let (mut names, mut func) = computed(1, 1);
414        assert_eq!(indirect(&mut func, &BRANCH, &FRAME, &mut names), 1);
415
416        // The branch goes to the new block carrying nothing, and the new block carries the value
417        // the branch used to. The address the `lea` works out is the new block's as well, since
418        // arriving at the label without going through the new block is arriving without the value.
419        assert_eq!(edges(&func), vec![vec![2], vec![], vec![1]]);
420        assert_eq!(func[mir::Block::new(0)].succs[0].args, Vec::new());
421        assert_eq!(addressed(&func), vec![2]);
422    }
423
424    #[test]
425    fn two_computed_gotos_that_reach_one_label_are_made_to_agree() {
426        let (mut names, mut func) = computed(2, 1);
427        assert_eq!(indirect(&mut func, &BRANCH, &FRAME, &mut names), 1);
428
429        // One block in front of the label and not two, because the label has one address and both
430        // branches arrive at it. What makes that sound is the move each branch writes in front of
431        // its own jump, which puts its value in the register that block carries.
432        assert_eq!(edges(&func), vec![vec![], vec![], vec![4], vec![4], vec![1]]);
433        let text = mir::print_func(&func, &names, &REGS);
434        assert_eq!(text.matches("x64.mov_rr_64").count(), 2, "{text}");
435        // In front of the jump rather than behind it, since nothing behind a jump runs.
436        for line in text.lines().collect::<Vec<_>>().windows(2) {
437            if line[1].contains("x64.jmp_reg") {
438                assert!(line[0].contains("x64.mov_rr_64"), "{text}");
439            }
440        }
441        assert_eq!(addressed(&func), vec![4, 4]);
442    }
443
444    #[test]
445    fn an_edge_out_of_a_computed_goto_that_carries_nothing_is_left_alone() {
446        let (mut names, mut func) = computed(1, 0);
447
448        // No values to carry, so no block to carry them, and the address stays the label's own.
449        assert_eq!(indirect(&mut func, &BRANCH, &FRAME, &mut names), 0);
450        assert_eq!(addressed(&func), vec![1]);
451    }
452
453    #[test]
454    fn a_function_with_no_computed_goto_in_it_is_left_alone() {
455        let (mut names, mut func, _) = diamond(1);
456        assert_eq!(indirect(&mut func, &BRANCH, &FRAME, &mut names), 0);
457        assert_eq!(edges(&func), vec![vec![1, 2], vec![3], vec![3], vec![]]);
458    }
459
460    #[test]
461    fn what_it_leaves_is_nothing_for_the_splitting_below_to_do() {
462        let (mut names, mut func) = computed(2, 1);
463        indirect(&mut func, &BRANCH, &FRAME, &mut names);
464        // The edges out of the branches carry nothing now, and the edges out of the blocks it
465        // added are the only way out of those blocks, so neither kind is critical.
466        assert_eq!(critical(&mut func), 0);
467    }
468
469    /// The first instruction of each block, by opcode, and an empty string for a block with
470    /// nothing in it.
471    fn opens(func: &mir::Func, names: &Interner) -> Vec<String> {
472        func.blocks()
473            .map(|block| match func.insts(block).next() {
474                Some(inst) => names.resolve(func[inst].opcode.name()).to_owned(),
475                None => String::new(),
476            })
477            .collect()
478    }
479
480    #[test]
481    fn the_block_a_label_begins_at_gets_a_landing_pad_when_the_forward_edge_is_checked() {
482        let (mut names, mut func) = computed(2, 1);
483        indirect(&mut func, &BRANCH, &FRAME, &mut names);
484
485        // One pad, at the block in front of the label, because that is the block both addresses
486        // name once the values have been moved on to it. The label's own block is arrived at by an
487        // ordinary edge from there and wants nothing.
488        assert_eq!(pads(&mut func, &FRAME, FRAME.landing, &mut names), 1);
489        assert_eq!(opens(&func, &names), ["", "", "x64.lea_64", "x64.lea_64", "x64.endbr64"]);
490    }
491
492    #[test]
493    fn a_label_with_no_block_in_front_of_it_gets_the_pad_itself() {
494        let (mut names, mut func) = computed(1, 0);
495        indirect(&mut func, &BRANCH, &FRAME, &mut names);
496
497        // Nothing was moved on to anything, so the address still names the label and the pad goes
498        // where the address goes.
499        assert_eq!(pads(&mut func, &FRAME, FRAME.landing, &mut names), 1);
500        assert_eq!(opens(&func, &names), ["x64.lea_64", "x64.endbr64"]);
501    }
502
503    #[test]
504    fn nothing_is_written_when_the_forward_edge_is_not_checked() {
505        let (mut names, mut func) = computed(1, 0);
506        assert_eq!(pads(&mut func, &FRAME, None, &mut names), 0);
507        assert_eq!(opens(&func, &names), ["x64.lea_64", ""]);
508    }
509}