use std::collections::HashMap;
use rucc_base::Interner;
use rucc_mir as mir;
use rucc_target::{BitInsts, Constraint, Role};
const EVERYTHING: u32 = u32::MAX;
pub fn dead(func: &mut mir::Func, insts: &BitInsts, names: &Interner) -> usize {
let wanted = demand(func, insts, names);
let mut sent: HashMap<mir::Reg, mir::Reg> = HashMap::new();
let mut gone: Vec<mir::Inst> = Vec::new();
for block in func.blocks() {
for inst in func.insts(block) {
let Some(name) = opcode(func, insts, names, inst) else { continue };
if !(insts.copies_low)(name) {
continue;
}
let Some((def, source)) = conversion(func, inst) else { continue };
let kept = (insts.width)(name, SOURCE).unwrap_or(EVERYTHING);
let read = wanted.get(&def).copied().unwrap_or(0);
if read == 0 || read > kept {
continue;
}
sent.insert(def, source);
gone.push(inst);
}
}
if gone.is_empty() {
return 0;
}
rename(func, &chased(&sent));
for &inst in &gone {
func.remove_inst(inst);
}
gone.len()
}
const SOURCE: u8 = 1;
fn demand(func: &mir::Func, insts: &BitInsts, names: &Interner) -> HashMap<mir::Reg, u32> {
let mut wanted: HashMap<mir::Reg, u32> = HashMap::new();
loop {
let mut moved = false;
for block in func.blocks() {
for inst in func.insts(block) {
let name = opcode(func, insts, names, inst);
let copies = name.is_some_and(|name| (insts.copies_low)(name));
let through = conversion(func, inst)
.filter(|_| copies)
.map_or(EVERYTHING, |(def, _)| wanted.get(&def).copied().unwrap_or(0));
let operands = &func[func[inst].operands];
for (at, operand) in operands.iter().enumerate() {
if operand.role != Role::Use {
continue;
}
let Ok(at) = u8::try_from(at) else { continue };
let asked = read(name, insts, operands, at).min(through);
moved |= raise(&mut wanted, operand.reg, asked);
}
}
for call in &func[block].succs {
for (arg, param) in call.args.iter().zip(&func[call.block].params) {
let asked = wanted.get(¶m.reg).copied().unwrap_or(0);
moved |= raise(&mut wanted, *arg, asked);
}
}
}
if !moved {
return wanted;
}
}
}
fn raise(wanted: &mut HashMap<mir::Reg, u32>, reg: mir::Reg, bits: u32) -> bool {
let had = wanted.entry(reg).or_insert(0);
if *had >= bits {
return false;
}
*had = bits;
true
}
fn read(name: Option<&str>, insts: &BitInsts, operands: &[mir::Operand], at: u8) -> u32 {
let Some(name) = name else { return EVERYTHING };
if let Some(bits) = (insts.width)(name, at) {
return bits;
}
for (index, operand) in operands.iter().enumerate() {
if operand.role == Role::Use || operand.constraint != Constraint::Reuse(at) {
continue;
}
let Ok(index) = u8::try_from(index) else { continue };
return (insts.width)(name, index).unwrap_or(EVERYTHING);
}
EVERYTHING
}
fn opcode<'a>(
func: &mir::Func,
insts: &BitInsts,
names: &'a Interner,
inst: mir::Inst,
) -> Option<&'a str> {
names.resolve(func[inst].opcode.name()).strip_prefix(insts.prefix)
}
fn conversion(func: &mir::Func, inst: mir::Inst) -> Option<(mir::Reg, mir::Reg)> {
if func[inst].mem.is_some() {
return None;
}
let operands = &func[func[inst].operands];
let [def, source] = operands else { return None };
if def.role == Role::Use || source.role != Role::Use {
return None;
}
if !def.reg.is_virtual() || !source.reg.is_virtual() {
return None;
}
Some((def.reg, source.reg))
}
fn chased(sent: &HashMap<mir::Reg, mir::Reg>) -> HashMap<mir::Reg, mir::Reg> {
sent.iter()
.map(|(&from, &first)| {
let mut into = first;
for _ in 0..sent.len() {
match sent.get(&into) {
Some(&next) => into = next,
None => break,
}
}
(from, into)
})
.collect()
}
fn rename(func: &mut mir::Func, sent: &HashMap<mir::Reg, mir::Reg>) {
for block in func.blocks().collect::<Vec<_>>() {
for inst in func.insts(block).collect::<Vec<_>>() {
let operands = func[inst].operands;
for operand in &mut func[operands] {
if operand.role != Role::Use {
continue;
}
if let Some(&into) = sent.get(&operand.reg) {
operand.reg = into;
}
}
}
for call in func.succs_mut(block) {
for arg in &mut call.args {
if let Some(&into) = sent.get(arg) {
*arg = into;
}
}
}
}
}
#[cfg(test)]
mod tests {
use rucc_target::x86_64::{BITS, GPR, RDI};
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 {
mir::Opcode::new(names.intern(&format!("{}{name}", BITS.prefix)))
}
fn takes(func: &mut mir::Func, names: &Interner) -> usize {
dead(func, &BITS, names)
}
fn shape(func: &mir::Func, names: &Interner, block: mir::Block) -> Vec<String> {
func.insts(block).map(|inst| names.resolve(func[inst].opcode.name()).to_owned()).collect()
}
fn reads(func: &mir::Func, inst: mir::Inst) -> Vec<mir::Reg> {
func[func[inst].operands]
.iter()
.filter(|operand| operand.role == Role::Use)
.map(|operand| operand.reg)
.collect()
}
#[test]
fn a_widening_whose_only_reader_is_as_narrow_as_its_source_goes() {
let (mut names, mut func, block) = empty();
let byte = func.new_vreg(GPR);
let wide = func.new_vreg(GPR);
let address = func.new_vreg(GPR);
let widen = op(&mut names, "movzx_8_32");
let store = op(&mut names, "mov_mr_8");
func.build(block, widen).def(wide, GPR).uses(byte, GPR).finish();
func.build(block, store)
.uses(wide, GPR)
.mem(mir::Mem::at(mir::Operand::read(address, GPR)))
.finish();
assert_eq!(takes(&mut func, &names), 1);
let left = shape(&func, &names, block);
assert_eq!(left.len(), 1, "the widening is still there: {left:?}");
let inst = func.insts(block).next().expect("the store is still there");
assert_eq!(reads(&func, inst)[0], byte, "the store was not sent to the source");
}
#[test]
fn a_widening_something_reads_the_whole_of_stays() {
let (mut names, mut func, block) = empty();
let byte = func.new_vreg(GPR);
let wide = func.new_vreg(GPR);
let out = func.new_vreg(GPR);
let widen = op(&mut names, "movzx_8_32");
let copy = op(&mut names, "mov_rr_64");
func.build(block, widen).def(wide, GPR).uses(byte, GPR).finish();
func.build(block, copy).def(out, GPR).uses(wide, GPR).finish();
assert_eq!(takes(&mut func, &names), 0);
assert_eq!(shape(&func, &names, block).len(), 2);
}
#[test]
fn a_widening_whose_narrow_reader_is_in_another_block_goes_too() {
let (mut names, mut func, block) = empty();
let next = func.create_block();
let byte = func.new_vreg(GPR);
let wide = func.new_vreg(GPR);
let arrived = func.new_vreg(GPR);
let address = func.new_vreg(GPR);
let widen = op(&mut names, "movzx_8_32");
let store = op(&mut names, "mov_mr_8");
func.build(block, widen).def(wide, GPR).uses(byte, GPR).finish();
func.params_mut(next).push(mir::Param { reg: arrived, class: GPR });
*func.succs_mut(block) = vec![mir::BlockCall::with(next, vec![wide])];
func.build(next, store)
.uses(arrived, GPR)
.mem(mir::Mem::at(mir::Operand::read(address, GPR)))
.finish();
assert_eq!(takes(&mut func, &names), 1);
assert!(shape(&func, &names, block).is_empty(), "the widening is still there");
assert_eq!(func[block].succs[0].args, vec![byte], "the edge still carries the wide one");
}
#[test]
fn a_widening_whose_reader_in_another_block_is_wide_stays() {
let (mut names, mut func, block) = empty();
let next = func.create_block();
let byte = func.new_vreg(GPR);
let wide = func.new_vreg(GPR);
let arrived = func.new_vreg(GPR);
let address = func.new_vreg(GPR);
let widen = op(&mut names, "movzx_8_32");
let store = op(&mut names, "mov_mr_32");
func.build(block, widen).def(wide, GPR).uses(byte, GPR).finish();
func.params_mut(next).push(mir::Param { reg: arrived, class: GPR });
*func.succs_mut(block) = vec![mir::BlockCall::with(next, vec![wide])];
func.build(next, store)
.uses(arrived, GPR)
.mem(mir::Mem::at(mir::Operand::read(address, GPR)))
.finish();
assert_eq!(takes(&mut func, &names), 0);
assert_eq!(shape(&func, &names, block).len(), 1);
assert_eq!(func[block].succs[0].args, vec![wide]);
}
#[test]
fn a_chain_of_conversions_goes_the_whole_way_and_its_reader_goes_to_the_first_source() {
let (mut names, mut func, block) = empty();
let byte = func.new_vreg(GPR);
let wide = func.new_vreg(GPR);
let narrowed = func.new_vreg(GPR);
let out = func.new_vreg(GPR);
let address = func.new_vreg(GPR);
let widen = op(&mut names, "movzx_8_64");
let low = op(&mut names, "low_32");
let narrow = op(&mut names, "low_8");
let store = op(&mut names, "mov_mr_8");
func.build(block, widen).def(wide, GPR).uses(byte, GPR).finish();
func.build(block, low).def(narrowed, GPR).uses(wide, GPR).finish();
func.build(block, narrow).def(out, GPR).uses(narrowed, GPR).finish();
func.build(block, store)
.uses(out, GPR)
.mem(mir::Mem::at(mir::Operand::read(address, GPR)))
.finish();
assert_eq!(takes(&mut func, &names), 3);
let left = shape(&func, &names, block);
assert_eq!(left.len(), 1, "some of the three are still there: {left:?}");
let inst = func.insts(block).next().expect("the store is still there");
assert_eq!(reads(&func, inst)[0], byte, "the chain was not followed to its end");
}
#[test]
fn a_store_reads_every_bit_of_what_it_stores() {
let (mut names, mut func, block) = empty();
let byte = func.new_vreg(GPR);
let wide = func.new_vreg(GPR);
let address = func.new_vreg(GPR);
let widen = op(&mut names, "movzx_8_64");
let store = op(&mut names, "mov_mr_64");
func.build(block, widen).def(wide, GPR).uses(byte, GPR).finish();
func.build(block, store)
.uses(wide, GPR)
.mem(mir::Mem::at(mir::Operand::read(address, GPR)))
.finish();
assert_eq!(takes(&mut func, &names), 0);
assert_eq!(shape(&func, &names, block).len(), 2);
}
#[test]
fn a_widening_read_as_an_address_stays() {
let (mut names, mut func, block) = empty();
let byte = func.new_vreg(GPR);
let wide = func.new_vreg(GPR);
let out = func.new_vreg(GPR);
let widen = op(&mut names, "movzx_8_64");
let load = op(&mut names, "mov_rm_32");
func.build(block, widen).def(wide, GPR).uses(byte, GPR).finish();
func.build(block, load)
.def(out, GPR)
.mem(mir::Mem::at(mir::Operand::read(wide, GPR)))
.finish();
assert_eq!(takes(&mut func, &names), 0);
assert_eq!(shape(&func, &names, block).len(), 2);
}
#[test]
fn a_tied_operand_reads_as_much_as_the_definition_it_is_tied_to() {
let (mut names, mut func, block) = empty();
let byte = func.new_vreg(GPR);
let wide = func.new_vreg(GPR);
let other = func.new_vreg(GPR);
let sum = func.new_vreg(GPR);
let address = func.new_vreg(GPR);
let widen = op(&mut names, "movzx_8_32");
let add = op(&mut names, "add_rr_32");
let store = op(&mut names, "mov_mr_32");
func.build(block, widen).def(wide, GPR).uses(byte, GPR).finish();
func.build(block, add)
.operand(mir::Operand::write(sum, GPR).with(Constraint::Reuse(1)))
.uses(wide, GPR)
.uses(other, GPR)
.finish();
func.build(block, store)
.uses(sum, GPR)
.mem(mir::Mem::at(mir::Operand::read(address, GPR)))
.finish();
assert_eq!(takes(&mut func, &names), 0);
assert_eq!(shape(&func, &names, block).len(), 3);
}
#[test]
fn a_widening_of_a_physical_register_stays() {
let (mut names, mut func, block) = empty();
let arrived = mir::Reg::physical(RDI);
let wide = func.new_vreg(GPR);
let address = func.new_vreg(GPR);
let widen = op(&mut names, "movzx_8_32");
let store = op(&mut names, "mov_mr_8");
func.build(block, widen).def(wide, GPR).uses(arrived, GPR).finish();
func.build(block, store)
.uses(wide, GPR)
.mem(mir::Mem::at(mir::Operand::read(address, GPR)))
.finish();
assert_eq!(takes(&mut func, &names), 0);
assert_eq!(shape(&func, &names, block).len(), 2);
}
#[test]
fn an_opcode_this_target_does_not_describe_reads_everything() {
let (mut names, mut func, block) = empty();
let byte = func.new_vreg(GPR);
let wide = func.new_vreg(GPR);
let out = func.new_vreg(GPR);
let widen = op(&mut names, "movzx_8_32");
let foreign = mir::Opcode::new(names.intern("elsewhere.narrow"));
func.build(block, widen).def(wide, GPR).uses(byte, GPR).finish();
func.build(block, foreign).def(out, GPR).uses(wide, GPR).finish();
assert_eq!(takes(&mut func, &names), 0);
assert_eq!(shape(&func, &names, block).len(), 2);
}
#[test]
fn a_conversion_nothing_reads_is_left_for_the_pass_that_owns_dead_code() {
let (mut names, mut func, block) = empty();
let byte = func.new_vreg(GPR);
let wide = func.new_vreg(GPR);
let widen = op(&mut names, "movzx_8_32");
func.build(block, widen).def(wide, GPR).uses(byte, GPR).finish();
assert_eq!(takes(&mut func, &names), 0);
assert_eq!(shape(&func, &names, block).len(), 1);
}
}