rucc_regalloc/lib.rs
1//! Both register allocators and the allocation checker.
2//!
3//! Design: `spec/10-backend.md`. Layer rank 11, see `spec/18-package-layout.md`.
4//!
5//! # Status
6//!
7//! Liveness is here, which is the question both allocators ask first: [`order`] lays a function out
8//! in the line the encoder will emit it in, and [`live`] says where in that line each value is
9//! wanted. So is [`moves`], which puts the moves an edge turns into in an order they can be made in
10//! one at a time. The single pass allocator's decision is in [`assign`]: where every value of a
11//! function goes, in one linear scan, which is what `-O0` asks for. The rewrite that makes that
12//! decision true in the function is in [`rewrite`], with what each instruction needs around it for
13//! the machine to accept the places worked out in [`legalize`], and [`run`] is the two of them
14//! together, which is the whole of the `-O0` allocator. [`check`] reads an assignment back and says
15//! whether it is one the machine can run, which [`run`] asserts on in debug and CI builds and which
16//! the backtracking allocator is held to the same way. [`trace`] asks the other half of the
17//! question, which is whether the rewrite wrote that decision down without losing a value on the
18//! way: it follows every value from the instruction that wrote it to the instructions that read it,
19//! through the moves, and [`run`] asserts on it in the same builds.
20//!
21//! The allocator the optimizer uses is in [`backtrack`]. [`pressure`] counts, at every point, the
22//! values that want a register against the registers there are, and [`spill`] reads that to pick
23//! which values go to memory before [`backtrack`] places the rest.
24//!
25//! Every crate in the workspace is published, and publishing implies a promise. This one is
26//! tier 3: its Rust API is explicitly unstable and will change without a major version bump.
27//! Depend on the `rucc` binary's behaviour, not on this.
28
29#![doc(html_root_url = "https://docs.rs/rucc-regalloc/0.15.3")]
30
31pub mod assign;
32pub mod backtrack;
33pub mod check;
34pub mod legalize;
35pub mod live;
36pub mod moves;
37pub mod order;
38pub mod pressure;
39pub mod rewrite;
40pub mod spill;
41pub mod trace;
42
43/// What allocating a function produced.
44///
45/// The moves are handed back rather than written into the function because a move is an
46/// instruction and an instruction belongs to a target, which `spec/10-backend.md` section 10.8
47/// says this crate holds nothing of. The consumer turns each one into whatever its target moves a
48/// register with.
49#[derive(Debug, Clone)]
50pub struct Allocation {
51 /// Where every value of the function went, which is what the frame layout reads.
52 pub assignment: assign::Assignment,
53 /// The moves the places do not already make true, in the order they have to be made in.
54 pub edits: Vec<rewrite::Edit>,
55 /// The line the function was laid out in while it was allocated, which the liveness below is
56 /// counted along.
57 pub order: order::Order,
58 /// Where every value was live.
59 ///
60 /// Handed back rather than dropped because the stack slot allocator shares one run of bytes
61 /// between two things that are never both wanted, and the only liveness that knows where a
62 /// spilled value is wanted is the one the spilling was decided from. Working a second one out
63 /// afterwards would cost a pass and would be free to disagree with this one.
64 /// `spec/optimizer/36-lowering-and-isel.md` section 36.7 asks for the one answer.
65 pub live: live::Live,
66}
67
68/// Allocates registers for a function the way `-O0` asks for, rewriting it as it goes.
69///
70/// This is the shape `spec/10-backend.md` section 10.4 gives an allocator: a function and the
71/// registers it may use in, an assignment and the moves that make it true out. The backtracking
72/// allocator will answer the same question the same way.
73///
74/// # Panics
75///
76/// Panics on a function the caller was told not to hand it, which is one with a critical edge or
77/// one whose entry block has parameters. See [`rewrite::rewrite`].
78///
79/// It also panics on an assignment [`check`] finds a problem with, which is a bug in this crate or
80/// in whatever produced the function rather than anything the caller did. `spec/10-backend.md`
81/// section 10.4 asks for that check in debug and CI builds, and it runs before the rewrite because
82/// the assignment is the decision and the rewrite only writes it down.
83///
84/// It panics on a rewrite [`trace`] finds a value missing from as well. That one runs afterwards,
85/// since a transcription can only be read once it has been made, and it is the check
86/// `spec/optimizer/39-register-allocation.md` section 39.6 asks for.
87///
88/// `verify` is what turns both of those on in a build that has assertions compiled out. A debug
89/// build runs them whatever it says, since that is where a broken pass should be caught, and a
90/// release build runs them when the caller asks, which is what `-Zverify-each` is for and what
91/// section 10.4 means by a CI build. It is a parameter rather than a `cfg!` because the thing
92/// worth catching is a pass that writes a function nothing defines a register in, the gate
93/// compiles release, and a check the gate never runs is a check that finds the bug after the merge
94/// rather than on the pull request. tamnd/rucc#1411.
95///
96/// `called` is what to call the function in that message. It is passed in rather than read off the
97/// function because the name there is a symbol and resolving one wants the interner, which this
98/// crate has no reason to be handed otherwise. Without it the message is a pair of register numbers
99/// and nothing that says where, and finding the function it was about in a file the size of the
100/// SQLite amalgamation means bisecting by hand.
101pub fn run(func: &mut rucc_mir::Func, env: &assign::Env, called: &str, verify: bool) -> Allocation {
102 run_with(func, env, called, verify, Allocator::Single)
103}
104
105/// Which of the two allocators decides where the values go.
106///
107/// The rewrite, the checks and the moves are the same for both, since each hands back an
108/// [`assign::Assignment`] and nothing after the decision asks which one made it.
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
110pub enum Allocator {
111 /// The single pass of [`assign`], which is what `-O0` asks for.
112 #[default]
113 Single,
114 /// The allocator of [`backtrack`], which weighs what each value costs to keep in memory.
115 Backtracking,
116}
117
118/// Allocates registers for a function with the allocator asked for, rewriting it as it goes.
119///
120/// # Panics
121///
122/// As [`run`].
123pub fn run_with(
124 func: &mut rucc_mir::Func,
125 env: &assign::Env,
126 called: &str,
127 verify: bool,
128 allocator: Allocator,
129) -> Allocation {
130 let checking = verify || cfg!(debug_assertions);
131 let order = order::Order::of(func);
132 let live = live::Live::of(func, &order);
133 let mut assignment = match allocator {
134 Allocator::Single => assign::assign(func, &order, &live, env),
135 Allocator::Backtracking => backtrack::assign(func, &order, &live, env),
136 };
137 // An answer that went over the second source of an instruction that reads its sources either
138 // way round. Swapping them makes it an ordinary reuse of the first, so the checker, the trace
139 // and the rewrite read it as one. Liveness does not care which way round two uses are.
140 for &inst in assignment.commuted() {
141 let list = func[inst].operands;
142 func[list].swap(1, 2);
143 }
144 if checking {
145 let problems = check::check(func, &order, &live, &assignment);
146 assert!(problems.is_empty(), "in '{called}': {}", check::report(&problems));
147 }
148 // What the rewrite is about to lose, taken while it is still there. Only in a build that is
149 // going to read it, since the snapshot is a copy of every operand list in the function.
150 let shape = checking.then(|| trace::shape(func));
151 let edits = rewrite::rewrite(func, &mut assignment, env);
152 if let Some(shape) = shape {
153 let faults = trace::trace(func, &shape, &assignment, &edits);
154 assert!(faults.is_empty(), "in '{called}': {}", trace::report(&faults));
155 }
156 Allocation { assignment, edits, order, live }
157}
158
159/// The milestone in `spec/17-milestones.md` that fills this crate in.
160pub const MILESTONE: &str = "M3";
161
162#[cfg(test)]
163mod tests {
164 use rucc_base::Interner;
165 use rucc_mir::{Func, Opcode};
166 use rucc_target::x86_64::{GPR, SYSV};
167
168 use super::*;
169
170 #[test]
171 fn milestone_is_recorded() {
172 assert!(MILESTONE.starts_with('M'));
173 }
174
175 #[test]
176 fn allocating_a_function_places_every_value_and_hands_back_the_moves_it_needs() {
177 let mut names = Interner::new();
178 let mut func = Func::new(names.intern("f"));
179 let opcode = Opcode::new(names.intern("x64.nop"));
180 let block = func.create_block();
181 let first = func.new_vreg(GPR);
182 let second = func.new_vreg(GPR);
183 let third = func.new_vreg(GPR);
184 func.build(block, opcode).def(first, GPR).finish();
185 func.build(block, opcode).def(second, GPR).finish();
186 func.build(block, opcode).def(third, GPR).finish();
187 func.build(block, opcode).uses(first, GPR).uses(second, GPR).uses(third, GPR).finish();
188
189 // Two registers to hand out and three values that are all wanted at once, so one of them
190 // goes to the stack and the instruction that reads it gets a reload. This is also where
191 // the checker runs, since a debug build asserts on what it says.
192 let env = assign::Env::new().with(GPR, &SYSV.int_order[..2], &SYSV.int_order[2..5]);
193 let allocation = run(&mut func, &env, "test", true);
194
195 assert_eq!(allocation.assignment.spilled(), 1);
196 assert_eq!(allocation.edits.len(), 2);
197 }
198}