use std::collections::{HashMap, HashSet};
use rucc_base::Idx;
use rucc_cost::heuristics;
use rucc_ir::{Block, BlockCall, Builder, Def, Func, Inst, Opcode, Start, Value, ValueList};
use crate::frontier::Frontiers;
use crate::header_copy::{clone_into, repeatable};
use crate::simplify_cfg::{Bindings, Edges, incoming, sweep, taken};
use crate::{Analyses, Cfg, Dominators, 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";
const COPIED: &str =
"edge pointed at a copy of the block it arrives at that goes straight to the arm it decides";
const ODD: &str =
"edge decides the branch it arrives at, but the block has something in it that is not copied";
const TOO_BIG: &str =
"edge decides the branch it arrives at, but the block is larger than a thread may copy";
const TOO_LONG: &str =
"edge decides the branch it arrives at, but the path of copies it is on would be too long";
const TOO_MANY: &str =
"edge decides the branch it arrives at, but this function has had its 64 copies";
#[derive(Debug)]
pub struct Thread {
name: &'static str,
budget: u32,
}
pub static FREE: Thread = Thread { name: "thread", budget: 0 };
pub static COPY: Thread =
Thread { name: "thread-copy", budget: heuristics::JUMP_THREAD_DUPLICATION_INSNS };
impl Pass for Thread {
fn name(&self) -> &'static str {
self.name
}
fn describe(&self) -> &'static str {
if self.budget == 0 {
"an edge that already decides the branch it arrives at is pointed at the arm that \
branch would have taken"
} else {
"an edge that already decides the branch it arrives at is pointed at the arm that \
branch would have taken, through a copy of the block if the block's values are read"
}
}
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 mut leaks = leaky(func);
let unbound = Bindings::new();
let mut threaded = false;
let mut paths: HashMap<Block, u32> = HashMap::new();
let mut copies = 0;
'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 effect = !skippable(func, block);
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 effect {
stats.missed(WOULD_COPY_EFFECT);
continue;
}
let (free, why) = if leaks.contains(&block) {
(None, WOULD_COPY_READ_BELOW)
} else {
(carried(func, block, call, &subst), WOULD_COPY_CARRIED)
};
let path = if free.is_some() {
0
} else {
match self.path(func, an.loops(func), block, from, call.block, &paths, copies) {
Ok(path) => path,
Err(reason) => {
stats.missed(reason.unwrap_or(why));
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;
}
if let Some(list) = edges.get_mut(&block) {
list.retain(|&(_, slot)| slot != at);
}
if let Some(args) = free {
let args = func.push_values(&args);
func.set_block_call(at, BlockCall { args, ..call });
edges.entry(call.block).or_default().push((from, at));
stats.optimized(THREADED);
} else {
let (copy, out) = copy(func, block, at, call, &subst);
edges.entry(call.block).or_default().push((copy, out));
paths.insert(copy, path);
copies += 1;
leaks = leaky(func);
stats.optimized(COPIED);
}
an.clear();
threaded = true;
}
}
if threaded {
sweep(func, an, &mut stats);
}
stats
}
}
impl Thread {
#[allow(clippy::too_many_arguments)]
fn path(
&self,
func: &Func,
loops: &Loops,
block: Block,
from: Block,
into: Block,
paths: &HashMap<Block, u32>,
copies: u32,
) -> Result<u32, Option<&'static str>> {
if self.budget == 0 {
return Err(None);
}
let body: Vec<Inst> = func.insts(block).filter(|&inst| !func.is_terminator(inst)).collect();
if !body.iter().all(|&inst| repeatable(func, inst)) {
return Err(Some(ODD));
}
let mut cost = u32::try_from(body.len()).unwrap_or(u32::MAX);
if back_edge(loops, block, into) {
cost = cost.saturating_mul(heuristics::JUMP_THREAD_BACK_EDGE_SCALE);
}
if cost > self.budget {
return Err(Some(TOO_BIG));
}
let path = paths.get(&from).copied().unwrap_or(0).saturating_add(cost);
if path > heuristics::JUMP_THREAD_PATH_INSNS {
return Err(Some(TOO_LONG));
}
if copies >= heuristics::JUMP_THREAD_PATHS {
return Err(Some(TOO_MANY));
}
Ok(path)
}
}
fn back_edge(loops: &Loops, from: Block, into: Block) -> bool {
let mut id = loops.innermost(from);
while let Some(loop_id) = id {
if loops.header(loop_id) == into {
return true;
}
id = loops.parent(loop_id);
}
false
}
fn copy(
func: &mut Func,
block: Block,
at: Idx<BlockCall>,
call: BlockCall,
subst: &Bindings,
) -> (Block, Idx<BlockCall>) {
let term = func.terminator(block).expect("the block was chosen for its terminator");
let mut map = subst.clone();
let copy = func.create_block();
let insts: Vec<Inst> = func.insts(block).filter(|&inst| inst != term).collect();
for inst in insts {
clone_into(func, copy, inst, &mut map);
}
let args: Vec<Value> =
func[call.args].iter().map(|value| map.get(value).copied().unwrap_or(*value)).collect();
let jump = Builder::new(func, copy).jump(call.block, &args);
let out = func.target_list(jump).iter().next().expect("a jump has one edge");
let edge = func[at];
func.set_block_call(at, BlockCall { block: copy, args: ValueList::EMPTY, ..edge });
repair(func, block, copy, &map);
(copy, out)
}
fn repair(func: &mut Func, block: Block, copy: Block, map: &Bindings) {
let values = read_outside(func, block, copy);
if values.is_empty() {
return;
}
let cfg = Cfg::new(func);
let dom = Dominators::new(&cfg);
let frontiers = Frontiers::new(&cfg, &dom);
let mut joins: HashSet<Block> = HashSet::new();
let mut work = vec![block, copy];
while let Some(at) = work.pop() {
for &join in frontiers.of(at) {
if joins.insert(join) {
work.push(join);
}
}
}
for value in values {
let copied = map.get(&value).copied().expect("the copy defines every value the block does");
let mut reaching = Reaching {
dom: &dom,
block,
copy,
value,
copied,
params: HashMap::new(),
memo: HashMap::new(),
};
merge(func, &cfg, &joins, &mut reaching);
}
}
fn read_outside(func: &Func, block: Block, copy: Block) -> Vec<Value> {
let mut seen = HashSet::new();
let mut out = Vec::new();
for other in func.blocks() {
if other == block || other == copy {
continue;
}
for inst in func.insts(other) {
uses::operands(func, inst, |value| {
if defined_in(func, value) == Some(block) && seen.insert(value) {
out.push(value);
}
});
}
}
out
}
struct Reaching<'a> {
dom: &'a Dominators,
block: Block,
copy: Block,
value: Value,
copied: Value,
params: HashMap<Block, Value>,
memo: HashMap<Block, Value>,
}
impl Reaching<'_> {
fn start(&mut self, of: Block) -> Value {
let mut chain = Vec::new();
let mut at = of;
let found = loop {
if let Some(¶m) = self.params.get(&at) {
break param;
}
if let Some(&known) = self.memo.get(&at) {
break known;
}
chain.push(at);
match self.dom.immediate_dominator(at) {
Some(up) if up == self.block => break self.value,
Some(up) if up == self.copy => break self.copied,
Some(up) => at = up,
None => break self.value,
}
};
for at in chain {
self.memo.insert(at, found);
}
found
}
fn end(&mut self, of: Block) -> Value {
if of == self.block {
self.value
} else if of == self.copy {
self.copied
} else {
self.start(of)
}
}
}
fn merge(func: &mut Func, cfg: &Cfg, joins: &HashSet<Block>, reaching: &mut Reaching<'_>) {
let (block, copy, value) = (reaching.block, reaching.copy, reaching.value);
let mut readers = Vec::new();
for other in func.blocks() {
if other == block || other == copy {
continue;
}
let mut reads = false;
for inst in func.insts(other) {
uses::operands(func, inst, |used| reads |= used == value);
}
if reads {
readers.push(other);
}
}
let mut live: HashSet<Block> = readers.iter().copied().collect();
let mut work = readers.clone();
while let Some(at) = work.pop() {
for &pred in cfg.predecessors(at) {
if pred != block && pred != copy && live.insert(pred) {
work.push(pred);
}
}
}
let mut places: Vec<Block> = joins
.iter()
.copied()
.filter(|&join| join != block && join != copy && live.contains(&join))
.collect();
places.sort_by_key(|join| join.index());
let ty = func[value].ty;
let decls: Vec<u32> = func.value_decls(value).collect();
for &place in &places {
let param = func.append_param(place, ty);
for &decl in &decls {
func.declare_value(param, decl);
}
reaching.params.insert(place, param);
}
for &reader in &readers {
let now = reaching.start(reader);
if now == value {
continue;
}
let swap = |had: Value| if had == value { now } else { had };
for inst in func.insts(reader).collect::<Vec<Inst>>() {
func.rewrite(func[inst].args, swap);
for at in func.target_list(inst).iter() {
func.rewrite(func[at].args, swap);
}
}
}
for other in func.blocks().collect::<Vec<Block>>() {
let Some(term) = func.terminator(other) else { continue };
for at in func.target_list(term).iter() {
let call = func[at];
if !reaching.params.contains_key(&call.block) {
continue;
}
let carry = reaching.end(other);
let args = func.append_arg(call.args, carry);
func.set_block_call(at, BlockCall { args, ..call });
}
}
let starts: Vec<(Start, Value)> = func
.value_starts(value)
.filter_map(|start| {
let at = func.start_place(start).map_or(start.block, |(at, _)| at);
if at == block || at == copy {
return None;
}
let now = reaching.start(at);
(now != value).then_some((start, now))
})
.collect();
let mut targets: Vec<Value> = starts.iter().map(|&(_, now)| now).collect();
targets.dedup();
for target in targets {
let which: Vec<Start> =
starts.iter().filter(|&&(_, now)| now == target).map(|&(start, _)| start).collect();
if !which.is_empty() {
func.move_starts(value, target, &which);
}
}
}
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 std::collections::HashMap;
use rucc_ir::{Module, verify_func};
use rucc_target::{TargetInfo, Triple};
use super::{COPY, FREE, Thread};
use crate::stats::Kind;
use crate::{Fuel, Pass, Stats};
fn thread(func: &mut Func) -> Stats {
FREE.run(func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
}
fn copying(func: &mut Func) -> Stats {
copying_with(©, func)
}
fn copying_with(pass: &Thread, func: &mut Func) -> Stats {
let stats =
pass.run(func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
let mut names = Interner::new();
let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
let module = Module::new(names.intern("t.c"), &target);
if let Err(errors) = verify_func(&module, func, &names) {
panic!("{errors:#?}");
}
stats
}
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,
owns: 0,
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);
}
fn clamp() -> Func {
clamp_with(|_, _| {})
}
fn clamp_with(extra: impl FnOnce(&mut Builder<'_>, Value)) -> Func {
let mut names = Interner::new();
let signature = Signature::new().with_returns(&[Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
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);
extra(&mut build, param);
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);
let floor = build.iconst(Type::int(32), 15);
build.ret(&[floor]);
let mut build = Builder::new(&mut func, no);
build.ret(&[param]);
func
}
#[test]
fn a_value_the_block_defines_and_something_below_it_reads_needs_the_copy() {
let mut func = clamp();
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 a_copy_threads_the_clamp_and_the_read_below_gets_a_merge() {
let mut func = clamp();
let stats = copying(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::COPIED), 2, "{stats:?}");
assert_eq!(stats.count(Kind::Missed, super::WOULD_COPY_READ_BELOW), 0);
assert!(!blocks(&func).contains(&3), "{:?}", blocks(&func));
let first = goes_to(&func, 1)[0];
let second = goes_to(&func, 2)[0];
assert_eq!(goes_to(&func, first), vec![4]);
assert_eq!(goes_to(&func, second), vec![5]);
let returned = Block::from_usize(5);
let term = func.terminator(returned).expect("a return");
let read = func[func[term].args][0];
assert_eq!(super::defined_in(&func, read), Some(Block::from_usize(2)));
}
#[test]
fn a_copy_is_not_made_of_a_block_with_something_that_happens_in_it() {
let mut func = clamp_with(|build, _| {
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,
owns: 0,
restrict: Restrict::NONE,
};
build.store(what, address, info, Flags::NONE);
});
let stats = copying(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::COPIED), 0);
assert_eq!(stats.count(Kind::Missed, super::WOULD_COPY_EFFECT), 2);
}
#[test]
fn a_block_larger_than_the_budget_is_not_copied() {
for (adds, copied) in [(13, 2), (14, 0)] {
let mut func = clamp_with(|build, param| {
let mut sum = param;
for _ in 0..adds {
sum = build.binary(rucc_ir::Opcode::Add, sum, param, Flags::NONE);
}
});
let stats = copying(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::COPIED), copied, "{adds}: {stats:?}");
if copied == 0 {
assert_eq!(stats.count(Kind::Missed, super::TOO_BIG), 2);
}
}
}
#[test]
fn a_path_of_copies_may_not_grow_past_its_limit() {
let func = clamp();
let an = crate::machine::fixtures::analyses();
let join = Block::from_usize(3);
let from = Block::from_usize(1);
let into = Block::from_usize(5);
let loops = an.loops(&func);
let mut paths = HashMap::new();
assert_eq!(COPY.path(&func, loops, join, from, into, &paths, 0), Ok(2));
paths.insert(from, 99);
assert_eq!(
COPY.path(&func, loops, join, from, into, &paths, 0),
Err(Some(super::TOO_LONG))
);
paths.insert(from, 98);
assert_eq!(COPY.path(&func, loops, join, from, into, &paths, 0), Ok(100));
assert_eq!(
COPY.path(&func, loops, join, from, into, &paths, 64),
Err(Some(super::TOO_MANY))
);
assert_eq!(FREE.path(&func, loops, join, from, into, &paths, 0), Err(None));
}
#[test]
fn one_run_makes_at_most_sixty_four_copies() {
let mut names = Interner::new();
let signature =
Signature::new().with_params(&[Type::int(32)]).with_returns(&[Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let pick = func.append_param(entry, Type::int(32));
let arms: Vec<Block> = (0..70).map(|_| func.create_block()).collect();
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 cases: Vec<(i128, Block)> = (0..).zip(arms.iter().copied()).collect();
let (&default, _) = arms.split_last().expect("arms");
Builder::new(&mut func, entry).switch(pick, default, &cases[..69]);
for (&arm, value) in arms.iter().zip(0..) {
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, &[], no, &[]);
let mut build = Builder::new(&mut func, yes);
let zero = build.iconst(Type::int(32), 0);
build.ret(&[zero]);
let mut build = Builder::new(&mut func, no);
build.ret(&[sum]);
let stats = copying(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::COPIED), 64, "{stats:?}");
assert_eq!(stats.count(Kind::Optimized, super::THREADED), 1, "{stats:?}");
assert_eq!(stats.count(Kind::Missed, super::TOO_MANY), 5, "{stats:?}");
}
#[test]
fn a_thread_back_to_a_loop_header_counts_each_instruction_twice() {
let func = loop_with_a_parameter(2);
let an = crate::machine::fixtures::analyses();
let loops = an.loops(&func);
let header = Block::from_usize(1);
let body = Block::from_usize(2);
assert!(super::back_edge(loops, body, header));
assert!(!super::back_edge(loops, header, Block::from_usize(3)));
}
#[test]
fn an_arm_carrying_a_value_the_block_worked_out_is_threaded_through_a_copy() {
let mut names = Interner::new();
let signature = Signature::new().with_returns(&[Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
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 got = 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, &[]);
let mut build = Builder::new(&mut func, yes);
build.ret(&[got]);
let mut build = Builder::new(&mut func, no);
let zero = build.iconst(Type::int(32), 0);
build.ret(&[zero]);
let stats = copying(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::THREADED), 1);
assert_eq!(stats.count(Kind::Optimized, super::COPIED), 1);
let copy = goes_to(&func, 1)[0];
assert_eq!(goes_to(&func, copy), vec![4]);
assert_eq!(carries(&func, copy).len(), 1);
}
#[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 = FREE.run(&mut func, &mut crate::machine::fixtures::analyses(), &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");
}
}