use std::collections::HashSet;
use rucc_base::Idx;
use rucc_ir::{Block, BlockCall, Def, Func, Inst, Opcode, Value};
use crate::simplify_cfg::{Bindings, Edges, incoming, sweep, taken};
use crate::{Analyses, Fuel, Loops, Pass, Preserved, Stats, uses};
const THREADED: &str =
"edge pointed straight at the arm of the branch it arrives at that it decides";
const NO_FUEL: &str = "edge left on a branch it decides, the pass ran out of fuel";
const WOULD_COPY_EFFECT: &str =
"edge decides the branch it arrives at, but something in the block has to happen on the way";
const WOULD_COPY_READ_BELOW: &str =
"edge decides the branch it arrives at, but a block below reads a value this one defines";
const WOULD_COPY_CARRIED: &str =
"edge decides the branch it arrives at, but the arm carries a value the block works out";
const WOULD_BREAK_A_LOOP: &str =
"edge decides the branch it arrives at, but threading it would give a loop a second way in";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Thread;
impl Pass for Thread {
fn name(&self) -> &'static str {
"thread"
}
fn describe(&self) -> &'static str {
"an edge that already decides the branch it arrives at is pointed at the arm that branch \
would have taken"
}
fn preserves(&self) -> Preserved {
Preserved::NONE
}
fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
let mut stats = Stats::new();
let Some(entry) = func.entry() else { return stats };
let mut edges: Edges = incoming(func);
let leaky = leaky(func);
let unbound = Bindings::new();
let mut threaded = false;
'blocks: for block in func.blocks().collect::<Vec<Block>>() {
if block == entry || func[block].params.is_empty() {
continue;
}
let Some(term) = func.terminator(block) else { continue };
if !matches!(func[term].opcode, Opcode::BrIf | Opcode::Switch) {
continue;
}
if taken(func, term, &unbound).is_some() {
continue;
}
let copied = if !skippable(func, block) {
Some(WOULD_COPY_EFFECT)
} else if leaky.contains(&block) {
Some(WOULD_COPY_READ_BELOW)
} else {
None
};
for (from, at) in edges.get(&block).cloned().unwrap_or_default() {
if from == block {
continue;
}
let subst = bind(func, block, at);
let Some(call) = taken(func, term, &subst) else { continue };
if call.block == block {
continue;
}
if let Some(reason) = copied {
stats.missed(reason);
continue;
}
let Some(args) = carried(func, block, call, &subst) else {
stats.missed(WOULD_COPY_CARRIED);
continue;
};
if !allowed(an.loops(func), from, call.block) {
stats.missed(WOULD_BREAK_A_LOOP);
continue;
}
if !fuel.take() {
stats.missed(NO_FUEL);
break 'blocks;
}
let args = func.push_values(&args);
func.set_block_call(at, BlockCall { block: call.block, args });
if let Some(list) = edges.get_mut(&block) {
list.retain(|&(_, slot)| slot != at);
}
edges.entry(call.block).or_default().push((from, at));
an.clear();
stats.optimized(THREADED);
threaded = true;
}
}
if threaded {
sweep(func, an, &mut stats);
}
stats
}
}
fn bind(func: &Func, block: Block, at: Idx<BlockCall>) -> Bindings {
let args = func[at].args;
let params = func[block].params.iter().copied();
params.zip(func[args].iter().copied()).collect()
}
fn carried(func: &Func, block: Block, call: BlockCall, subst: &Bindings) -> Option<Vec<Value>> {
let mut out = Vec::with_capacity(func[call.args].len());
for &arg in &func[call.args] {
if let Some(&bound) = subst.get(&arg) {
out.push(bound);
continue;
}
if let Def::Result { inst, .. } = func[arg].def {
if func.block_of(inst) == Some(block) {
return None;
}
}
out.push(arg);
}
Some(out)
}
fn skippable(func: &Func, block: Block) -> bool {
func.insts(block).all(|inst| func.is_terminator(inst) || !func[inst].opcode.has_effects())
}
fn leaky(func: &Func) -> HashSet<Block> {
let mut out = HashSet::new();
for block in func.blocks().collect::<Vec<Block>>() {
for inst in func.insts(block).collect::<Vec<Inst>>() {
uses::operands(func, inst, |value| {
if let Some(home) = defined_in(func, value) {
if home != block {
out.insert(home);
}
}
});
}
}
out
}
fn defined_in(func: &Func, value: Value) -> Option<Block> {
match func[value].def {
Def::Result { inst, .. } => func.block_of(inst),
Def::Param { block, .. } => Some(block),
}
}
fn allowed(loops: &Loops, from: Block, into: Block) -> bool {
if loops.is_irreducible(from) || loops.is_irreducible(into) {
return false;
}
if loops.all().any(|id| loops.latches(id).contains(&from)) {
return false;
}
let mut id = loops.innermost(into);
while let Some(loop_id) = id {
if !loops.contains(loop_id, from) && loops.header(loop_id) != into {
return false;
}
id = loops.parent(loop_id);
}
true
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{
Block, Builder, Flags, Func, IntPred, MemInfo, MemOrder, Restrict, Signature, Type, Value,
};
use super::Thread;
use crate::stats::Kind;
use crate::{Analyses, Fuel, Pass, Stats};
fn thread(func: &mut Func) -> Stats {
Thread.run(func, &mut Analyses::new(), &mut Fuel::unlimited())
}
fn blocks(func: &Func) -> Vec<usize> {
func.blocks().map(Block::index).collect()
}
fn goes_to(func: &Func, block: usize) -> Vec<usize> {
let block = Block::from_usize(block);
let term = func.terminator(block).expect("every block here has one");
func.successors(term).map(|call| call.block.index()).collect()
}
fn carries(func: &Func, block: usize) -> Vec<Value> {
let block = Block::from_usize(block);
let term = func.terminator(block).expect("every block here has one");
let call = func.successors(term).next().expect("a terminator here has an edge");
func[call.args].to_vec()
}
fn diamond(left: i128, right: i128) -> (Func, [Value; 2]) {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
let param = func.append_param(join, Type::int(32));
let yes = func.create_block();
let no = func.create_block();
let mut build = Builder::new(&mut func, entry);
let cond = build.iconst(Type::int(1), 1);
build.br_if(cond, arms[0], &[], arms[1], &[]);
let mut sent = Vec::new();
for (arm, value) in arms.iter().zip([left, right]) {
let mut build = Builder::new(&mut func, *arm);
let it = build.iconst(Type::int(32), value);
sent.push(it);
build.jump(join, &[it]);
}
let mut build = Builder::new(&mut func, join);
let one = build.iconst(Type::int(32), 1);
let test = build.icmp(IntPred::Eq, param, one);
build.br_if(test, yes, &[], no, &[]);
for block in [yes, no] {
let mut build = Builder::new(&mut func, block);
build.ret(&[]);
}
(func, [sent[0], sent[1]])
}
#[test]
fn both_edges_of_a_join_that_decides_its_test_are_threaded() {
let (mut func, _) = diamond(1, 2);
let stats = thread(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::THREADED), 2);
assert_eq!(goes_to(&func, 1), vec![4]);
assert_eq!(goes_to(&func, 2), vec![5]);
assert_eq!(blocks(&func), vec![0, 1, 2, 4, 5]);
assert_eq!(stats.count(Kind::Optimized, crate::simplify_cfg::REMOVED), 1);
}
#[test]
fn an_edge_that_does_not_decide_the_test_is_left_alone() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let outside = func.append_param(entry, Type::int(32));
let join = func.create_block();
let param = func.append_param(join, Type::int(32));
let yes = func.create_block();
let no = func.create_block();
let mut build = Builder::new(&mut func, entry);
build.jump(join, &[outside]);
let mut build = Builder::new(&mut func, join);
let one = build.iconst(Type::int(32), 1);
let test = build.icmp(IntPred::Eq, param, one);
build.br_if(test, yes, &[], no, &[]);
for block in [yes, no] {
let mut build = Builder::new(&mut func, block);
build.ret(&[]);
}
let stats = thread(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::THREADED), 0);
assert_eq!(goes_to(&func, 0), vec![1]);
}
#[test]
fn a_branch_decided_whichever_way_control_arrived_is_left_to_simplify_cfg() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
func.append_param(join, Type::int(32));
let yes = func.create_block();
let no = func.create_block();
let mut build = Builder::new(&mut func, entry);
let cond = build.iconst(Type::int(1), 1);
build.br_if(cond, arms[0], &[], arms[1], &[]);
for (arm, value) in arms.iter().zip([1, 2]) {
let mut build = Builder::new(&mut func, *arm);
let it = build.iconst(Type::int(32), value);
build.jump(join, &[it]);
}
let mut build = Builder::new(&mut func, join);
let known = build.iconst(Type::int(1), 1);
build.br_if(known, yes, &[], no, &[]);
for block in [yes, no] {
let mut build = Builder::new(&mut func, block);
build.ret(&[]);
}
let stats = thread(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::THREADED), 0);
assert_eq!(goes_to(&func, 1), vec![3]);
assert_eq!(goes_to(&func, 2), vec![3]);
}
#[test]
fn a_block_with_something_that_happens_in_it_needs_the_copy() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
let param = func.append_param(join, Type::int(32));
let yes = func.create_block();
let no = func.create_block();
let mut build = Builder::new(&mut func, entry);
let cond = build.iconst(Type::int(1), 1);
build.br_if(cond, arms[0], &[], arms[1], &[]);
for (arm, value) in arms.iter().zip([1, 2]) {
let mut build = Builder::new(&mut func, *arm);
let it = build.iconst(Type::int(32), value);
build.jump(join, &[it]);
}
let mut build = Builder::new(&mut func, join);
let what = build.iconst(Type::int(32), 7);
let address = build.iconst(Type::int(64), 16);
let address = build.unary(rucc_ir::Opcode::IntToPtr, address, Type::PTR);
let info = MemInfo {
size: 4,
align: 4,
order: MemOrder::NotAtomic,
tbaa: None,
restrict: Restrict::NONE,
};
build.store(what, address, info, Flags::NONE);
let one = build.iconst(Type::int(32), 1);
let test = build.icmp(IntPred::Eq, param, one);
build.br_if(test, yes, &[], no, &[]);
for block in [yes, no] {
let mut build = Builder::new(&mut func, block);
build.ret(&[]);
}
let stats = thread(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::THREADED), 0);
assert_eq!(stats.count(Kind::Missed, super::WOULD_COPY_EFFECT), 2);
}
#[test]
fn a_value_the_block_defines_and_something_below_it_reads_needs_the_copy() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
let param = func.append_param(join, Type::int(32));
let yes = func.create_block();
let no = func.create_block();
let mut build = Builder::new(&mut func, entry);
let cond = build.iconst(Type::int(1), 1);
build.br_if(cond, arms[0], &[], arms[1], &[]);
for (arm, value) in arms.iter().zip([1, 2]) {
let mut build = Builder::new(&mut func, *arm);
let it = build.iconst(Type::int(32), value);
build.jump(join, &[it]);
}
let mut build = Builder::new(&mut func, join);
let one = build.iconst(Type::int(32), 1);
let test = build.icmp(IntPred::Eq, param, one);
build.br_if(test, yes, &[], no, &[]);
let mut build = Builder::new(&mut func, yes);
build.ret(&[]);
let mut build = Builder::new(&mut func, no);
build.ret(&[param]);
let stats = thread(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::THREADED), 0);
assert_eq!(stats.count(Kind::Missed, super::WOULD_COPY_READ_BELOW), 2);
assert_eq!(goes_to(&func, 1), vec![3]);
assert_eq!(goes_to(&func, 2), vec![3]);
}
#[test]
fn an_arm_carrying_a_value_the_block_worked_out_needs_the_copy() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
let param = func.append_param(join, Type::int(32));
let yes = func.create_block();
func.append_param(yes, Type::int(32));
let no = func.create_block();
let mut build = Builder::new(&mut func, entry);
let cond = build.iconst(Type::int(1), 1);
build.br_if(cond, arms[0], &[], arms[1], &[]);
for (arm, value) in arms.iter().zip([1, 2]) {
let mut build = Builder::new(&mut func, *arm);
let it = build.iconst(Type::int(32), value);
build.jump(join, &[it]);
}
let mut build = Builder::new(&mut func, join);
let one = build.iconst(Type::int(32), 1);
let test = build.icmp(IntPred::Eq, param, one);
let sum = build.binary(rucc_ir::Opcode::Add, param, one, Flags::NONE);
build.br_if(test, yes, &[sum], no, &[]);
for block in [yes, no] {
let mut build = Builder::new(&mut func, block);
build.ret(&[]);
}
let stats = thread(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::THREADED), 1);
assert_eq!(stats.count(Kind::Missed, super::WOULD_COPY_CARRIED), 1);
assert_eq!(goes_to(&func, 2), vec![5]);
assert_eq!(goes_to(&func, 1), vec![3]);
}
#[test]
fn the_block_parameter_is_substituted_into_what_the_arm_carries() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
let param = func.append_param(join, Type::int(32));
let yes = func.create_block();
func.append_param(yes, Type::int(32));
let no = func.create_block();
let mut build = Builder::new(&mut func, entry);
let cond = build.iconst(Type::int(1), 1);
build.br_if(cond, arms[0], &[], arms[1], &[]);
let mut sent = Vec::new();
for (arm, value) in arms.iter().zip([1, 2]) {
let mut build = Builder::new(&mut func, *arm);
let it = build.iconst(Type::int(32), value);
sent.push(it);
build.jump(join, &[it]);
}
let mut build = Builder::new(&mut func, join);
let one = build.iconst(Type::int(32), 1);
let test = build.icmp(IntPred::Eq, param, one);
build.br_if(test, yes, &[param], no, &[]);
for block in [yes, no] {
let mut build = Builder::new(&mut func, block);
build.ret(&[]);
}
let stats = thread(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::THREADED), 2);
assert_eq!(goes_to(&func, 1), vec![4]);
assert_eq!(carries(&func, 1), vec![sent[0]]);
}
#[test]
fn a_switch_the_edge_decides_is_threaded() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let arms = [func.create_block(), func.create_block()];
let join = func.create_block();
let param = func.append_param(join, Type::int(32));
let cases = [func.create_block(), func.create_block(), func.create_block()];
let mut build = Builder::new(&mut func, entry);
let cond = build.iconst(Type::int(1), 1);
build.br_if(cond, arms[0], &[], arms[1], &[]);
for (arm, value) in arms.iter().zip([0, 1]) {
let mut build = Builder::new(&mut func, *arm);
let it = build.iconst(Type::int(32), value);
build.jump(join, &[it]);
}
let mut build = Builder::new(&mut func, join);
build.switch(param, cases[0], &[(0, cases[1]), (1, cases[2])]);
for block in cases {
let mut build = Builder::new(&mut func, block);
build.ret(&[]);
}
let stats = thread(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::THREADED), 2);
assert_eq!(goes_to(&func, 1), vec![cases[1].index()]);
assert_eq!(goes_to(&func, 2), vec![cases[2].index()]);
}
fn loop_with_a_parameter(arm: usize) -> Func {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let outside = func.append_param(entry, Type::int(32));
let header = func.create_block();
let param = func.append_param(header, Type::int(32));
let body = func.create_block();
let out = func.create_block();
let elsewhere = func.create_block();
let taken = [entry, header, body, out, elsewhere][arm];
let mut build = Builder::new(&mut func, entry);
let one = build.iconst(Type::int(32), 1);
build.jump(header, &[one]);
let mut build = Builder::new(&mut func, header);
let lit = build.iconst(Type::int(32), 1);
let test = build.icmp(IntPred::Eq, param, lit);
build.br_if(test, taken, &[], out, &[]);
let mut build = Builder::new(&mut func, body);
build.jump(header, &[outside]);
for block in [out, elsewhere] {
let mut build = Builder::new(&mut func, block);
build.ret(&[]);
}
func
}
#[test]
fn threading_into_a_loop_anywhere_but_its_header_is_refused() {
let mut func = loop_with_a_parameter(2);
let stats = thread(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::THREADED), 0);
assert_eq!(stats.count(Kind::Missed, super::WOULD_BREAK_A_LOOP), 1);
assert_eq!(goes_to(&func, 0), vec![1]);
}
#[test]
fn threading_onto_a_block_outside_the_loop_is_allowed() {
let mut func = loop_with_a_parameter(4);
let stats = thread(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::THREADED), 1);
assert_eq!(goes_to(&func, 0), vec![4]);
}
#[test]
fn threading_onto_the_header_of_a_loop_is_allowed() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let join = func.create_block();
let param = func.append_param(join, Type::int(32));
let header = func.create_block();
let out = func.create_block();
let mut build = Builder::new(&mut func, entry);
let one = build.iconst(Type::int(32), 1);
build.jump(join, &[one]);
let mut build = Builder::new(&mut func, join);
let lit = build.iconst(Type::int(32), 1);
let test = build.icmp(IntPred::Eq, param, lit);
build.br_if(test, header, &[], out, &[]);
let mut build = Builder::new(&mut func, header);
let again = build.iconst(Type::int(1), 1);
build.br_if(again, header, &[], out, &[]);
let mut build = Builder::new(&mut func, out);
build.ret(&[]);
let stats = thread(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::THREADED), 1);
assert_eq!(goes_to(&func, 0), vec![2]);
}
#[test]
fn fuel_stops_the_threading_where_it_stands() {
let (mut func, _) = diamond(1, 2);
let mut fuel = Fuel::of(1);
let stats = Thread.run(&mut func, &mut Analyses::new(), &mut fuel);
assert_eq!(stats.count(Kind::Optimized, super::THREADED), 1);
assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
assert_eq!(goes_to(&func, 2), vec![3], "the second edge is where it was");
}
}