use std::collections::{HashMap, HashSet};
use rucc_base::Interner;
use rucc_mir as mir;
use rucc_target::{BranchInsts, Fusion, Role};
pub fn blocks(
func: &mut mir::Func,
insts: &BranchInsts,
names: &mut Interner,
fusable: &HashSet<mir::Inst>,
) {
let table = table(insts, names);
let mut order = order(func);
let mut writer = Writer { func, insts, names, table, fusable };
let mut at = 0;
while at < order.len() {
if let Some(bridge) = writer.edges(order[at], order.get(at + 1).copied()) {
order.insert(at + 1, bridge);
}
at += 1;
}
func.set_block_order(&order);
}
fn order(func: &mir::Func) -> Vec<mir::Block> {
let mut order = Vec::with_capacity(func.block_count());
let mut seen = vec![false; func.block_count()];
if let Some(entry) = func.entry() {
seen[entry.index()] = true;
let mut stack = vec![(entry, 0usize)];
while let Some((block, next)) = stack.pop() {
let succs = &func[block].succs;
let Some(arm) = succs.len().checked_sub(next + 1) else {
order.push(block);
continue;
};
stack.push((block, next + 1));
let to = succs[arm].block;
if !std::mem::replace(&mut seen[to.index()], true) {
stack.push((to, 0));
}
}
order.reverse();
}
order.extend(func.blocks().filter(|block| !seen[block.index()]));
order
}
fn table(insts: &BranchInsts, names: &mut Interner) -> HashMap<mir::Opcode, &'static Fusion> {
insts
.fused
.iter()
.map(|fusion| {
(mir::Opcode::new(names.intern(&format!("{}{}", insts.prefix, fusion.set))), fusion)
})
.collect()
}
#[must_use]
pub fn fusable(func: &mir::Func, insts: &BranchInsts, names: &mut Interner) -> HashSet<mir::Inst> {
let table = table(insts, names);
let branch = mir::Opcode::new(names.intern(&format!("{}{}", insts.prefix, insts.cond)));
let reads = crate::fold::reads(func);
let mut found = HashSet::new();
for block in func.blocks() {
let insts: Vec<mir::Inst> = func.insts(block).collect();
let [.., compare, last] = insts[..] else { continue };
if func[last].opcode != branch || !table.contains_key(&func[compare].opcode) {
continue;
}
let operands = &func[func[compare].operands];
let Some(byte) = operands.first().filter(|operand| operand.role != Role::Use) else {
continue;
};
if !byte.reg.is_virtual() || reads.get(&byte.reg) != Some(&1) {
continue;
}
if func[func[last].operands].first().map(|operand| operand.reg) == Some(byte.reg) {
found.insert(compare);
}
}
found
}
struct Writer<'a> {
func: &'a mut mir::Func,
insts: &'a BranchInsts,
names: &'a mut Interner,
table: HashMap<mir::Opcode, &'static Fusion>,
fusable: &'a HashSet<mir::Inst>,
}
impl Writer<'_> {
fn edges(&mut self, block: mir::Block, next: Option<mir::Block>) -> Option<mir::Block> {
match self.func[block].succs.len() {
0 => None,
1 => {
self.one(block, next);
None
}
2 => self.two(block, next),
arms => panic!("a block with {arms} arms, and nothing lowers to one"),
}
}
fn one(&mut self, block: mir::Block, next: Option<mir::Block>) {
if Some(self.func[block].succs[0].block) == next {
return;
}
let opcode = self.opcode(self.insts.jump);
self.func.build(block, opcode).finish();
}
fn two(&mut self, block: mir::Block, next: Option<mir::Block>) -> Option<mir::Block> {
let fused = self.fused(block);
let condition = self.take(block);
let (if_true, if_false) = match fused {
Some((_, fusion)) => (fusion.if_true, fusion.if_false),
None => (self.insts.if_true, self.insts.if_false),
};
let arms: Vec<mir::Block> = self.func[block].succs.iter().map(|arm| arm.block).collect();
let (name, bridge) = if next == Some(arms[1]) {
(if_true, None)
} else if next == Some(arms[0]) {
self.func.succs_mut(block).swap(0, 1);
(if_false, None)
} else {
(if_true, Some(self.bridge(block)))
};
match fused {
Some((compare, fusion)) => self.keep_only_the_flags(compare, fusion),
None => {
let opcode = self.opcode(self.insts.test);
self.func.build(block, opcode).operand(condition).finish();
}
}
let opcode = self.opcode(name);
self.func.build(block, opcode).finish();
bridge
}
fn fused(&self, block: mir::Block) -> Option<(mir::Inst, &'static Fusion)> {
let insts: Vec<mir::Inst> = self.func.insts(block).collect();
let [.., compare, last] = insts[..] else { return None };
if !self.fusable.contains(&compare) {
return None;
}
let fusion = *self.table.get(&self.func[compare].opcode)?;
let byte = self.func[self.func[compare].operands].first()?.reg;
(self.func[self.func[last].operands].first()?.reg == byte).then_some((compare, fusion))
}
fn keep_only_the_flags(&mut self, compare: mir::Inst, fusion: &Fusion) {
let read: Vec<mir::Operand> =
self.func[self.func[compare].operands].iter().skip(1).copied().collect();
let operands = self.func.push_operands(&read);
self.func[compare].opcode = self.opcode(fusion.cmp);
self.func[compare].operands = operands;
}
fn take(&mut self, block: mir::Block) -> mir::Operand {
let branch = self.func.terminator(block).expect("a block with two arms has a branch");
let cond = self.opcode(self.insts.cond);
assert_eq!(
self.func[branch].opcode, cond,
"a block with two arms whose last instruction is not the branch"
);
let operands = self.func[branch].operands;
let condition = self.func[operands][0];
self.func.remove_inst(branch);
condition
}
fn bridge(&mut self, block: mir::Block) -> mir::Block {
let bridge = self.func.create_block();
let edge = self.func[block].succs[1].clone();
*self.func.succs_mut(bridge) = vec![edge];
self.func.succs_mut(block)[1] = mir::BlockCall::to(bridge);
bridge
}
fn opcode(&mut self, name: &str) -> mir::Opcode {
mir::Opcode::new(self.names.intern(&format!("{}{name}", self.insts.prefix)))
}
}
#[cfg(test)]
mod tests {
use rucc_mir::{BlockCall, Opcode, Operand, Reg};
use rucc_target::x86_64::{BRANCH, GPR, RAX, RCX, REGS};
use super::*;
fn blank(count: usize) -> (Interner, mir::Func, Vec<mir::Block>) {
let mut names = Interner::new();
let mut func = mir::Func::new(names.intern("f"));
let blocks = (0..count).map(|_| func.create_block()).collect();
(names, func, blocks)
}
fn branch(func: &mut mir::Func, names: &mut Interner, block: mir::Block, arms: &[mir::Block]) {
let opcode = Opcode::new(names.intern("x64.br_cond_8"));
func.build(block, opcode).operand(Operand::read(Reg::physical(RAX), GPR)).finish();
*func.succs_mut(block) = arms.iter().map(|&arm| BlockCall::to(arm)).collect();
}
fn laid_out(func: &mut mir::Func, names: &mut Interner) -> Vec<String> {
let fusable = fusable(func, &BRANCH, names);
blocks(func, &BRANCH, names, &fusable);
mir::print_func(func, names, ®S)
.lines()
.filter(|line| !line.trim().is_empty() && !line.starts_with("mfunc") && *line != "}")
.map(|line| line.trim().to_string())
.collect()
}
fn order_of(func: &mir::Func) -> Vec<usize> {
func.blocks().map(mir::Block::index).collect()
}
#[test]
fn a_block_that_falls_into_the_next_one_gets_no_jump_at_all() {
let (mut names, mut func, made) = blank(2);
*func.succs_mut(made[0]) = vec![BlockCall::to(made[1])];
let text = laid_out(&mut func, &mut names);
assert_eq!(text, ["block0:", "block1", "block1:"]);
}
#[test]
fn a_block_that_goes_somewhere_that_is_not_next_gets_a_jump() {
let (mut names, mut func, made) = blank(2);
*func.succs_mut(made[0]) = vec![BlockCall::to(made[1])];
*func.succs_mut(made[1]) = vec![BlockCall::to(made[0])];
let text = laid_out(&mut func, &mut names);
assert_eq!(text, ["block0:", "block1", "block1:", "x64.jmp block0"]);
}
#[test]
fn a_branch_that_falls_into_its_false_arm_jumps_when_the_condition_holds() {
let (mut names, mut func, made) = blank(3);
*func.succs_mut(made[0]) = vec![BlockCall::to(made[1])];
branch(&mut func, &mut names, made[1], &[made[0], made[2]]);
let text = laid_out(&mut func, &mut names);
assert_eq!(order_of(&func), [0, 1, 2]);
assert_eq!(
text,
[
"block0:",
"block1",
"block1:",
"x64.test_rr_8 $rax",
"x64.jcc_ne block0, block2",
"block2:",
]
);
}
#[test]
fn a_branch_that_falls_into_its_true_arm_jumps_when_the_condition_does_not_hold() {
let (mut names, mut func, made) = blank(3);
branch(&mut func, &mut names, made[0], &[made[1], made[2]]);
let text = laid_out(&mut func, &mut names);
assert_eq!(order_of(&func), [0, 1, 2]);
assert_eq!(
text,
["block0:", "x64.test_rr_8 $rax", "x64.jcc_e block2, block1", "block1:", "block2:"]
);
}
#[test]
fn a_branch_that_can_fall_into_neither_arm_is_given_a_block_to_jump_from() {
let (mut names, mut func, made) = blank(2);
*func.succs_mut(made[0]) = vec![BlockCall::to(made[1])];
branch(&mut func, &mut names, made[1], &[made[0], made[1]]);
let text = laid_out(&mut func, &mut names);
assert_eq!(order_of(&func), [0, 1, 2]);
assert_eq!(
text,
[
"block0:",
"block1",
"block1:",
"x64.test_rr_8 $rax",
"x64.jcc_ne block0, block2",
"block2:",
"x64.jmp block1",
]
);
}
#[test]
fn the_test_reads_the_register_the_branch_read() {
let (mut names, mut func, made) = blank(3);
branch(&mut func, &mut names, made[0], &[made[1], made[2]]);
let fusable = fusable(&func, &BRANCH, &mut names);
blocks(&mut func, &BRANCH, &mut names, &fusable);
let test = func.insts(made[0]).next().expect("a test");
let operands = func[test].operands;
assert_eq!(func[operands], [Operand::read(Reg::physical(RAX), GPR)]);
}
#[test]
fn a_block_nothing_reaches_is_laid_out_at_the_end_rather_than_deleted() {
let (mut names, mut func, made) = blank(4);
*func.succs_mut(made[0]) = vec![BlockCall::to(made[3])];
let fusable = fusable(&func, &BRANCH, &mut names);
blocks(&mut func, &BRANCH, &mut names, &fusable);
assert_eq!(order_of(&func), [0, 3, 1, 2]);
}
#[test]
fn a_function_with_no_blocks_is_left_alone() {
let mut names = Interner::new();
let mut func = mir::Func::new(names.intern("f"));
let fusable = fusable(&func, &BRANCH, &mut names);
blocks(&mut func, &BRANCH, &mut names, &fusable);
assert_eq!(func.block_count(), 0);
}
#[test]
#[should_panic(expected = "a block with 3 arms")]
fn a_block_with_three_arms_is_refused_rather_than_laid_out_wrongly() {
let (mut names, mut func, made) = blank(4);
branch(&mut func, &mut names, made[0], &[made[1], made[2], made[3]]);
let fusable = fusable(&func, &BRANCH, &mut names);
blocks(&mut func, &BRANCH, &mut names, &fusable);
}
#[test]
#[should_panic(expected = "whose last instruction is not the branch")]
fn a_block_with_two_arms_and_no_branch_in_it_is_refused() {
let (mut names, mut func, made) = blank(3);
let opcode = Opcode::new(names.intern("x64.nop"));
func.build(made[0], opcode).finish();
*func.succs_mut(made[0]) = vec![BlockCall::to(made[1]), BlockCall::to(made[2])];
let fusable = fusable(&func, &BRANCH, &mut names);
blocks(&mut func, &BRANCH, &mut names, &fusable);
}
fn compare(
func: &mut mir::Func,
names: &mut Interner,
block: mir::Block,
arms: &[mir::Block],
) -> Reg {
let byte = func.new_vreg(GPR);
let opcode = Opcode::new(names.intern("x64.cmp_set_l_32"));
func.build(block, opcode)
.def(byte, GPR)
.operand(Operand::read(Reg::physical(RAX), GPR))
.operand(Operand::read(Reg::physical(RCX), GPR))
.finish();
let opcode = Opcode::new(names.intern("x64.br_cond_8"));
func.build(block, opcode).operand(Operand::read(byte, GPR)).finish();
*func.succs_mut(block) = arms.iter().map(|&arm| BlockCall::to(arm)).collect();
byte
}
#[test]
fn a_branch_on_a_comparison_is_the_comparison_and_a_jump_on_what_it_found() {
let (mut names, mut func, made) = blank(3);
compare(&mut func, &mut names, made[0], &[made[1], made[2]]);
let text = laid_out(&mut func, &mut names);
assert_eq!(
text,
[
"block0:",
"x64.cmp_rr_32 $rax, $rcx",
"x64.jcc_ge block2, block1",
"block1:",
"block2:",
]
);
}
#[test]
fn a_comparison_whose_answer_something_else_reads_keeps_its_byte_and_its_test() {
let (mut names, mut func, made) = blank(3);
let byte = compare(&mut func, &mut names, made[0], &[made[1], made[2]]);
let opcode = Opcode::new(names.intern("x64.mov_rr_64"));
func.build(made[1], opcode)
.def(Reg::physical(RAX), GPR)
.operand(Operand::read(byte, GPR))
.finish();
let text = laid_out(&mut func, &mut names);
assert!(text.contains(&"x64.test_rr_8 %0".to_owned()), "{text:?}");
assert!(text.contains(&"x64.jcc_e block2, block1".to_owned()), "{text:?}");
}
#[test]
fn a_comparison_that_is_no_longer_in_front_of_its_branch_keeps_its_test() {
let (mut names, mut func, made) = blank(3);
compare(&mut func, &mut names, made[0], &[made[1], made[2]]);
let fusable = fusable(&func, &BRANCH, &mut names);
assert_eq!(fusable.len(), 1, "the comparison is one the byte's count allows");
let branch = func.terminator(made[0]).expect("a block with two arms has a branch");
let opcode = Opcode::new(names.intern("x64.mov_rr_64"));
let reload = func
.build_loose(opcode)
.def(Reg::physical(RCX), GPR)
.operand(Operand::read(Reg::physical(RAX), GPR))
.finish();
func.insert_before(branch, reload);
blocks(&mut func, &BRANCH, &mut names, &fusable);
let text = mir::print_func(&func, &names, ®S);
assert!(text.contains("x64.cmp_set_l_32"), "{text}");
assert!(text.contains("x64.test_rr_8"), "{text}");
assert!(!text.contains("x64.cmp_rr_32"), "{text}");
}
}