#![doc(html_root_url = "https://docs.rs/rucc-regalloc/0.24.5")]
pub mod assign;
pub mod backtrack;
pub mod check;
pub mod legalize;
pub mod live;
pub mod moves;
pub mod order;
pub mod pressure;
pub mod rewrite;
pub mod spill;
pub mod trace;
#[derive(Debug, Clone)]
pub struct Allocation {
pub assignment: assign::Assignment,
pub edits: Vec<rewrite::Edit>,
pub order: order::Order,
pub live: live::Live,
}
pub fn run(func: &mut rucc_mir::Func, env: &assign::Env, called: &str, verify: bool) -> Allocation {
run_with(func, env, called, verify, Allocator::Single)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Allocator {
#[default]
Single,
Backtracking,
}
pub fn run_with(
func: &mut rucc_mir::Func,
env: &assign::Env,
called: &str,
verify: bool,
allocator: Allocator,
) -> Allocation {
let order = order::Order::of(func);
let live = live::Live::of(func, &order);
let assignment = decide(func, &order, &live, env, allocator);
write(func, assignment, env, order, live, called, verify)
}
pub fn run_either(
func: &mut rucc_mir::Func,
wide: &assign::Env,
env: &assign::Env,
called: &str,
verify: bool,
allocator: Allocator,
) -> Allocation {
let order = order::Order::of(func);
let live = live::Live::of(func, &order);
let tried = decide(func, &order, &live, wide, allocator);
if rewrite::fits(func, &tried, wide) {
return write(func, tried, wide, order, live, called, verify);
}
let assignment = decide(func, &order, &live, env, allocator);
write(func, assignment, env, order, live, called, verify)
}
fn decide(
func: &rucc_mir::Func,
order: &order::Order,
live: &live::Live,
env: &assign::Env,
allocator: Allocator,
) -> assign::Assignment {
match allocator {
Allocator::Single => assign::assign(func, order, live, env),
Allocator::Backtracking => backtrack::assign(func, order, live, env),
}
}
fn write(
func: &mut rucc_mir::Func,
mut assignment: assign::Assignment,
env: &assign::Env,
order: order::Order,
live: live::Live,
called: &str,
verify: bool,
) -> Allocation {
let checking = verify || cfg!(debug_assertions);
for &inst in assignment.commuted() {
let list = func[inst].operands;
func[list].swap(1, 2);
}
if checking {
let problems = check::check(func, &order, &live, &assignment);
assert!(problems.is_empty(), "in '{called}': {}", check::report(&problems));
}
let shape = checking.then(|| trace::shape(func));
let edits = rewrite::rewrite(func, &mut assignment, env);
if let Some(shape) = shape {
let faults = trace::trace(func, &shape, &assignment, &edits);
assert!(faults.is_empty(), "in '{called}': {}", trace::report(&faults));
}
Allocation { assignment, edits, order, live }
}
pub const MILESTONE: &str = "M3";
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_mir::{BlockCall, Func, Opcode, Operand, Reg, Weight};
use rucc_target::x86_64::{GPR, RAX, RCX, RDX, SYSV};
use super::*;
#[test]
fn milestone_is_recorded() {
assert!(MILESTONE.starts_with('M'));
}
#[test]
fn allocating_a_function_places_every_value_and_hands_back_the_moves_it_needs() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"));
let opcode = Opcode::new(names.intern("x64.nop"));
let block = func.create_block();
let first = func.new_vreg(GPR);
let second = func.new_vreg(GPR);
let third = func.new_vreg(GPR);
func.build(block, opcode).def(first, GPR).finish();
func.build(block, opcode).def(second, GPR).finish();
func.build(block, opcode).def(third, GPR).finish();
func.build(block, opcode).uses(first, GPR).uses(second, GPR).uses(third, GPR).finish();
let env = assign::Env::new().with(GPR, &SYSV.int_order[..2], &SYSV.int_order[2..5]);
let allocation = run(&mut func, &env, "test", true);
assert_eq!(allocation.assignment.spilled(), 1);
assert_eq!(allocation.edits.len(), 2);
}
fn three_at_once(names: &mut Interner) -> (Func, [Reg; 3]) {
let mut func = Func::new(names.intern("f"));
let opcode = Opcode::new(names.intern("x64.nop"));
let block = func.create_block();
let values = [(); 3].map(|()| func.new_vreg(GPR));
for value in values {
func.build(block, opcode).def(value, GPR).finish();
}
func.build(block, opcode)
.uses(values[0], GPR)
.uses(values[1], GPR)
.uses(values[2], GPR)
.finish();
(func, values)
}
#[test]
fn a_function_that_needs_no_scratch_register_is_given_the_scratch_registers() {
let mut names = Interner::new();
let (mut func, values) = three_at_once(&mut names);
let wide = assign::Env::new().with(GPR, &SYSV.int_order[..3], &[]);
let env = assign::Env::new().with(GPR, &SYSV.int_order[..2], &SYSV.int_order[2..4]);
let allocation = run_either(&mut func, &wide, &env, "test", true, Allocator::Single);
assert_eq!(allocation.assignment.spilled(), 0);
assert!(allocation.edits.is_empty());
let mut places: Vec<_> =
values.iter().filter_map(|&value| allocation.assignment.place(value)).collect();
places.sort_by_key(|place| match place {
assign::Place::Reg(reg) => reg.number(),
assign::Place::Slot(_) => u8::MAX,
});
let regs = [RAX, RCX, RDX].map(assign::Place::Reg);
assert_eq!(places, regs);
}
#[test]
fn a_function_that_spills_is_allocated_again_with_the_scratch_registers_held_back() {
let mut names = Interner::new();
let (mut func, _) = three_at_once(&mut names);
let wide = assign::Env::new().with(GPR, &SYSV.int_order[..2], &[]);
let env = assign::Env::new().with(GPR, &SYSV.int_order[..2], &SYSV.int_order[2..4]);
let allocation = run_either(&mut func, &wide, &env, "test", true, Allocator::Single);
assert_eq!(allocation.assignment.spilled(), 1);
assert_eq!(allocation.edits.len(), 2);
}
#[test]
fn two_values_swapping_on_an_edge_are_allocated_with_the_scratch_registers_held_back() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"));
let opcode = Opcode::new(names.intern("x64.nop"));
let head = func.create_block();
let body = func.create_block();
let first = func.new_vreg(GPR);
let second = func.new_vreg(GPR);
func.build(head, opcode).def(first, GPR).finish();
func.build(head, opcode).def(second, GPR).finish();
let left = func.append_param(body, GPR);
let right = func.append_param(body, GPR);
*func.succs_mut(head) = vec![BlockCall::with(body, vec![first, second])];
func.build(body, opcode).uses(left, GPR).uses(right, GPR).finish();
*func.succs_mut(body) = vec![BlockCall::with(body, vec![right, left])];
let wide = assign::Env::new().with(GPR, &SYSV.int_order[..2], &[]);
let env = assign::Env::new().with(GPR, &SYSV.int_order[..2], &SYSV.int_order[2..4]);
let allocation = run_either(&mut func, &wide, &env, "test", true, Allocator::Single);
assert_eq!(allocation.assignment.spilled(), 0);
let through = assign::Place::Reg(RDX);
assert!(allocation.edits.iter().any(|edit| edit.mov.to == through), "{allocation:?}");
}
#[test]
fn a_value_carried_round_a_loop_is_allocated_with_the_scratch_registers_given_out() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"));
let opcode = Opcode::new(names.intern("x64.nop"));
let head = func.create_block();
let body = func.create_block();
let first = func.new_vreg(GPR);
func.build(head, opcode).def(first, GPR).finish();
let carried = func.append_param(body, GPR);
*func.succs_mut(head) = vec![BlockCall::with(body, vec![first])];
let next = func.new_vreg(GPR);
func.build(body, opcode).def(next, GPR).uses(carried, GPR).finish();
*func.succs_mut(body) = vec![BlockCall::with(body, vec![next])];
let wide = assign::Env::new().with(GPR, &SYSV.int_order[..2], &[]);
let env = assign::Env::new().with(GPR, &SYSV.int_order[..1], &SYSV.int_order[1..3]);
let allocation = run_either(&mut func, &wide, &env, "test", true, Allocator::Single);
assert_eq!(allocation.assignment.spilled(), 0);
for value in [first, carried, next] {
let place = allocation.assignment.place(value);
assert!(matches!(place, Some(assign::Place::Reg(_))), "{allocation:?}");
}
}
#[test]
fn a_value_a_loop_reads_is_put_away_around_the_call_the_loop_seldom_makes() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"));
let opcode = Opcode::new(names.intern("x64.nop"));
let [entry, head, cold, skip, latch, back, out] = [(); 7].map(|()| func.create_block());
let step = func.new_vreg(GPR);
func.build(entry, opcode).def(step, GPR).finish();
*func.succs_mut(entry) = vec![BlockCall::to(head)];
func.build(head, opcode).uses(step, GPR).finish();
*func.succs_mut(head) = vec![BlockCall::to(cold), BlockCall::to(skip)];
let call = func
.build(cold, opcode)
.operand(Operand::write(Reg::physical(RAX), GPR))
.operand(Operand::write(Reg::physical(RCX), GPR))
.finish();
*func.succs_mut(cold) = vec![BlockCall::to(latch)];
*func.succs_mut(skip) = vec![BlockCall::to(latch)];
func.build(latch, opcode).uses(step, GPR).finish();
*func.succs_mut(latch) = vec![BlockCall::to(back), BlockCall::to(out)];
*func.succs_mut(back) = vec![BlockCall::to(head)];
func.build(out, opcode).uses(step, GPR).finish();
for (block, often) in [(head, 100), (skip, 99), (latch, 100), (back, 99)] {
func.set_weight(block, Weight::parts(often * Weight::SCALE));
}
let env = assign::Env::new().with(GPR, &SYSV.int_order[..2], &SYSV.int_order[2..5]);
let allocation = run_with(&mut func, &env, "test", true, Allocator::Backtracking);
let assignment = &allocation.assignment;
assert_eq!(assignment.spilled(), 0);
let Some(assign::Place::Reg(at)) = assignment.place(step) else {
panic!("the value went to the stack");
};
let saves = assignment.saves();
assert_eq!(saves.len(), 1);
assert_eq!((saves[0].reg, saves[0].inst), (step, call));
let slot = assign::Place::Slot(saves[0].slot);
let here = |edit: &&rewrite::Edit| match edit.at {
rewrite::At::Before(inst) | rewrite::At::After(inst) => inst == call,
_ => false,
};
let around: Vec<_> = allocation
.edits
.iter()
.filter(here)
.map(|edit| (edit.at, edit.mov.to, edit.mov.from))
.collect();
assert_eq!(
around,
[
(rewrite::At::Before(call), slot, assign::Place::Reg(at)),
(rewrite::At::After(call), assign::Place::Reg(at), slot),
]
);
}
}