use std::collections::HashMap;
use rucc_base::Interner;
use rucc_mir::{self as mir, Role};
use rucc_target::{BranchInsts, FlagInsts, Fusion, MachineInsts};
use crate::changes::{self, Changes, Plan};
#[must_use]
pub fn fusable(
func: &mir::Func,
insts: &BranchInsts,
names: &mut Interner,
) -> HashMap<mir::Inst, usize> {
let compares = compares(insts, names);
let selects = selects(insts, names);
let reads = changes::Reads::of(func);
let mut found = HashMap::new();
for block in func.blocks() {
let mut waiting: HashMap<mir::Reg, (mir::Inst, usize)> = HashMap::new();
for inst in func.insts(block) {
let data = &func[inst];
let operands = &func[data.operands];
if selects.contains(&data.opcode) {
let condition = operands.get(3).map(|operand| operand.reg);
if let Some(entry) = condition.and_then(|reg| waiting.get_mut(®)) {
entry.1 += 1;
}
}
if compares.contains_key(&data.opcode) {
let byte = operands.first().filter(|operand| operand.role != Role::Use);
if let Some(byte) = byte.filter(|byte| byte.reg.is_virtual()) {
waiting.insert(byte.reg, (inst, 0));
}
}
}
for (reg, (compare, count)) in waiting {
if count > 0 && reads.count(reg) == count {
found.insert(compare, count);
}
}
}
found
}
pub fn moves(
func: &mut mir::Func,
insts: &BranchInsts,
flags: &FlagInsts,
machine: &MachineInsts,
names: &mut Interner,
fusable: &HashMap<mir::Inst, usize>,
) -> usize {
if fusable.is_empty() {
return 0;
}
let compares = compares(insts, names);
let kept: HashMap<&str, mir::Opcode> =
insts.fused.iter().map(|fusion| (fusion.cmp, opcode(insts, names, fusion.cmp))).collect();
let chosen: HashMap<(mir::Opcode, &str), mir::Opcode> = insts
.moves
.iter()
.map(|entry| {
let select = opcode(insts, names, entry.select);
((select, entry.when), opcode(insts, names, entry.cmov))
})
.collect();
let names = &*names;
let mut counts = changes::Reads::of(func);
let mut made = 0;
for block in func.blocks().collect::<Vec<_>>() {
let sequence: Vec<mir::Inst> = func.insts(block).collect();
for (at, &compare) in sequence.iter().enumerate() {
let Some(&wanted) = fusable.get(&compare) else { continue };
let Some(&fusion) = compares.get(&func[compare].opcode) else { continue };
let Some(&byte) = func[func[compare].operands].first() else { continue };
let found = reached(func, flags, names, &chosen, fusion, byte, &sequence[at + 1..]);
if found.len() != wanted {
continue;
}
let Some(&cmp) = kept.get(fusion.cmp) else { continue };
let mut set = Changes::new();
set.rewrite(compare, flags_only(func, compare, cmp));
for (select, cmov) in found {
let mut plan = Plan::of(func, select);
plan.operands.truncate(3);
set.rewrite(select, Plan { opcode: cmov, ..plan });
}
if set.commit(func, &mut counts, names, machine).is_ok() {
made += 1;
}
}
}
made
}
fn reached(
func: &mir::Func,
flags: &FlagInsts,
names: &Interner,
chosen: &HashMap<(mir::Opcode, &str), mir::Opcode>,
fusion: &Fusion,
byte: mir::Operand,
after: &[mir::Inst],
) -> Vec<(mir::Inst, mir::Opcode)> {
let place = (byte.class, byte.reg);
let mut found = Vec::new();
for &inst in after {
let data = &func[inst];
let operands = &func[data.operands];
let writes = operands
.iter()
.any(|operand| operand.role != Role::Use && (operand.class, operand.reg) == place);
if let Some(&cmov) = chosen.get(&(data.opcode, fusion.if_true)) {
let [_, false_arm, true_arm, condition] = operands else { break };
let arms = [false_arm, true_arm];
if (condition.class, condition.reg) == place
&& arms.iter().all(|arm| (arm.class, arm.reg) != place)
{
found.push((inst, cmov));
if writes {
break;
}
continue;
}
}
let Some(name) = names.resolve(data.opcode.name()).strip_prefix(flags.prefix) else {
break;
};
if (flags.writes)(name) || writes {
break;
}
}
found
}
fn flags_only(func: &mir::Func, compare: mir::Inst, cmp: mir::Opcode) -> Plan {
let mut plan = Plan::of(func, compare);
plan.operands.remove(0);
plan.amode = plan.amode.map(|mut amode| {
amode.base = amode.base.map(|position| position - 1);
amode.index = amode.index.map(|position| position - 1);
amode
});
Plan { opcode: cmp, ..plan }
}
fn compares(insts: &BranchInsts, names: &mut Interner) -> HashMap<mir::Opcode, &'static Fusion> {
insts.fused.iter().map(|fusion| (opcode(insts, names, fusion.set), fusion)).collect()
}
fn selects(insts: &BranchInsts, names: &mut Interner) -> Vec<mir::Opcode> {
let mut found: Vec<mir::Opcode> =
insts.moves.iter().map(|entry| opcode(insts, names, entry.select)).collect();
found.dedup();
found
}
fn opcode(insts: &BranchInsts, names: &mut Interner, name: &str) -> mir::Opcode {
mir::Opcode::new(names.intern(&format!("{}{name}", insts.prefix)))
}
#[cfg(test)]
mod tests {
use rucc_target::x86_64::{BRANCH, FLAGS, GPR, MACHINE};
use super::*;
fn empty() -> (Interner, mir::Func, mir::Block) {
let mut names = Interner::new();
let mut func = mir::Func::new(names.intern("f"));
let block = func.create_block();
(names, func, block)
}
fn op(names: &mut Interner, name: &str) -> mir::Opcode {
opcode(&BRANCH, names, name)
}
fn fuse(func: &mut mir::Func, names: &mut Interner) -> usize {
let found = fusable(func, &BRANCH, names);
moves(func, &BRANCH, &FLAGS, &MACHINE, names, &found)
}
fn shape(func: &mir::Func, names: &Interner, block: mir::Block) -> Vec<String> {
func.insts(block)
.map(|inst| {
let name = names.resolve(func[inst].opcode.name());
name.strip_prefix(BRANCH.prefix).unwrap_or("").to_owned()
})
.collect()
}
fn tied(f: mir::Reg) -> mir::Operand {
mir::Operand::write(f, GPR).with(rucc_mir::Constraint::Reuse(1))
}
fn compare_and_select(
func: &mut mir::Func,
names: &mut Interner,
block: mir::Block,
condition: &str,
) -> [mir::Reg; 2] {
let [x, y, byte, t, f] = [(); 5].map(|()| func.new_vreg(GPR));
let cmp = op(names, &format!("cmp_set_{condition}_32"));
let select = op(names, "test_cmov_ne_32");
func.build(block, cmp).def(byte, GPR).uses(x, GPR).uses(y, GPR).finish();
func.build(block, select)
.operand(tied(f))
.uses(f, GPR)
.uses(t, GPR)
.uses(byte, GPR)
.finish();
[byte, t]
}
#[test]
fn a_select_on_a_comparison_becomes_a_move_on_its_condition() {
for condition in ["e", "l", "ge", "b", "a"] {
let (mut names, mut func, block) = empty();
compare_and_select(&mut func, &mut names, block, condition);
assert_eq!(fuse(&mut func, &mut names), 1, "{condition}");
let expected = ["cmp_rr_32".to_owned(), format!("cmov_{condition}_32")];
assert_eq!(shape(&func, &names, block), expected);
let last = func.insts(block).last().expect("the move");
assert_eq!(func[func[last].operands].len(), 3);
}
}
#[test]
fn two_selects_on_one_comparison_both_become_moves() {
let (mut names, mut func, block) = empty();
let [byte, _] = compare_and_select(&mut func, &mut names, block, "g");
let [t, f] = [(); 2].map(|()| func.new_vreg(GPR));
let select = op(&mut names, "test_cmov_ne_64");
func.build(block, select)
.operand(tied(f))
.uses(f, GPR)
.uses(t, GPR)
.uses(byte, GPR)
.finish();
assert_eq!(fuse(&mut func, &mut names), 1);
assert_eq!(shape(&func, &names, block), ["cmp_rr_32", "cmov_g_32", "cmov_g_64"]);
}
#[test]
fn a_copy_between_the_comparison_and_the_select_does_not_stop_it() {
let (mut names, mut func, block) = empty();
let [x, y, byte, t, f, spare] = [(); 6].map(|()| func.new_vreg(GPR));
let cmp = op(&mut names, "cmp_set_l_32");
let copy = op(&mut names, "mov_rr_64");
let select = op(&mut names, "test_cmov_ne_32");
func.build(block, cmp).def(byte, GPR).uses(x, GPR).uses(y, GPR).finish();
func.build(block, copy).def(spare, GPR).uses(f, GPR).finish();
func.build(block, select)
.operand(tied(f))
.uses(f, GPR)
.uses(t, GPR)
.uses(byte, GPR)
.finish();
assert_eq!(fuse(&mut func, &mut names), 1);
assert_eq!(shape(&func, &names, block), ["cmp_rr_32", "mov_rr_64", "cmov_l_32"]);
}
#[test]
fn arithmetic_between_the_comparison_and_the_select_keeps_the_test() {
let (mut names, mut func, block) = empty();
let [x, y, byte, t, f] = [(); 5].map(|()| func.new_vreg(GPR));
let cmp = op(&mut names, "cmp_set_l_32");
let add = op(&mut names, "add_rr_32");
let select = op(&mut names, "test_cmov_ne_32");
func.build(block, cmp).def(byte, GPR).uses(x, GPR).uses(y, GPR).finish();
func.build(block, add).def(t, GPR).uses(t, GPR).uses(x, GPR).finish();
func.build(block, select)
.operand(tied(f))
.uses(f, GPR)
.uses(t, GPR)
.uses(byte, GPR)
.finish();
assert_eq!(fuse(&mut func, &mut names), 0);
assert_eq!(shape(&func, &names, block), ["cmp_set_l_32", "add_rr_32", "test_cmov_ne_32"]);
}
#[test]
fn a_byte_something_else_reads_is_kept_and_so_is_the_test() {
let (mut names, mut func, block) = empty();
let [byte, _] = compare_and_select(&mut func, &mut names, block, "e");
let spare = func.new_vreg(GPR);
let copy = op(&mut names, "mov_rr_64");
func.build(block, copy).def(spare, GPR).uses(byte, GPR).finish();
assert_eq!(fuse(&mut func, &mut names), 0);
assert_eq!(shape(&func, &names, block), ["cmp_set_e_32", "test_cmov_ne_32", "mov_rr_64"]);
}
#[test]
fn a_select_that_chooses_the_byte_itself_keeps_the_test() {
let (mut names, mut func, block) = empty();
let [x, y, byte, f] = [(); 4].map(|()| func.new_vreg(GPR));
let cmp = op(&mut names, "cmp_set_ne_32");
let select = op(&mut names, "test_cmov_ne_32");
func.build(block, cmp).def(byte, GPR).uses(x, GPR).uses(y, GPR).finish();
func.build(block, select)
.operand(tied(f))
.uses(f, GPR)
.uses(byte, GPR)
.uses(byte, GPR)
.finish();
assert_eq!(fuse(&mut func, &mut names), 0);
}
#[test]
fn a_register_written_again_before_the_select_keeps_the_test() {
let (mut names, mut func, block) = empty();
let byte = mir::Reg::physical(rucc_target::x86_64::RAX);
let [x, y, t, f, other] = [(); 5].map(|()| func.new_vreg(GPR));
let cmp = op(&mut names, "cmp_set_l_32");
let copy = op(&mut names, "mov_rr_64");
let select = op(&mut names, "test_cmov_ne_32");
func.build(block, cmp).def(byte, GPR).uses(x, GPR).uses(y, GPR).finish();
func.build(block, copy).def(byte, GPR).uses(other, GPR).finish();
func.build(block, select)
.operand(tied(f))
.uses(f, GPR)
.uses(t, GPR)
.uses(byte, GPR)
.finish();
let found = HashMap::from([(func.insts(block).next().expect("the comparison"), 1)]);
assert_eq!(moves(&mut func, &BRANCH, &FLAGS, &MACHINE, &mut names, &found), 0);
assert_eq!(shape(&func, &names, block), ["cmp_set_l_32", "mov_rr_64", "test_cmov_ne_32"]);
}
#[test]
fn a_comparison_against_memory_keeps_its_address() {
let (mut names, mut func, block) = empty();
let [x, base, byte, t, f] = [(); 5].map(|()| func.new_vreg(GPR));
let cmp = op(&mut names, "cmp_set_l_rm_32");
let select = op(&mut names, "test_cmov_ne_32");
let place = mir::Mem { disp: 8, ..mir::Mem::at(mir::Operand::read(base, GPR)) };
let compare = func.build(block, cmp).def(byte, GPR).uses(x, GPR).mem(place).finish();
func.build(block, select)
.operand(tied(f))
.uses(f, GPR)
.uses(t, GPR)
.uses(byte, GPR)
.finish();
assert_eq!(fuse(&mut func, &mut names), 1);
assert_eq!(shape(&func, &names, block), ["cmp_rm_32", "cmov_l_32"]);
let mode = func[func[compare].mem.expect("the address")];
assert_eq!((mode.base, mode.disp), (Some(1), 8));
}
}