use std::collections::HashMap;
use rucc_base::Interner;
use rucc_mir as mir;
use rucc_target::{BranchInsts, FrameInsts};
pub fn critical(func: &mut mir::Func) -> usize {
let preds = preds(func);
let blocks: Vec<mir::Block> = func.blocks().collect();
let mut split = 0;
for block in blocks {
if func[block].succs.len() < 2 {
continue;
}
for index in 0..func[block].succs.len() {
let call = func[block].succs[index].clone();
if call.args.is_empty() || preds[call.block.index()] < 2 {
continue;
}
let weight = call.weight;
let half = func.create_block();
func.set_weight(half, weight);
*func.succs_mut(half) = vec![call];
func.succs_mut(block)[index] = mir::BlockCall::to(half).taken(weight);
split += 1;
}
}
split
}
pub fn indirect(
func: &mut mir::Func,
branch: &BranchInsts,
frame: &FrameInsts,
names: &mut Interner,
) -> usize {
let jump = mir::Opcode::new(names.intern(&format!("{}{}", branch.prefix, branch.indirect)));
let branches: Vec<mir::Block> = func
.blocks()
.filter(|&block| func.terminator(block).is_some_and(|last| func[last].opcode == jump))
.collect();
if branches.is_empty() {
return 0;
}
let mut targets: Vec<mir::Block> = Vec::new();
for &block in &branches {
for call in &func[block].succs {
if !call.args.is_empty() && !targets.contains(&call.block) {
targets.push(call.block);
}
}
}
let mut entries: HashMap<mir::Block, mir::Block> = HashMap::new();
for target in targets {
let params = func[target].params.clone();
let homes: Vec<mir::Reg> = params.iter().map(|param| func.new_vreg(param.class)).collect();
let entry = func.create_block();
let mut total = mir::Weight::NEVER;
for &block in &branches {
for index in 0..func[block].succs.len() {
if func[block].succs[index].block != target {
continue;
}
let call = func[block].succs[index].clone();
let last = func.terminator(block).expect("a block that ends in a jump");
for (home, (arg, param)) in homes.iter().zip(call.args.iter().zip(¶ms)) {
let name = frame.moves(param.class).expect("a class this machine can move").mov;
let opcode = mir::Opcode::new(names.intern(&format!("{}{name}", frame.prefix)));
let inst = func
.build_loose(opcode)
.def(*home, param.class)
.uses(*arg, param.class)
.finish();
func.insert_before(last, inst);
}
total = mir::Weight::parts(total.raw().saturating_add(call.weight.raw()));
func.succs_mut(block)[index] = mir::BlockCall::to(entry).taken(call.weight);
}
}
func.set_weight(entry, total);
*func.succs_mut(entry) = vec![mir::BlockCall::with(target, homes).taken(total)];
entries.insert(target, entry);
}
let mut addresses: Vec<mir::MemRef> = Vec::new();
for block in func.blocks() {
for inst in func.insts(block) {
if let Some(mem) = func[inst].mem {
addresses.push(mem);
}
}
}
for mem in addresses {
if let Some(named) = func[mem].block {
if let Some(&entry) = entries.get(&named) {
func[mem].block = Some(entry);
}
}
}
for (block, _) in &mut func.labels {
if let Some(&entry) = entries.get(block) {
*block = entry;
}
}
entries.len()
}
pub fn pads(
func: &mut mir::Func,
frame: &FrameInsts,
landing: Option<&'static str>,
names: &mut Interner,
) -> usize {
let Some(name) = landing else { return 0 };
let opcode = mir::Opcode::new(names.intern(&format!("{}{name}", frame.prefix)));
let mut addressed: Vec<mir::Block> = Vec::new();
for block in func.blocks() {
for inst in func.insts(block) {
if let Some(mem) = func[inst].mem {
if let Some(named) = func[mem].block {
if !addressed.contains(&named) {
addressed.push(named);
}
}
}
}
}
for &block in &addressed {
let inst = func.build_loose(opcode).finish();
func.prepend_inst(block, inst);
}
addressed.len()
}
fn preds(func: &mir::Func) -> Vec<usize> {
let mut counts = vec![0; func.block_count()];
for block in func.blocks() {
for call in &func[block].succs {
counts[call.block.index()] += 1;
}
}
counts
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_target::x86_64::{BRANCH, FRAME, GPR, REGS};
use super::*;
fn diamond(params: usize) -> (Interner, mir::Func, [mir::Block; 4]) {
let mut names = Interner::new();
let mut func = mir::Func::new(names.intern("f"));
let head = func.create_block();
let left = func.create_block();
let right = func.create_block();
let join = func.create_block();
let args: Vec<mir::Reg> = (0..params).map(|_| func.append_param(head, GPR)).collect();
for _ in 0..params {
func.append_param(join, GPR);
}
*func.succs_mut(head) = vec![mir::BlockCall::to(left), mir::BlockCall::to(right)];
*func.succs_mut(left) = vec![mir::BlockCall::with(join, args.clone())];
*func.succs_mut(right) = vec![mir::BlockCall::with(join, args)];
(names, func, [head, left, right, join])
}
fn edges(func: &mir::Func) -> Vec<Vec<usize>> {
func.blocks()
.map(|block| func[block].succs.iter().map(|call| call.block.index()).collect())
.collect()
}
#[test]
fn an_edge_that_is_the_only_way_out_is_left_alone() {
let (_, mut func, _) = diamond(1);
assert_eq!(critical(&mut func), 0);
assert_eq!(edges(&func), vec![vec![1, 2], vec![3], vec![3], vec![]]);
}
#[test]
fn a_critical_edge_carrying_a_value_is_split_in_two() {
let (_, mut func, [head, _, _, join]) = diamond(1);
let arg = func.append_param(head, GPR);
func.succs_mut(head).push(mir::BlockCall::with(join, vec![arg]));
func.succs_mut(head).swap(1, 2);
assert_eq!(critical(&mut func), 1);
assert_eq!(
edges(&func),
vec![vec![1, 4, 2], vec![3], vec![3], vec![], vec![3]]
);
}
#[test]
fn a_critical_edge_carrying_nothing_is_left_alone() {
let (_, mut func, [head, _, _, join]) = diamond(0);
func.succs_mut(head).push(mir::BlockCall::to(join));
assert_eq!(critical(&mut func), 0);
}
#[test]
fn the_arguments_move_on_to_the_half_that_arrives() {
let (names, mut func, [head, _, _, join]) = diamond(1);
let arg = func.append_param(head, GPR);
func.succs_mut(head).push(mir::BlockCall::with(join, vec![arg]));
assert_eq!(critical(&mut func), 1);
let half = func.blocks().last().expect("the block the split added");
assert_eq!(func[head].succs[2].args, Vec::new());
assert_eq!(func[half].succs[0].args, vec![arg]);
assert_eq!(
mir::print_func(&func, &names, ®S),
"mfunc @f {\nblock0(%0:gpr, %1:gpr):\n block1, block2, block4\n\n\
block1:\n block3(%0)\n\nblock2:\n block3(%0)\n\n\
block3(%2:gpr):\n\nblock4:\n block3(%1)\n}\n"
);
}
#[test]
fn splitting_twice_is_splitting_once() {
let (_, mut func, [head, _, _, join]) = diamond(1);
let arg = func.append_param(head, GPR);
func.succs_mut(head).push(mir::BlockCall::with(join, vec![arg]));
assert_eq!(critical(&mut func), 1);
assert_eq!(critical(&mut func), 0);
}
fn computed(branches: usize, params: usize) -> (Interner, mir::Func) {
let mut names = Interner::new();
let mut func = mir::Func::new(names.intern("f"));
let head = func.create_block();
let label = func.create_block();
for _ in 0..params {
func.append_param(label, GPR);
}
let lea = mir::Opcode::new(names.intern("x64.lea_64"));
let jump = mir::Opcode::new(names.intern("x64.jmp_reg"));
for _ in 0..branches {
let args: Vec<mir::Reg> = (0..params).map(|_| func.append_param(head, GPR)).collect();
let address = func.new_vreg(GPR);
let at = if branches == 1 { head } else { func.create_block() };
func.build(at, lea).def(address, GPR).mem(mir::Mem::block(label)).finish();
func.build(at, jump).operand(mir::Operand::read(address, GPR)).finish();
*func.succs_mut(at) = vec![mir::BlockCall::with(label, args)];
}
(names, func)
}
fn addressed(func: &mir::Func) -> Vec<usize> {
func.blocks()
.flat_map(|block| func.insts(block).collect::<Vec<_>>())
.filter_map(|inst| func[inst].mem)
.filter_map(|mem| func[mem].block)
.map(mir::Block::index)
.collect()
}
#[test]
fn the_values_a_computed_goto_carries_move_into_a_block_in_front_of_the_label() {
let (mut names, mut func) = computed(1, 1);
assert_eq!(indirect(&mut func, &BRANCH, &FRAME, &mut names), 1);
assert_eq!(edges(&func), vec![vec![2], vec![], vec![1]]);
assert_eq!(func[mir::Block::new(0)].succs[0].args, Vec::new());
assert_eq!(addressed(&func), vec![2]);
}
#[test]
fn two_computed_gotos_that_reach_one_label_are_made_to_agree() {
let (mut names, mut func) = computed(2, 1);
assert_eq!(indirect(&mut func, &BRANCH, &FRAME, &mut names), 1);
assert_eq!(edges(&func), vec![vec![], vec![], vec![4], vec![4], vec![1]]);
let text = mir::print_func(&func, &names, ®S);
assert_eq!(text.matches("x64.mov_rr_64").count(), 2, "{text}");
for line in text.lines().collect::<Vec<_>>().windows(2) {
if line[1].contains("x64.jmp_reg") {
assert!(line[0].contains("x64.mov_rr_64"), "{text}");
}
}
assert_eq!(addressed(&func), vec![4, 4]);
}
#[test]
fn an_edge_out_of_a_computed_goto_that_carries_nothing_is_left_alone() {
let (mut names, mut func) = computed(1, 0);
assert_eq!(indirect(&mut func, &BRANCH, &FRAME, &mut names), 0);
assert_eq!(addressed(&func), vec![1]);
}
#[test]
fn a_function_with_no_computed_goto_in_it_is_left_alone() {
let (mut names, mut func, _) = diamond(1);
assert_eq!(indirect(&mut func, &BRANCH, &FRAME, &mut names), 0);
assert_eq!(edges(&func), vec![vec![1, 2], vec![3], vec![3], vec![]]);
}
#[test]
fn what_it_leaves_is_nothing_for_the_splitting_below_to_do() {
let (mut names, mut func) = computed(2, 1);
indirect(&mut func, &BRANCH, &FRAME, &mut names);
assert_eq!(critical(&mut func), 0);
}
fn opens(func: &mir::Func, names: &Interner) -> Vec<String> {
func.blocks()
.map(|block| match func.insts(block).next() {
Some(inst) => names.resolve(func[inst].opcode.name()).to_owned(),
None => String::new(),
})
.collect()
}
#[test]
fn the_block_a_label_begins_at_gets_a_landing_pad_when_the_forward_edge_is_checked() {
let (mut names, mut func) = computed(2, 1);
indirect(&mut func, &BRANCH, &FRAME, &mut names);
assert_eq!(pads(&mut func, &FRAME, FRAME.landing, &mut names), 1);
assert_eq!(opens(&func, &names), ["", "", "x64.lea_64", "x64.lea_64", "x64.endbr64"]);
}
#[test]
fn a_label_with_no_block_in_front_of_it_gets_the_pad_itself() {
let (mut names, mut func) = computed(1, 0);
indirect(&mut func, &BRANCH, &FRAME, &mut names);
assert_eq!(pads(&mut func, &FRAME, FRAME.landing, &mut names), 1);
assert_eq!(opens(&func, &names), ["x64.lea_64", "x64.endbr64"]);
}
#[test]
fn nothing_is_written_when_the_forward_edge_is_not_checked() {
let (mut names, mut func) = computed(1, 0);
assert_eq!(pads(&mut func, &FRAME, None, &mut names), 0);
assert_eq!(opens(&func, &names), ["x64.lea_64", ""]);
}
}