use rucc_base::Interner;
use rucc_mir::{Func, Inst, Opcode, Reg};
use rucc_target::MachineInsts;
use crate::changes::{Changes, Plan, Reads};
use crate::fold::Pending;
pub const WINDOW: usize = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Fold {
pub from: &'static str,
pub into: &'static str,
pub load: &'static str,
pub commutes: bool,
}
pub static FOLDS: &[Fold] = &[
Fold { from: "add_rr_8", into: "add_rm_8", load: "mov_rm_8", commutes: true },
Fold { from: "add_rr_16", into: "add_rm_16", load: "mov_rm_16", commutes: true },
Fold { from: "add_rr_32", into: "add_rm_32", load: "mov_rm_32", commutes: true },
Fold { from: "add_rr_64", into: "add_rm_64", load: "mov_rm_64", commutes: true },
Fold { from: "sub_rr_8", into: "sub_rm_8", load: "mov_rm_8", commutes: false },
Fold { from: "sub_rr_16", into: "sub_rm_16", load: "mov_rm_16", commutes: false },
Fold { from: "sub_rr_32", into: "sub_rm_32", load: "mov_rm_32", commutes: false },
Fold { from: "sub_rr_64", into: "sub_rm_64", load: "mov_rm_64", commutes: false },
Fold { from: "and_rr_8", into: "and_rm_8", load: "mov_rm_8", commutes: true },
Fold { from: "and_rr_16", into: "and_rm_16", load: "mov_rm_16", commutes: true },
Fold { from: "and_rr_32", into: "and_rm_32", load: "mov_rm_32", commutes: true },
Fold { from: "and_rr_64", into: "and_rm_64", load: "mov_rm_64", commutes: true },
Fold { from: "or_rr_8", into: "or_rm_8", load: "mov_rm_8", commutes: true },
Fold { from: "or_rr_16", into: "or_rm_16", load: "mov_rm_16", commutes: true },
Fold { from: "or_rr_32", into: "or_rm_32", load: "mov_rm_32", commutes: true },
Fold { from: "or_rr_64", into: "or_rm_64", load: "mov_rm_64", commutes: true },
Fold { from: "xor_rr_8", into: "xor_rm_8", load: "mov_rm_8", commutes: true },
Fold { from: "xor_rr_16", into: "xor_rm_16", load: "mov_rm_16", commutes: true },
Fold { from: "xor_rr_32", into: "xor_rm_32", load: "mov_rm_32", commutes: true },
Fold { from: "xor_rr_64", into: "xor_rm_64", load: "mov_rm_64", commutes: true },
Fold { from: "imul_rr_16", into: "imul_rm_16", load: "mov_rm_16", commutes: true },
Fold { from: "imul_rr_32", into: "imul_rm_32", load: "mov_rm_32", commutes: true },
Fold { from: "imul_rr_64", into: "imul_rm_64", load: "mov_rm_64", commutes: true },
];
#[derive(Debug, Clone, Copy)]
struct Waiting {
inst: Inst,
reg: Reg,
load: &'static str,
at: usize,
}
pub fn loads(
func: &mut Func,
machine: &MachineInsts,
names: &mut Interner,
pending: &mut Pending<'_>,
) -> usize {
let mut reads = Reads::of(func);
let mut done = 0;
for block in func.blocks().collect::<Vec<_>>() {
let mut waiting: Option<Waiting> = None;
for (at, inst) in func.insts(block).collect::<Vec<_>>().into_iter().enumerate() {
let name = names.resolve(func[inst].opcode.name()).to_owned();
let bare = machine.bare(&name).to_owned();
let barrier = machine.calls(&name) || !machine.has(&name) || machine.touches_mem(&name);
if let Some(carried) = waiting {
if let Some(plan) = joined(func, &reads, carried, machine, names, inst, &bare) {
let mut set = Changes::new();
set.rewrite(inst, plan);
set.remove(carried.inst);
if set.commit(func, &mut reads, names, machine).is_ok() {
pending.moved(carried.inst, &[inst]);
waiting = None;
done += 1;
}
}
}
if barrier {
waiting = None;
}
if let Some(carried) = waiting {
if at - carried.at >= WINDOW || writes_what_it_reads(func, inst, &carried) {
waiting = None;
}
}
if let Some(load) = FOLDS.iter().find(|fold| fold.load == bare).map(|fold| fold.load) {
let operands = &func[func[inst].operands];
if let Some(first) = operands.first().filter(|operand| operand.role.is_def()) {
waiting = Some(Waiting { inst, reg: first.reg, load, at });
}
}
}
}
done
}
fn writes_what_it_reads(func: &Func, inst: Inst, carried: &Waiting) -> bool {
let written: Vec<Reg> = func[func[inst].operands]
.iter()
.filter(|operand| operand.role.is_def())
.map(|operand| operand.reg)
.collect();
func[func[carried.inst].operands].iter().any(|operand| written.contains(&operand.reg))
}
fn joined(
func: &Func,
reads: &Reads,
carried: Waiting,
machine: &MachineInsts,
names: &mut Interner,
inst: Inst,
bare: &str,
) -> Option<Plan> {
let fold = FOLDS.iter().find(|fold| fold.from == bare)?;
if carried.load != fold.load || reads.count(carried.reg) != 1 {
return None;
}
let operands = func[func[inst].operands].to_vec();
let [answer, first, second] = operands[..] else { return None };
let kept = if second.reg == carried.reg {
first
} else if fold.commutes && first.reg == carried.reg {
second
} else {
return None;
};
let load = carried.inst;
let address = func[func[load].operands][1..].to_vec();
let mut amode = func[func[load].mem?];
amode.base = amode.base.map(|at| at + 1);
amode.index = amode.index.map(|at| at + 1);
let into = names.intern(&format!("{}{}", machine.prefix, fold.into));
Some(Plan {
opcode: Opcode::new(into),
operands: [answer, kept].into_iter().chain(address).collect(),
imm: None,
amode: Some(amode),
symbol: func[load].symbol,
})
}
#[cfg(test)]
mod tests {
use rucc_mir::{self as mir, Constraint, Mem, Operand};
use rucc_target::x86_64::{GPR, MACHINE};
use super::*;
fn empty() -> (Interner, Func, mir::Block) {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"));
let block = func.create_block();
(names, func, block)
}
fn op(names: &mut Interner, name: &str) -> Opcode {
Opcode::new(names.intern(&format!("{}{name}", MACHINE.prefix)))
}
fn load(func: &mut Func, names: &mut Interner, block: mir::Block, base: Reg) -> Reg {
let into = func.new_vreg(GPR);
let mov = op(names, "mov_rm_64");
func.build(block, mov)
.def(into, GPR)
.mem(Mem { disp: 16, ..Mem::at(Operand::read(base, GPR)) })
.finish();
into
}
fn alu(
func: &mut Func,
names: &mut Interner,
block: mir::Block,
name: &str,
first: Reg,
second: Reg,
) -> Reg {
let answer = func.new_vreg(GPR);
let opcode = op(names, name);
func.build(block, opcode)
.operand(Operand::write(answer, GPR).with(Constraint::Reuse(1)))
.uses(first, GPR)
.uses(second, GPR)
.finish();
answer
}
fn shape(func: &Func, names: &Interner, block: mir::Block) -> Vec<String> {
func.insts(block).map(|inst| names.resolve(func[inst].opcode.name()).to_owned()).collect()
}
fn combine(func: &mut Func, names: &mut Interner) -> usize {
let mut addresses = Vec::new();
let mut arguments = Vec::new();
let mut dynamic = Vec::new();
let mut pending =
Pending { addresses: &mut addresses, arguments: &mut arguments, dynamic: &mut dynamic };
loads(func, &MACHINE, names, &mut pending)
}
#[test]
fn a_load_read_once_by_an_addition_becomes_its_memory_operand() {
let (mut names, mut func, block) = empty();
let base = func.new_vreg(GPR);
let other = func.new_vreg(GPR);
let word = load(&mut func, &mut names, block, base);
alu(&mut func, &mut names, block, "add_rr_64", other, word);
assert_eq!(combine(&mut func, &mut names), 1);
assert_eq!(shape(&func, &names, block), ["x64.add_rm_64"]);
let inst = func.insts(block).next().expect("the addition");
let mem = func[inst].mem.expect("the addition reads memory now");
assert_eq!(func[mem].disp, 16, "the load's displacement came with it");
assert_eq!(func[mem].base, Some(2), "and names the operand behind the source it kept");
assert_eq!(func[func[inst].operands][1].reg, other, "the source it kept");
assert_eq!(func[func[inst].operands][2].reg, base, "the address it took on");
}
#[test]
fn a_load_feeding_the_first_source_of_an_addition_is_swapped_and_folded() {
let (mut names, mut func, block) = empty();
let base = func.new_vreg(GPR);
let other = func.new_vreg(GPR);
let word = load(&mut func, &mut names, block, base);
alu(&mut func, &mut names, block, "add_rr_64", word, other);
assert_eq!(combine(&mut func, &mut names), 1);
assert_eq!(shape(&func, &names, block), ["x64.add_rm_64"]);
let inst = func.insts(block).next().expect("the addition");
assert_eq!(func[func[inst].operands][1].reg, other);
}
#[test]
fn a_load_feeding_the_left_of_a_subtraction_stays_a_load() {
let (mut names, mut func, block) = empty();
let base = func.new_vreg(GPR);
let other = func.new_vreg(GPR);
let word = load(&mut func, &mut names, block, base);
alu(&mut func, &mut names, block, "sub_rr_64", word, other);
assert_eq!(combine(&mut func, &mut names), 0);
assert_eq!(shape(&func, &names, block), ["x64.mov_rm_64", "x64.sub_rr_64"]);
}
#[test]
fn a_load_feeding_the_right_of_a_subtraction_folds() {
let (mut names, mut func, block) = empty();
let base = func.new_vreg(GPR);
let other = func.new_vreg(GPR);
let word = load(&mut func, &mut names, block, base);
alu(&mut func, &mut names, block, "sub_rr_64", other, word);
assert_eq!(combine(&mut func, &mut names), 1);
assert_eq!(shape(&func, &names, block), ["x64.sub_rm_64"]);
}
#[test]
fn a_load_two_instructions_read_stays_a_load() {
let (mut names, mut func, block) = empty();
let base = func.new_vreg(GPR);
let other = func.new_vreg(GPR);
let word = load(&mut func, &mut names, block, base);
alu(&mut func, &mut names, block, "add_rr_64", other, word);
alu(&mut func, &mut names, block, "xor_rr_64", other, word);
assert_eq!(combine(&mut func, &mut names), 0);
assert_eq!(
shape(&func, &names, block),
["x64.mov_rm_64", "x64.add_rr_64", "x64.xor_rr_64"]
);
}
#[test]
fn a_load_with_a_store_between_it_and_its_reader_stays_a_load() {
let (mut names, mut func, block) = empty();
let base = func.new_vreg(GPR);
let other = func.new_vreg(GPR);
let word = load(&mut func, &mut names, block, base);
let store = op(&mut names, "mov_mr_64");
func.build(block, store).uses(other, GPR).mem(Mem::at(Operand::read(base, GPR))).finish();
alu(&mut func, &mut names, block, "add_rr_64", other, word);
assert_eq!(combine(&mut func, &mut names), 0);
assert_eq!(
shape(&func, &names, block),
["x64.mov_rm_64", "x64.mov_mr_64", "x64.add_rr_64"]
);
}
#[test]
fn a_load_with_another_load_between_it_and_its_reader_stays_a_load() {
let (mut names, mut func, block) = empty();
let base = func.new_vreg(GPR);
let other = func.new_vreg(GPR);
let word = load(&mut func, &mut names, block, base);
load(&mut func, &mut names, block, other);
alu(&mut func, &mut names, block, "add_rr_64", other, word);
assert_eq!(combine(&mut func, &mut names), 0);
assert_eq!(
shape(&func, &names, block),
["x64.mov_rm_64", "x64.mov_rm_64", "x64.add_rr_64"]
);
}
#[test]
fn the_later_of_two_loads_is_the_one_that_folds() {
let (mut names, mut func, block) = empty();
let base = func.new_vreg(GPR);
let other = func.new_vreg(GPR);
let first = load(&mut func, &mut names, block, base);
let second = load(&mut func, &mut names, block, other);
alu(&mut func, &mut names, block, "add_rr_64", first, second);
assert_eq!(combine(&mut func, &mut names), 1);
assert_eq!(shape(&func, &names, block), ["x64.mov_rm_64", "x64.add_rm_64"]);
let addition = func.insts(block).nth(1).expect("the addition");
assert_eq!(func[func[addition].operands][1].reg, first, "the earlier load is still read");
assert_eq!(func[func[addition].operands][2].reg, other, "and the later one is the address");
}
#[test]
fn a_load_with_a_call_between_it_and_its_reader_stays_a_load() {
let (mut names, mut func, block) = empty();
let base = func.new_vreg(GPR);
let other = func.new_vreg(GPR);
let word = load(&mut func, &mut names, block, base);
let call = op(&mut names, "call");
func.build(block, call).finish();
alu(&mut func, &mut names, block, "add_rr_64", other, word);
assert_eq!(combine(&mut func, &mut names), 0);
assert_eq!(shape(&func, &names, block), ["x64.mov_rm_64", "x64.call", "x64.add_rr_64"]);
}
#[test]
fn a_load_whose_address_register_is_written_between_the_two_stays_a_load() {
let (mut names, mut func, block) = empty();
let base = Reg::physical(rucc_target::x86_64::RSP);
let other = func.new_vreg(GPR);
let word = load(&mut func, &mut names, block, base);
let sub = op(&mut names, "sub_ri_64");
func.build(block, sub)
.operand(Operand::write(base, GPR).with(Constraint::Reuse(1)))
.uses(base, GPR)
.imm(32)
.finish();
alu(&mut func, &mut names, block, "add_rr_64", other, word);
assert_eq!(combine(&mut func, &mut names), 0);
}
#[test]
fn a_load_of_the_wrong_width_stays_a_load() {
let (mut names, mut func, block) = empty();
let base = func.new_vreg(GPR);
let other = func.new_vreg(GPR);
let into = func.new_vreg(GPR);
let narrow = op(&mut names, "mov_rm_32");
func.build(block, narrow).def(into, GPR).mem(Mem::at(Operand::read(base, GPR))).finish();
alu(&mut func, &mut names, block, "add_rr_64", other, into);
assert_eq!(combine(&mut func, &mut names), 0);
assert_eq!(shape(&func, &names, block), ["x64.mov_rm_32", "x64.add_rr_64"]);
}
#[test]
fn a_load_whose_value_an_edge_carries_stays_a_load() {
let (mut names, mut func, block) = empty();
let next = func.create_block();
let base = func.new_vreg(GPR);
let other = func.new_vreg(GPR);
let word = load(&mut func, &mut names, block, base);
alu(&mut func, &mut names, block, "add_rr_64", other, word);
let arrived = func.new_vreg(GPR);
func.params_mut(next).push(mir::Param { reg: arrived, class: GPR });
*func.succs_mut(block) = vec![mir::BlockCall::with(next, vec![word])];
assert_eq!(combine(&mut func, &mut names), 0);
assert_eq!(shape(&func, &names, block), ["x64.mov_rm_64", "x64.add_rr_64"]);
}
#[test]
fn a_reader_in_another_block_stays_where_it_is() {
let (mut names, mut func, block) = empty();
let next = func.create_block();
let base = func.new_vreg(GPR);
let other = func.new_vreg(GPR);
let word = load(&mut func, &mut names, block, base);
alu(&mut func, &mut names, next, "add_rr_64", other, word);
assert_eq!(combine(&mut func, &mut names), 0);
assert_eq!(shape(&func, &names, block), ["x64.mov_rm_64"]);
assert_eq!(shape(&func, &names, next), ["x64.add_rr_64"]);
}
#[test]
fn a_reader_past_the_window_stays_where_it_is() {
let (mut names, mut func, block) = empty();
let base = func.new_vreg(GPR);
let other = func.new_vreg(GPR);
let word = load(&mut func, &mut names, block, base);
let nop = op(&mut names, "nop");
for _ in 0..WINDOW {
func.build(block, nop).finish();
}
alu(&mut func, &mut names, block, "add_rr_64", other, word);
assert_eq!(combine(&mut func, &mut names), 0);
}
#[test]
fn a_reader_at_the_edge_of_the_window_folds() {
let (mut names, mut func, block) = empty();
let base = func.new_vreg(GPR);
let other = func.new_vreg(GPR);
let word = load(&mut func, &mut names, block, base);
let nop = op(&mut names, "nop");
for _ in 0..WINDOW - 1 {
func.build(block, nop).finish();
}
alu(&mut func, &mut names, block, "add_rr_64", other, word);
assert_eq!(combine(&mut func, &mut names), 1);
}
#[test]
fn the_frame_entry_of_a_load_that_moves_goes_with_it() {
let (mut names, mut func, block) = empty();
let base = Reg::physical(rucc_target::x86_64::RSP);
let other = func.new_vreg(GPR);
let word = load(&mut func, &mut names, block, base);
let reader = func.insts(block).nth(1);
assert!(reader.is_none(), "the block holds the load alone so far");
alu(&mut func, &mut names, block, "add_rr_64", other, word);
let held = func.insts(block).next().expect("the load");
let mut addresses = vec![(held, 3usize)];
let mut arguments = Vec::new();
let mut dynamic = Vec::new();
let mut pending =
Pending { addresses: &mut addresses, arguments: &mut arguments, dynamic: &mut dynamic };
assert_eq!(loads(&mut func, &MACHINE, &mut names, &mut pending), 1);
let inst = func.insts(block).next().expect("the addition");
assert_eq!(addresses, [(inst, 3usize)], "the entry names the instruction that took it");
}
#[test]
fn every_row_of_the_table_is_three_instructions_this_target_has() {
for fold in FOLDS {
assert!(MACHINE.has(fold.from), "{} is not an instruction", fold.from);
assert!(MACHINE.has(fold.into), "{} is not an instruction", fold.into);
assert!(MACHINE.has(fold.load), "{} is not an instruction", fold.load);
let width = |name: &str| name.rsplit_once('_').map(|(_, width)| width.to_owned());
assert_eq!(width(fold.from), width(fold.into), "{} changes width", fold.from);
assert_eq!(width(fold.from), width(fold.load), "{} loads another width", fold.from);
assert!((MACHINE.takes_mem)(fold.into), "{} reads no memory", fold.into);
assert!(!(MACHINE.takes_mem)(fold.from), "{} already reads memory", fold.from);
}
}
#[test]
fn the_table_covers_the_arithmetic_this_target_has() {
assert_eq!(FOLDS.len(), 23, "six operations at four widths, less the eight bit multiply");
let commuting = FOLDS.iter().filter(|fold| fold.commutes).count();
assert_eq!(commuting, 19, "everything but the four subtractions");
}
}