use rucc_ir::{
Block, Builder, Extra, Flags, Func, Inst, IntPred, MemInfo, MemOrder, Opcode, RmwOp, Type,
Value,
};
pub fn loops(func: &mut Func) {
let found: Vec<Inst> = func
.blocks()
.flat_map(|block| func.insts(block).collect::<Vec<_>>())
.filter(|&inst| wanted(func, inst))
.collect();
for inst in found {
rewrite(func, inst);
}
}
fn wanted(func: &Func, inst: Inst) -> bool {
let Extra::Rmw(op, _) = func[inst].extra else { return false };
if matches!(op, RmwOp::Xchg | RmwOp::Add | RmwOp::Sub) {
return false;
}
let Some(old) = func[inst].first_result else { return false };
let ty = func[old].ty;
ty.is_int() && matches!(ty.bits(), 8 | 16 | 32 | 64)
}
fn rewrite(func: &mut Func, inst: Inst) {
let head = func.block_of(inst).expect("the instruction is in a block");
let span = func.span(inst);
let Extra::Rmw(op, mem) = func[inst].extra else { return };
let info = func[mem];
let flags = func[inst].flags;
let [addr, operand] = func[func[inst].args] else { return };
let Some(old) = func[inst].first_result else { return };
let ty = func[old].ty;
let tail: Vec<Inst> = func.insts(head).skip_while(|&at| at != inst).skip(1).collect();
let spin = func.create_block();
let done = func.create_block();
let seen = func.append_param(spin, ty);
let before = func.append_param(done, ty);
func.remove_inst(inst);
for at in tail {
func.remove_inst(at);
func.append_inst(done, at);
}
let mut build = Builder::new(func, head).at(span);
let first = build.atomic_load(ty, addr, MemInfo { order: MemOrder::Relaxed, ..info }, flags);
build.jump(spin, &[first]);
let mut build = Builder::new(func, spin).at(span);
let want = compute(&mut build, op, seen, operand, ty);
let (got, ok) = build.cmpxchg(addr, seen, want, info, flags);
build.br_if(ok, done, &[seen], spin, &[got]);
replace(func, old, before);
}
fn compute(build: &mut Builder<'_>, op: RmwOp, seen: Value, operand: Value, ty: Type) -> Value {
let opcode = match op {
RmwOp::And | RmwOp::Nand => Opcode::And,
RmwOp::Or => Opcode::Or,
RmwOp::Xor => Opcode::Xor,
RmwOp::SMax => return pick(build, IntPred::Sgt, seen, operand),
RmwOp::SMin => return pick(build, IntPred::Slt, seen, operand),
RmwOp::UMax => return pick(build, IntPred::Ugt, seen, operand),
RmwOp::UMin => return pick(build, IntPred::Ult, seen, operand),
_ => unreachable!("the operations with an instruction never reach this pass"),
};
let answer = build.binary(opcode, seen, operand, Flags::NONE);
if op != RmwOp::Nand {
return answer;
}
let ones = build.iconst(ty, -1);
build.binary(Opcode::Xor, answer, ones, Flags::NONE)
}
fn pick(build: &mut Builder<'_>, pred: IntPred, seen: Value, operand: Value) -> Value {
let wins = build.icmp(pred, seen, operand);
build.select(wins, seen, operand)
}
fn replace(func: &mut Func, from: Value, to: Value) {
let blocks: Vec<Block> = func.blocks().collect();
for block in blocks {
let insts: Vec<Inst> = func.insts(block).collect();
for inst in insts {
let mut lists = vec![func[inst].args];
lists.extend(func.successors(inst).map(|call| call.args));
for list in lists {
func.rewrite(list, |value| if value == from { to } else { value });
}
}
}
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{
Builder, Float, Func, MemInfo, MemOrder, Module, Opcode, Restrict, Signature, Type,
};
use rucc_target::{Arch, Env, Os, TargetInfo, Triple};
use super::{Flags, Inst, RmwOp, Value, loops};
fn target() -> TargetInfo {
TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu))
}
fn info(bytes: u64) -> MemInfo {
MemInfo {
size: bytes,
align: u32::try_from(bytes).expect("a small width"),
order: MemOrder::SeqCst,
tbaa: None,
restrict: Restrict::NONE,
}
}
fn built(op: RmwOp, ty: Type) -> (Interner, Func) {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::PTR, ty]).with_returns(&[ty]);
let mut func = Func::new(names.intern("rmw"), signature);
let entry = func.create_block();
let addr = func.append_param(entry, Type::PTR);
let operand = func.append_param(entry, ty);
let bytes = u64::from(ty.bits() / 8);
let mut build = Builder::new(&mut func, entry);
let old = build.atomic_rmw(op, addr, operand, info(bytes), Flags::NONE);
build.ret(&[old]);
(names, func)
}
fn printed(func: &Func, names: &mut Interner) -> String {
let module = Module::new(names.intern("rmw.c"), &target());
rucc_ir::print_func(&module, func, names)
}
fn verified(func: &Func, names: &mut Interner) {
let module = Module::new(names.intern("rmw.c"), &target());
rucc_ir::verify_func(&module, func, names).expect("the rewrite builds valid IR");
}
fn opcodes(func: &Func) -> Vec<Opcode> {
func.blocks().flat_map(|block| func.insts(block).map(|inst| func[inst].opcode)).collect()
}
fn only(func: &Func, opcode: Opcode) -> Inst {
let found: Vec<Inst> = func
.blocks()
.flat_map(|block| func.insts(block).collect::<Vec<_>>())
.filter(|&inst| func[inst].opcode == opcode)
.collect();
assert_eq!(found.len(), 1, "expected one {opcode:?}");
found[0]
}
#[test]
fn an_operation_with_no_instruction_becomes_a_loop() {
for op in [RmwOp::And, RmwOp::Nand, RmwOp::Or, RmwOp::Xor] {
let (mut names, mut func) = built(op, Type::int(32));
loops(&mut func);
verified(&func, &mut names);
let text = printed(&func, &mut names);
assert_eq!(func.blocks().count(), 3, "{op:?}: {text}");
let kinds = opcodes(&func);
assert!(kinds.contains(&Opcode::Cmpxchg), "{op:?}: {text}");
assert!(kinds.contains(&Opcode::AtomicLoad), "{op:?}: {text}");
assert!(!kinds.contains(&Opcode::AtomicRmw), "{op:?}: {text}");
}
}
#[test]
fn the_loop_answers_the_value_that_was_there_before() {
let (mut names, mut func) = built(RmwOp::Or, Type::int(32));
loops(&mut func);
let exchange = only(&func, Opcode::Cmpxchg);
let expected = func[func[exchange].args][1];
let branch = only(&func, Opcode::BrIf);
let taken = func.successors(branch).next().expect("a branch has a first edge");
assert_eq!(func[taken.args], [expected], "{}", printed(&func, &mut names));
let found = func[exchange].first_result.expect("the exchange answers what it found");
let again = func.successors(branch).nth(1).expect("a branch has a second edge");
assert_eq!(func[again.args], [found], "{}", printed(&func, &mut names));
}
#[test]
fn a_use_below_the_loop_reads_the_block_parameter() {
let (mut names, mut func) = built(RmwOp::Xor, Type::int(32));
loops(&mut func);
let ret = only(&func, Opcode::Return);
let block = func.block_of(ret).expect("the return is in a block");
let returned: Vec<Value> = func[func[ret].args].to_vec();
assert_eq!(returned, func[block].params, "{}", printed(&func, &mut names));
}
#[test]
fn a_maximum_or_a_minimum_is_a_compare_and_a_select() {
for op in [RmwOp::SMax, RmwOp::SMin, RmwOp::UMax, RmwOp::UMin] {
let (mut names, mut func) = built(op, Type::int(64));
loops(&mut func);
verified(&func, &mut names);
let kinds = opcodes(&func);
assert!(kinds.contains(&Opcode::ICmp), "{op:?}");
assert!(kinds.contains(&Opcode::Select), "{op:?}");
assert!(kinds.contains(&Opcode::Cmpxchg), "{op:?}");
}
}
#[test]
fn what_has_an_instruction_and_what_has_no_width_are_left_alone() {
for op in [RmwOp::Xchg, RmwOp::Add, RmwOp::Sub] {
let (_, mut func) = built(op, Type::int(32));
loops(&mut func);
assert_eq!(func.blocks().count(), 1, "{op:?} has an instruction");
assert!(opcodes(&func).contains(&Opcode::AtomicRmw), "{op:?}");
}
for op in [RmwOp::FAdd, RmwOp::FSub] {
let (_, mut func) = built(op, Type::float(Float::F64));
loops(&mut func);
assert_eq!(func.blocks().count(), 1, "{op:?} has no width to carry it");
assert!(opcodes(&func).contains(&Opcode::AtomicRmw), "{op:?}");
}
}
}