use std::collections::{HashMap, HashSet, VecDeque};
use rucc_base::Idx;
use rucc_ir::{Block, BlockCall, Def, Extra, Func, Inst, IntPred, Opcode, Value};
use crate::fold::constant;
use crate::{Analyses, Fuel, Pass, Preserved, Stats, uses};
const FOLDED: &str = "branch on a condition that is always the same way replaced by a jump";
pub(crate) const REMOVED: &str = "block nothing reaches removed";
const MERGED: &str = "block with one way into it merged into the block above it";
const FORWARDED: &str = "block that only jumped somewhere else removed and its edges pointed past";
const SAME_EVERY_WAY: &str = "block parameter that arrives as the same value every way in removed";
const NO_FUEL: &str = "branch on a known condition left alone, the pass ran out of fuel";
const NO_FUEL_MERGE: &str = "block with one way into it left alone, the pass ran out of fuel";
const NO_FUEL_FORWARD: &str =
"block that only jumped somewhere else kept, the pass ran out of fuel";
const NO_FUEL_PARAM: &str = "block parameter that is one value kept, the pass ran out of fuel";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SimplifyCfg;
impl Pass for SimplifyCfg {
fn name(&self) -> &'static str {
"simplify-cfg"
}
fn describe(&self) -> &'static str {
"unreachable blocks go, a branch that only goes one way becomes a jump, a block that only \
jumps stops being in the way, and a block with one way in is merged into the one above it"
}
fn preserves(&self) -> Preserved {
Preserved::NONE
}
fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
let mut stats = Stats::new();
sweep(func, an, &mut stats);
let mut folded = false;
let unbound = Bindings::new();
for block in func.blocks().collect::<Vec<Block>>() {
let Some(term) = func.terminator(block) else { continue };
let Some(taken) = taken(func, term, &unbound) else { continue };
if !fuel.take() {
stats.missed(NO_FUEL);
continue;
}
jump_to(func, term, taken);
stats.optimized(FOLDED);
folded = true;
}
if folded {
an.clear();
sweep(func, an, &mut stats);
}
let mut forward = HashMap::new();
if straighten(func, fuel, &mut stats, &mut forward) {
an.clear();
}
for chain in chains(func, an) {
for (at, &block) in chain.iter().enumerate().skip(1) {
if !fuel.take() {
for _ in at..chain.len() {
stats.missed(NO_FUEL_MERGE);
}
break;
}
merge(func, chain[0], block, &mut forward);
stats.optimized(MERGED);
}
}
if !forward.is_empty() {
uses::substitute(func, &forward);
}
stats
}
}
pub(crate) type Bindings = HashMap<Value, Value>;
fn resolve(subst: &Bindings, value: Value) -> Value {
subst.get(&value).copied().unwrap_or(value)
}
pub(crate) fn taken(func: &Func, term: Inst, subst: &Bindings) -> Option<BlockCall> {
let data = &func[term];
let arg = *func[data.args].first()?;
match data.opcode {
Opcode::BrIf => {
let Extra::Targets(targets) = data.extra else { return None };
if let Some(call) = one_place(func, &func[targets]) {
return Some(call);
}
let arm = usize::from(!known(func, arg, subst)?);
func[targets].get(arm).copied()
}
Opcode::Switch => {
let Extra::Switch(at) = data.extra else { return None };
let info = func[at];
if let Some(call) = one_place(func, &func[info.targets]) {
return Some(call);
}
let (value, _) = constant(func, resolve(subst, arg))?;
let case = func[info.cases].iter().position(|it| *it == value);
func[info.targets].get(case.map_or(0, |case| case + 1)).copied()
}
_ => None,
}
}
fn one_place(func: &Func, calls: &[BlockCall]) -> Option<BlockCall> {
let &first = calls.first()?;
let same = |call: &BlockCall| call.block == first.block && func[call.args] == func[first.args];
calls[1..].iter().all(same).then_some(first)
}
fn jump_to(func: &mut Func, term: Inst, call: BlockCall) {
let targets = func.push_block_calls(&[call]);
let args = func.push_values(&[]);
let data = &mut func[term];
data.opcode = Opcode::Jump;
data.args = args;
data.extra = Extra::Targets(targets);
}
pub(crate) fn sweep(func: &mut Func, an: &mut Analyses, stats: &mut Stats) {
let gone = stranded(func, an);
if gone.is_empty() {
return;
}
for block in gone {
func.remove_block(block);
stats.optimized(REMOVED);
}
an.clear();
}
fn stranded(func: &Func, an: &mut Analyses) -> Vec<Block> {
let cfg = an.cfg(func);
let Some(entry) = cfg.entry() else { return Vec::new() };
let mut seen = vec![false; cfg.capacity()];
seen[entry.index()] = true;
let mut stack = vec![entry];
let mut reached = Vec::new();
while let Some(block) = stack.pop() {
for &succ in cfg.successors(block) {
if !seen[succ.index()] {
seen[succ.index()] = true;
stack.push(succ);
}
}
reached.push(block);
}
let mut next = reached;
while !next.is_empty() {
let mut found = Vec::new();
for block in next {
for inst in func.insts(block) {
if func[inst].opcode != Opcode::BlockAddr {
continue;
}
for call in func.successors(inst) {
if !seen[call.block.index()] {
seen[call.block.index()] = true;
found.push(call.block);
}
}
}
}
let mut stack = found.clone();
while let Some(block) = stack.pop() {
for &succ in cfg.successors(block) {
if !seen[succ.index()] {
seen[succ.index()] = true;
stack.push(succ);
found.push(succ);
}
}
}
next = found;
}
func.blocks().filter(|block| !seen[block.index()]).collect()
}
pub(crate) type Edges = HashMap<Block, Vec<(Block, Idx<BlockCall>)>>;
pub(crate) fn incoming(func: &Func) -> Edges {
let mut edges: Edges = HashMap::new();
for block in func.blocks() {
let Some(term) = func.terminator(block) else { continue };
for at in func.target_list(term).iter() {
edges.entry(func[at].block).or_default().push((block, at));
}
}
edges
}
fn straighten(
func: &mut Func,
fuel: &mut Fuel,
stats: &mut Stats,
forward: &mut HashMap<Value, Value>,
) -> bool {
let Some(entry) = func.entry() else { return false };
let addressed = addressed(func);
let mut edges = incoming(func);
let mut work: VecDeque<Block> = func.blocks().collect();
let mut queued: HashSet<Block> = work.iter().copied().collect();
let mut gone: HashSet<Block> = HashSet::new();
let mut changed = false;
while let Some(block) = work.pop_front() {
queued.remove(&block);
if gone.contains(&block) {
continue;
}
let mut starved = false;
if block != entry {
let drop = redundant(func, block, edges.get(&block), forward);
let mut taking = Vec::new();
for (index, value) in drop {
if !fuel.take() {
stats.missed(NO_FUEL_PARAM);
starved = true;
break;
}
let value = uses::chase(forward, value);
forward.insert(func[block].params[index], value);
taking.push(index);
stats.optimized(SAME_EVERY_WAY);
}
if !taking.is_empty() {
take_params(func, block, &taking, edges.get(&block));
requeue(block, &mut work, &mut queued);
if let Some(term) = func.terminator(block) {
for call in func.successors(term).collect::<Vec<BlockCall>>() {
requeue(call.block, &mut work, &mut queued);
}
}
changed = true;
}
}
if starved {
break;
}
let Some((term, into, args)) = forwards(func, block, entry, &addressed, &edges) else {
continue;
};
if !fuel.take() {
stats.missed(NO_FUEL_FORWARD);
break;
}
let out = func.target_list(term).iter().next().expect("a jump has a target");
if let Some(list) = edges.get_mut(&into) {
list.retain(|&(_, at)| at != out);
}
let ins = edges.remove(&block).unwrap_or_default();
for &(_, at) in &ins {
let args = func.push_values(&args);
func.set_block_call(at, BlockCall { block: into, args });
}
edges.entry(into).or_default().extend(ins.iter().copied());
func.remove_block(block);
gone.insert(block);
stats.optimized(FORWARDED);
changed = true;
requeue(into, &mut work, &mut queued);
for &(from, _) in &ins {
requeue(from, &mut work, &mut queued);
}
}
changed
}
fn requeue(block: Block, work: &mut VecDeque<Block>, queued: &mut HashSet<Block>) {
if queued.insert(block) {
work.push_back(block);
}
}
fn redundant(
func: &Func,
block: Block,
ins: Option<&Vec<(Block, Idx<BlockCall>)>>,
forward: &HashMap<Value, Value>,
) -> Vec<(usize, Value)> {
let Some(ins) = ins.filter(|ins| !ins.is_empty()) else { return Vec::new() };
let mut found = Vec::new();
for (index, ¶m) in func[block].params.iter().enumerate() {
let mut only = None;
let mut agree = true;
for &(_, at) in ins {
let list = func[at].args;
let Some(&arg) = func[list].get(index) else {
agree = false;
break;
};
let arg = uses::chase(forward, arg);
if arg == param {
continue;
}
match only {
None => only = Some(arg),
Some(seen) if seen == arg => {}
Some(_) => {
agree = false;
break;
}
}
}
if !agree {
continue;
}
if let Some(value) = only {
found.push((index, value));
}
}
found
}
fn take_params(
func: &mut Func,
block: Block,
taking: &[usize],
ins: Option<&Vec<(Block, Idx<BlockCall>)>>,
) {
for &(_, at) in ins.into_iter().flatten() {
let call = func[at];
let kept: Vec<Value> = func[call.args]
.iter()
.enumerate()
.filter(|(index, _)| !taking.contains(index))
.map(|(_, &value)| value)
.collect();
let args = func.push_values(&kept);
func.set_block_call(at, BlockCall { block: call.block, args });
}
let mut index = 0;
func.retain_params(block, |_| {
let keep = !taking.contains(&index);
index += 1;
keep
});
}
fn forwards(
func: &Func,
block: Block,
entry: Block,
addressed: &HashSet<Block>,
edges: &Edges,
) -> Option<(Inst, Block, Vec<Value>)> {
if block == entry || addressed.contains(&block) || !func[block].params.is_empty() {
return None;
}
let term = func.terminator(block)?;
if func[term].opcode != Opcode::Jump {
return None;
}
if func.insts(block).count() != 1 {
return None;
}
let call = func.successors(term).next()?;
if call.block == block {
return None;
}
if carrying(func, block, call.block, func[call.args].len(), edges) {
return None;
}
Some((term, call.block, func[call.args].to_vec()))
}
fn carrying(func: &Func, block: Block, into: Block, args: usize, edges: &Edges) -> bool {
if args == 0 {
return false;
}
let ins = edges.get(&block).map_or(0, Vec::len);
let after = edges.get(&into).map_or(0, Vec::len) - 1 + ins;
if after < 2 {
return false;
}
edges.get(&block).into_iter().flatten().any(|&(from, _)| {
let Some(term) = func.terminator(from) else { return false };
func.target_list(term).iter().count() >= 2
})
}
fn chains(func: &Func, an: &mut Analyses) -> Vec<Vec<Block>> {
let cfg = an.cfg(func);
let Some(entry) = cfg.entry() else { return Vec::new() };
let addressed = addressed(func);
let mut below = HashMap::new();
let mut is_below = HashSet::new();
for block in func.blocks() {
let Some(term) = func.terminator(block) else { continue };
if func[term].opcode != Opcode::Jump {
continue;
}
let Some(call) = func.successors(term).next() else { continue };
let into = call.block;
let preds = cfg.predecessors(into);
if into == entry || into == block || addressed.contains(&into) {
continue;
}
if preds.len() != 1 || preds[0] != block {
continue;
}
below.insert(block, into);
is_below.insert(into);
}
let heads = func.blocks().filter(|it| below.contains_key(it) && !is_below.contains(it));
heads
.map(|head| {
let mut chain = vec![head];
let mut at = head;
while let Some(&next) = below.get(&at) {
chain.push(next);
at = next;
}
chain
})
.collect()
}
fn addressed(func: &Func) -> HashSet<Block> {
let mut taken = HashSet::new();
for block in func.blocks() {
for inst in func.insts(block) {
if func[inst].opcode != Opcode::BlockAddr {
continue;
}
for call in func.successors(inst) {
taken.insert(call.block);
}
}
}
taken
}
fn merge(func: &mut Func, head: Block, block: Block, forward: &mut HashMap<Value, Value>) {
let term = func.terminator(head).expect("the head of a chain ends in a jump");
let call = func.successors(term).next().expect("a jump goes somewhere");
let args = func[call.args].to_vec();
let params = func[block].params.clone();
for (param, arg) in params.into_iter().zip(args) {
let arg = uses::chase(forward, arg);
forward.insert(param, arg);
}
func.remove_inst(term);
for inst in func.insts(block).collect::<Vec<Inst>>() {
func.remove_inst(inst);
func.append_inst(head, inst);
}
func.remove_block(block);
}
fn known(func: &Func, value: Value, subst: &Bindings) -> Option<bool> {
let value = resolve(subst, value);
if let Some((imm, _)) = constant(func, value) {
return Some(imm.unsigned() != 0);
}
compared(func, value, subst)
}
fn compared(func: &Func, value: Value, subst: &Bindings) -> Option<bool> {
let Def::Result { inst, .. } = func[value].def else { return None };
let data = &func[inst];
if data.opcode != Opcode::ICmp {
return None;
}
let Extra::IntPred(pred) = data.extra else { return None };
let args = &func[data.args];
let (lhs, ty) = constant(func, resolve(subst, *args.first()?))?;
let (rhs, _) = constant(func, resolve(subst, *args.get(1)?))?;
Some(match pred {
IntPred::Eq => lhs == rhs,
IntPred::Ne => lhs != rhs,
IntPred::Slt => lhs.signed(ty) < rhs.signed(ty),
IntPred::Sle => lhs.signed(ty) <= rhs.signed(ty),
IntPred::Sgt => lhs.signed(ty) > rhs.signed(ty),
IntPred::Sge => lhs.signed(ty) >= rhs.signed(ty),
IntPred::Ult => lhs.unsigned() < rhs.unsigned(),
IntPred::Ule => lhs.unsigned() <= rhs.unsigned(),
IntPred::Ugt => lhs.unsigned() > rhs.unsigned(),
IntPred::Uge => lhs.unsigned() >= rhs.unsigned(),
})
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{
Block, Builder, Def, Func, Inst, IntPred, Module, Opcode, Signature, Type, Value,
};
use rucc_target::{Arch, Env, Os, TargetInfo, Triple};
use super::SimplifyCfg;
use crate::stats::Kind;
use crate::testing::graph;
use crate::{Analyses, Fuel, Pass, Preserved, Stats};
fn simplify(func: &mut Func) -> Stats {
SimplifyCfg.run(func, &mut Analyses::new(), &mut Fuel::unlimited())
}
fn blocks(func: &Func) -> Vec<usize> {
func.blocks().map(Block::index).collect()
}
fn terminator(func: &Func, block: usize) -> Opcode {
let block = Block::from_usize(block);
func[func.terminator(block).expect("every block here has one")].opcode
}
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 lives_in(func: &Func, value: Value) -> Option<usize> {
let Def::Result { inst, .. } = func[value].def else { return None };
func.block_of(inst).map(Block::index)
}
fn diamond(cond: impl FnOnce(&mut Builder<'_>) -> Value) -> (Func, [Value; 2]) {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let then_block = func.create_block();
let else_block = func.create_block();
let join = func.create_block();
let mut build = Builder::new(&mut func, entry);
let cond = cond(&mut build);
build.br_if(cond, then_block, &[], else_block, &[]);
let mut marks = Vec::new();
for (arm, mark) in [(then_block, 111), (else_block, 222)] {
let mut build = Builder::new(&mut func, arm);
marks.push(build.iconst(Type::int(32), mark));
build.jump(join, &[]);
}
let mut build = Builder::new(&mut func, join);
build.ret(&[]);
(func, [marks[0], marks[1]])
}
#[test]
fn a_branch_on_a_true_constant_becomes_a_jump_to_the_first_arm() {
let (mut func, [taken, other]) = diamond(|build| build.iconst(Type::int(1), 1));
let stats = simplify(&mut func);
assert!(stats.changed());
assert_eq!(stats.count(Kind::Optimized, super::FOLDED), 1);
assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 1);
assert_eq!(stats.count(Kind::Optimized, super::MERGED), 2);
assert_eq!(lives_in(&func, taken), Some(0));
assert_eq!(lives_in(&func, other), None);
assert_eq!(blocks(&func), [0]);
}
#[test]
fn a_branch_on_a_false_constant_becomes_a_jump_to_the_second_arm() {
let (mut func, [other, taken]) = diamond(|build| build.iconst(Type::int(1), 0));
assert!(simplify(&mut func).changed());
assert_eq!(lives_in(&func, taken), Some(0));
assert_eq!(lives_in(&func, other), None);
assert_eq!(blocks(&func), [0]);
}
#[test]
fn folding_a_branch_and_merging_what_it_leaves_are_two_things_fuel_buys_apart() {
let (mut func, _) = diamond(|build| build.iconst(Type::int(1), 1));
let stats = SimplifyCfg.run(&mut func, &mut Analyses::new(), &mut Fuel::of(1));
assert_eq!(terminator(&func, 0), Opcode::Jump);
assert_eq!(goes_to(&func, 0), [1]);
assert_eq!(blocks(&func), [0, 1, 3]);
assert_eq!(stats.count(Kind::Optimized, super::MERGED), 0);
assert_eq!(stats.count(Kind::Missed, super::NO_FUEL_MERGE), 2);
}
#[test]
fn a_branch_on_a_comparison_of_two_constants_is_read_without_folding_it() {
let cases: &[(IntPred, i128, i128, bool)] = &[
(IntPred::Eq, 7, 7, true),
(IntPred::Eq, 7, 8, false),
(IntPred::Ne, 7, 8, true),
(IntPred::Ne, 7, 7, false),
(IntPred::Slt, -1, 1, true),
(IntPred::Slt, 1, -1, false),
(IntPred::Sle, -1, -1, true),
(IntPred::Sle, 1, -1, false),
(IntPred::Sgt, 1, -1, true),
(IntPred::Sgt, -1, 1, false),
(IntPred::Sge, -1, -1, true),
(IntPred::Sge, -1, 1, false),
(IntPred::Ult, 1, -1, true),
(IntPred::Ult, -1, 1, false),
(IntPred::Ule, -1, -1, true),
(IntPred::Ule, -1, 1, false),
(IntPred::Ugt, -1, 1, true),
(IntPred::Ugt, 1, -1, false),
(IntPred::Uge, -1, -1, true),
(IntPred::Uge, 1, -1, false),
];
for &(pred, lhs, rhs, taken) in cases {
let (mut func, marks) = diamond(|build| {
let lhs = build.iconst(Type::int(32), lhs);
let rhs = build.iconst(Type::int(32), rhs);
build.icmp(pred, lhs, rhs)
});
assert!(simplify(&mut func).changed(), "{pred:?} {lhs} {rhs}");
let [went, gone] = if taken { [marks[0], marks[1]] } else { [marks[1], marks[0]] };
assert_eq!(lives_in(&func, went), Some(0), "{pred:?} {lhs} {rhs}");
assert_eq!(lives_in(&func, gone), None, "{pred:?} {lhs} {rhs}");
let kept = func.insts(Block::from_usize(0)).any(|it| func[it].opcode == Opcode::ICmp);
assert!(kept, "the comparison was folded away and issue 352 says it must not be");
}
}
#[test]
fn a_branch_on_something_nobody_knows_is_left_alone() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new().with_params(&[Type::int(1)]));
let entry = func.create_block();
let then_block = func.create_block();
let else_block = func.create_block();
let cond = func.append_param(entry, Type::int(1));
let mut build = Builder::new(&mut func, entry);
build.br_if(cond, then_block, &[], else_block, &[]);
for arm in [then_block, else_block] {
let mut build = Builder::new(&mut func, arm);
build.ret(&[]);
}
let stats = simplify(&mut func);
assert!(!stats.changed());
assert!(stats.is_empty(), "a pass with nothing to say should say nothing");
assert_eq!(terminator(&func, 0), Opcode::BrIf);
assert_eq!(blocks(&func), [0, 1, 2]);
}
fn switched(on: i128, cases: &[i128]) -> (Func, Vec<Value>) {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let arms: Vec<Block> = (0..=cases.len()).map(|_| func.create_block()).collect();
let mut build = Builder::new(&mut func, entry);
let value = build.iconst(Type::int(32), on);
let pairs: Vec<(i128, Block)> =
cases.iter().enumerate().map(|(at, &case)| (case, arms[at + 1])).collect();
build.switch(value, arms[0], &pairs);
let mut marks = Vec::new();
for (at, &arm) in arms.iter().enumerate() {
let mut build = Builder::new(&mut func, arm);
marks.push(build.iconst(Type::int(32), 100 + at as i128));
build.ret(&[]);
}
(func, marks)
}
#[test]
fn a_switch_on_a_constant_takes_the_case_that_matches() {
let (mut func, marks) = switched(5, &[4, 5]);
assert!(simplify(&mut func).changed());
assert_eq!(lives_in(&func, marks[2]), Some(0));
assert_eq!(lives_in(&func, marks[0]), None);
assert_eq!(lives_in(&func, marks[1]), None);
assert_eq!(blocks(&func), [0]);
}
#[test]
fn a_switch_on_a_constant_no_case_names_takes_the_default() {
let (mut func, marks) = switched(9, &[4]);
assert!(simplify(&mut func).changed());
assert_eq!(lives_in(&func, marks[0]), Some(0));
assert_eq!(lives_in(&func, marks[1]), None);
assert_eq!(blocks(&func), [0]);
}
#[test]
fn the_arguments_travel_with_the_edge_that_survives() {
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 mut build = Builder::new(&mut func, entry);
let cond = build.iconst(Type::int(1), 0);
let taken = build.iconst(Type::int(32), 11);
let other = build.iconst(Type::int(32), 22);
build.br_if(cond, join, &[other], join, &[taken]);
let mut build = Builder::new(&mut func, join);
build.ret(&[param]);
assert!(simplify(&mut func).changed());
assert_eq!(blocks(&func), [0]);
let term = func.terminator(entry).expect("the entry has one");
assert_eq!(func[func[term].args], [taken]);
assert_ne!(func[func[term].args], [param]);
}
#[test]
fn a_branch_whose_arms_are_the_same_edge_becomes_a_jump() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(1)]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let join = func.create_block();
let cond = func.append_param(entry, Type::int(1));
let mut build = Builder::new(&mut func, entry);
build.br_if(cond, join, &[], join, &[]);
let mut build = Builder::new(&mut func, join);
build.ret(&[]);
let stats = simplify(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::FOLDED), 1);
assert_eq!(stats.count(Kind::Optimized, super::MERGED), 1);
assert_eq!(blocks(&func), [0]);
assert_eq!(terminator(&func, 0), Opcode::Return);
}
#[test]
fn a_switch_whose_cases_all_go_to_one_place_becomes_a_jump() {
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 join = func.create_block();
let value = func.append_param(entry, Type::int(32));
let mut build = Builder::new(&mut func, entry);
build.switch(value, join, &[(4, join), (5, join)]);
let mut build = Builder::new(&mut func, join);
build.ret(&[]);
let stats = simplify(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::FOLDED), 1);
assert_eq!(blocks(&func), [0]);
}
#[test]
fn a_branch_to_one_block_by_two_edges_that_differ_is_left_alone() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(1)]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let join = func.create_block();
let cond = func.append_param(entry, Type::int(1));
func.append_param(join, Type::int(32));
let mut build = Builder::new(&mut func, entry);
let first = build.iconst(Type::int(32), 11);
let second = build.iconst(Type::int(32), 22);
build.br_if(cond, join, &[first], join, &[second]);
let mut build = Builder::new(&mut func, join);
build.ret(&[]);
let stats = simplify(&mut func);
assert!(!stats.changed());
assert_eq!(terminator(&func, 0), Opcode::BrIf);
assert_eq!(blocks(&func), [0, 1]);
}
#[test]
fn a_block_the_dead_arm_shared_with_a_live_one_stays() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new().with_params(&[Type::int(32)]));
let entry = func.create_block();
let dead = func.create_block();
let shared = func.create_block();
let exit = func.create_block();
let x = func.append_param(entry, Type::int(32));
let mut build = Builder::new(&mut func, entry);
let never = build.iconst(Type::int(1), 0);
build.switch(x, exit, &[(0, dead), (1, shared)]);
let mut build = Builder::new(&mut func, dead);
build.iconst(Type::int(32), 1);
build.br_if(never, shared, &[], exit, &[]);
for arm in [shared, exit] {
let mut build = Builder::new(&mut func, arm);
build.ret(&[]);
}
let stats = simplify(&mut func);
assert!(stats.changed());
assert_eq!(terminator(&func, 0), Opcode::Switch);
assert_eq!(goes_to(&func, 1), [3]);
assert_eq!(blocks(&func), [0, 1, 2, 3]);
assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 0);
}
#[test]
fn a_block_whose_address_is_taken_is_not_removed() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let labelled = func.create_block();
let arm = func.create_block();
let mut build = Builder::new(&mut func, entry);
let cond = build.iconst(Type::int(1), 1);
let addr = build.block_addr(labelled);
build.br_if(cond, arm, &[], labelled, &[]);
let mut build = Builder::new(&mut func, arm);
build.indirect_br(addr, &[labelled]);
let mut build = Builder::new(&mut func, labelled);
build.ret(&[]);
assert!(simplify(&mut func).changed());
assert!(blocks(&func).contains(&1), "the labelled block went with the arm");
assert_eq!(blocks(&func), [0, 1]);
assert_eq!(goes_to(&func, 0), [1]);
}
#[test]
fn a_block_only_an_unreachable_block_takes_the_address_of_goes_too() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let dead = func.create_block();
let labelled = func.create_block();
let mut build = Builder::new(&mut func, entry);
let cond = build.iconst(Type::int(1), 1);
build.br_if(cond, entry, &[], dead, &[]);
let mut build = Builder::new(&mut func, dead);
let addr = build.block_addr(labelled);
build.indirect_br(addr, &[labelled]);
let mut build = Builder::new(&mut func, labelled);
build.ret(&[]);
assert!(simplify(&mut func).changed());
assert_eq!(blocks(&func), [0]);
}
#[test]
fn a_block_nothing_reaches_goes_even_when_no_branch_folded() {
let mut func = graph(&[&[], &[]]);
let stats = simplify(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::FOLDED), 0);
assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 1);
assert_eq!(blocks(&func), [0]);
}
#[test]
fn a_block_with_one_way_into_it_goes_into_the_block_above_it() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let middle = func.create_block();
let last = func.create_block();
let mut build = Builder::new(&mut func, entry);
build.iconst(Type::int(32), 1);
build.jump(middle, &[]);
let mut build = Builder::new(&mut func, middle);
build.iconst(Type::int(32), 2);
build.jump(last, &[]);
let mut build = Builder::new(&mut func, last);
build.ret(&[]);
let stats = simplify(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::MERGED), 2);
assert_eq!(stats.count(Kind::Optimized, super::FORWARDED), 0);
assert_eq!(blocks(&func), [0]);
assert_eq!(terminator(&func, 0), Opcode::Return);
}
#[test]
fn a_block_with_two_ways_into_it_stays_where_it_is() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(1)]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let then_block = func.create_block();
let else_block = func.create_block();
let join = func.create_block();
let cond = func.append_param(entry, Type::int(1));
let mut build = Builder::new(&mut func, entry);
build.br_if(cond, then_block, &[], else_block, &[]);
for (arm, mark) in [(then_block, 111), (else_block, 222)] {
let mut build = Builder::new(&mut func, arm);
build.iconst(Type::int(32), mark);
build.jump(join, &[]);
}
let mut build = Builder::new(&mut func, join);
build.ret(&[]);
let stats = simplify(&mut func);
assert!(!stats.changed());
assert_eq!(blocks(&func), [0, 1, 2, 3]);
}
#[test]
fn a_block_above_one_that_does_not_end_in_a_jump_keeps_it() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(1)]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let arm = func.create_block();
let exit = func.create_block();
let cond = func.append_param(entry, Type::int(1));
let mut build = Builder::new(&mut func, entry);
build.br_if(cond, arm, &[], exit, &[]);
for block in [arm, exit] {
let mut build = Builder::new(&mut func, block);
build.ret(&[]);
}
let stats = simplify(&mut func);
assert!(!stats.changed());
assert_eq!(blocks(&func), [0, 1, 2]);
}
#[test]
fn the_entry_block_is_never_the_one_that_moves() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(1)]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let latch = func.create_block();
let exit = func.create_block();
let cond = func.append_param(entry, Type::int(1));
let mut build = Builder::new(&mut func, entry);
build.br_if(cond, latch, &[], exit, &[]);
let mut build = Builder::new(&mut func, latch);
build.iconst(Type::int(32), 1);
build.jump(entry, &[]);
let mut build = Builder::new(&mut func, exit);
build.ret(&[]);
let stats = simplify(&mut func);
assert!(!stats.changed());
assert_eq!(blocks(&func), [0, 1, 2]);
}
#[test]
fn a_block_whose_address_is_taken_is_not_merged_away_either() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let middle = func.create_block();
let labelled = func.create_block();
let mut build = Builder::new(&mut func, entry);
build.block_addr(labelled);
build.jump(middle, &[]);
let mut build = Builder::new(&mut func, middle);
build.iconst(Type::int(32), 1);
build.jump(labelled, &[]);
let mut build = Builder::new(&mut func, labelled);
build.ret(&[]);
let stats = simplify(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::MERGED), 1);
assert_eq!(blocks(&func), [0, 2]);
}
#[test]
fn merging_binds_a_block_parameter_to_the_argument_the_jump_carried() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let below = func.create_block();
let param = func.append_param(below, Type::int(32));
let mut build = Builder::new(&mut func, entry);
let arg = build.iconst(Type::int(32), 7);
build.jump(below, &[arg]);
let mut build = Builder::new(&mut func, below);
build.ret(&[param]);
assert!(simplify(&mut func).changed());
assert_eq!(blocks(&func), [0]);
let term = func.terminator(entry).expect("the entry has one");
assert_eq!(func[func[term].args], [arg]);
}
#[test]
fn a_chain_of_merges_follows_a_parameter_bound_to_a_parameter() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let middle = func.create_block();
let last = func.create_block();
let carried = func.append_param(middle, Type::int(32));
let arrived = func.append_param(last, Type::int(32));
let mut build = Builder::new(&mut func, entry);
let arg = build.iconst(Type::int(32), 7);
build.jump(middle, &[arg]);
let mut build = Builder::new(&mut func, middle);
build.jump(last, &[carried]);
let mut build = Builder::new(&mut func, last);
build.ret(&[arrived]);
assert!(simplify(&mut func).changed());
assert_eq!(blocks(&func), [0]);
let term = func.terminator(entry).expect("the entry has one");
assert_eq!(func[func[term].args], [arg]);
}
fn arms(func: &mut Func) -> (Value, [Block; 2]) {
let entry = func.create_block();
let first = func.create_block();
let second = func.create_block();
let cond = func.append_param(entry, Type::int(1));
let mut build = Builder::new(func, entry);
let carried = build.iconst(Type::int(32), 7);
build.br_if(cond, first, &[], second, &[]);
for (arm, mark) in [(first, 111), (second, 222)] {
let mut build = Builder::new(func, arm);
build.iconst(Type::int(32), mark);
}
(carried, [first, second])
}
fn taking_a_condition() -> Func {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(1)]);
Func::new(names.intern("f"), signature)
}
fn carries(func: &Func, block: usize, edge: 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).nth(edge).expect("the edge is there");
func[call.args].to_vec()
}
#[test]
fn a_block_that_does_nothing_but_jump_stops_being_in_the_way() {
let mut func = taking_a_condition();
let (_, arms) = arms(&mut func);
let forwarder = func.create_block();
let exit = func.create_block();
for arm in arms {
Builder::new(&mut func, arm).jump(forwarder, &[]);
}
Builder::new(&mut func, forwarder).jump(exit, &[]);
Builder::new(&mut func, exit).ret(&[]);
let stats = simplify(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::FORWARDED), 1);
assert_eq!(blocks(&func), [0, 1, 2, 4]);
assert_eq!(goes_to(&func, 1), [4]);
assert_eq!(goes_to(&func, 2), [4]);
}
#[test]
fn a_forwarder_hands_its_predecessors_the_arguments_it_was_passing() {
let mut func = taking_a_condition();
let (carried, [arm, above]) = arms(&mut func);
let forwarder = func.create_block();
let exit = func.create_block();
let other = func.append_param(exit, Type::int(32));
let mut build = Builder::new(&mut func, arm);
let mine = build.iconst(Type::int(32), 9);
build.jump(exit, &[mine]);
Builder::new(&mut func, above).jump(forwarder, &[]);
Builder::new(&mut func, forwarder).jump(exit, &[carried]);
Builder::new(&mut func, exit).ret(&[other]);
let stats = simplify(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::FORWARDED), 1);
assert_eq!(blocks(&func), [0, 1, 2, 4]);
assert_eq!(carries(&func, 2, 0), [carried]);
assert_eq!(carries(&func, 1, 0), [mine]);
assert_eq!(stats.count(Kind::Optimized, super::SAME_EVERY_WAY), 0);
}
#[test]
fn a_forwarder_carrying_something_on_an_edge_out_of_a_branch_stays() {
let mut func = taking_a_condition();
let (carried, [arm, forwarder]) = arms(&mut func);
let exit = func.create_block();
let other = func.append_param(exit, Type::int(32));
for inst in func.insts(forwarder).collect::<Vec<Inst>>() {
func.remove_inst(inst);
}
let mut build = Builder::new(&mut func, arm);
let mine = build.iconst(Type::int(32), 9);
build.jump(exit, &[mine]);
Builder::new(&mut func, forwarder).jump(exit, &[carried]);
Builder::new(&mut func, exit).ret(&[other]);
let stats = simplify(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::FORWARDED), 0);
assert_eq!(blocks(&func), [0, 1, 2, 3]);
}
#[test]
fn a_forwarder_carrying_nothing_out_of_a_branch_goes_anyway() {
let mut func = taking_a_condition();
let (_, [arm, forwarder]) = arms(&mut func);
let exit = func.create_block();
for inst in func.insts(forwarder).collect::<Vec<Inst>>() {
func.remove_inst(inst);
}
Builder::new(&mut func, arm).jump(exit, &[]);
Builder::new(&mut func, forwarder).jump(exit, &[]);
Builder::new(&mut func, exit).ret(&[]);
let stats = simplify(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::FORWARDED), 1);
assert_eq!(blocks(&func), [0, 1, 3]);
}
#[test]
fn a_block_that_jumps_to_itself_is_not_a_forwarder() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let spin = func.create_block();
Builder::new(&mut func, entry).jump(spin, &[]);
Builder::new(&mut func, spin).jump(spin, &[]);
let stats = simplify(&mut func);
assert!(!stats.changed());
assert_eq!(blocks(&func), [0, 1]);
}
#[test]
fn the_entry_block_is_never_the_forwarder_that_goes() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let below = func.create_block();
Builder::new(&mut func, entry).jump(below, &[]);
let mut build = Builder::new(&mut func, below);
build.iconst(Type::int(32), 1);
build.ret(&[]);
let stats = simplify(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::FORWARDED), 0);
assert_eq!(stats.count(Kind::Optimized, super::MERGED), 1);
assert_eq!(blocks(&func), [0]);
}
#[test]
fn a_block_whose_address_is_taken_is_not_forwarded_past_either() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let labelled = func.create_block();
let exit = func.create_block();
let mut build = Builder::new(&mut func, entry);
let addr = build.block_addr(labelled);
build.indirect_br(addr, &[labelled]);
Builder::new(&mut func, labelled).jump(exit, &[]);
let mut build = Builder::new(&mut func, exit);
build.iconst(Type::int(32), 1);
build.ret(&[]);
let stats = simplify(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::FORWARDED), 0);
assert!(blocks(&func).contains(&1), "the labelled block was forwarded past");
}
#[test]
fn a_run_of_forwarders_comes_out_as_one_edge() {
let mut func = taking_a_condition();
let (_, arms) = arms(&mut func);
let first = func.create_block();
let second = func.create_block();
let exit = func.create_block();
for arm in arms {
Builder::new(&mut func, arm).jump(first, &[]);
}
Builder::new(&mut func, first).jump(second, &[]);
Builder::new(&mut func, second).jump(exit, &[]);
Builder::new(&mut func, exit).ret(&[]);
let stats = simplify(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::FORWARDED), 2);
assert_eq!(blocks(&func), [0, 1, 2, 5]);
assert_eq!(goes_to(&func, 1), [5]);
assert_eq!(goes_to(&func, 2), [5]);
}
#[test]
fn a_block_parameter_that_arrives_as_one_value_every_way_in_goes() {
let mut func = taking_a_condition();
let (carried, arms) = arms(&mut func);
let join = func.create_block();
let param = func.append_param(join, Type::int(32));
for arm in arms {
Builder::new(&mut func, arm).jump(join, &[carried]);
}
Builder::new(&mut func, join).ret(&[param]);
let stats = simplify(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::SAME_EVERY_WAY), 1);
assert!(func[Block::from_usize(3)].params.is_empty());
let term = func.terminator(Block::from_usize(3)).expect("the join has one");
assert_eq!(func[func[term].args], [carried]);
assert!(carries(&func, 1, 0).is_empty());
assert!(carries(&func, 2, 0).is_empty());
}
#[test]
fn a_block_parameter_that_differs_on_one_way_in_stays() {
let mut func = taking_a_condition();
let (carried, arms) = arms(&mut func);
let join = func.create_block();
let param = func.append_param(join, Type::int(32));
let mut build = Builder::new(&mut func, arms[0]);
let mine = build.iconst(Type::int(32), 9);
build.jump(join, &[mine]);
Builder::new(&mut func, arms[1]).jump(join, &[carried]);
Builder::new(&mut func, join).ret(&[param]);
let stats = simplify(&mut func);
assert!(!stats.changed());
assert_eq!(func[Block::from_usize(3)].params, [param]);
}
#[test]
fn a_loop_header_parameter_whose_other_argument_is_itself_is_what_it_started_as() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(1)]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let header = func.create_block();
let latch = func.create_block();
let exit = func.create_block();
let cond = func.append_param(entry, Type::int(1));
let param = func.append_param(header, Type::int(32));
let mut build = Builder::new(&mut func, entry);
let init = build.iconst(Type::int(32), 7);
build.jump(header, &[init]);
Builder::new(&mut func, header).br_if(cond, latch, &[], exit, &[]);
let mut build = Builder::new(&mut func, latch);
build.iconst(Type::int(32), 1);
build.jump(header, &[param]);
Builder::new(&mut func, exit).ret(&[param]);
let stats = simplify(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::SAME_EVERY_WAY), 1);
assert!(func[Block::from_usize(1)].params.is_empty());
let term = func.terminator(Block::from_usize(3)).expect("the exit has one");
assert_eq!(func[func[term].args], [init]);
}
#[test]
fn the_entry_blocks_parameters_are_the_functions_and_stay() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(1), Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let latch = func.create_block();
let exit = func.create_block();
let cond = func.append_param(entry, Type::int(1));
let x = func.append_param(entry, Type::int(32));
Builder::new(&mut func, entry).br_if(cond, latch, &[], exit, &[]);
let mut build = Builder::new(&mut func, latch);
let one = build.iconst(Type::int(1), 1);
let seven = build.iconst(Type::int(32), 7);
build.jump(entry, &[one, seven]);
Builder::new(&mut func, exit).ret(&[x]);
let stats = simplify(&mut func);
assert!(!stats.changed());
assert_eq!(func[Block::from_usize(0)].params, [cond, x]);
}
#[test]
fn taking_one_parameter_away_is_what_makes_the_next_one_redundant() {
let mut func = taking_a_condition();
let (carried, arms) = arms(&mut func);
let join = func.create_block();
let inner = func.append_param(join, Type::int(32));
let left = func.create_block();
let right = func.create_block();
let last = func.create_block();
let outer = func.append_param(last, Type::int(32));
for arm in arms {
Builder::new(&mut func, arm).jump(join, &[carried]);
}
let cond = func[Block::from_usize(0)].params[0];
Builder::new(&mut func, join).br_if(cond, left, &[], right, &[]);
let mut build = Builder::new(&mut func, left);
build.iconst(Type::int(32), 1);
build.jump(last, &[inner]);
let mut build = Builder::new(&mut func, right);
build.iconst(Type::int(32), 2);
build.jump(last, &[carried]);
Builder::new(&mut func, last).ret(&[outer]);
let stats = simplify(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::SAME_EVERY_WAY), 2);
let term = func.terminator(Block::from_usize(6)).expect("the last block has one");
assert_eq!(func[func[term].args], [carried]);
}
#[test]
fn a_forwarder_with_a_parameter_goes_once_the_parameter_does() {
let mut func = taking_a_condition();
let (carried, arms) = arms(&mut func);
let forwarder = func.create_block();
let param = func.append_param(forwarder, Type::int(32));
let exit = func.create_block();
let arrived = func.append_param(exit, Type::int(32));
for arm in arms {
Builder::new(&mut func, arm).jump(forwarder, &[carried]);
}
Builder::new(&mut func, forwarder).jump(exit, &[param]);
Builder::new(&mut func, exit).ret(&[arrived]);
let stats = simplify(&mut func);
assert_eq!(stats.count(Kind::Optimized, super::FORWARDED), 1);
assert_eq!(stats.count(Kind::Optimized, super::SAME_EVERY_WAY), 2);
assert_eq!(blocks(&func), [0, 1, 2, 4]);
let term = func.terminator(Block::from_usize(4)).expect("the exit has one");
assert_eq!(func[func[term].args], [carried]);
}
#[test]
fn fuel_stops_step_three_the_same_way_it_stops_the_rest() {
let mut func = taking_a_condition();
let (carried, arms) = arms(&mut func);
let forwarder = func.create_block();
let param = func.append_param(forwarder, Type::int(32));
let exit = func.create_block();
for arm in arms {
Builder::new(&mut func, arm).jump(forwarder, &[carried]);
}
Builder::new(&mut func, forwarder).jump(exit, &[param]);
Builder::new(&mut func, exit).ret(&[]);
let stats = SimplifyCfg.run(&mut func, &mut Analyses::new(), &mut Fuel::of(1));
assert_eq!(stats.count(Kind::Optimized, super::SAME_EVERY_WAY), 1);
assert_eq!(stats.count(Kind::Optimized, super::FORWARDED), 0);
assert_eq!(stats.count(Kind::Missed, super::NO_FUEL_FORWARD), 1);
assert_eq!(blocks(&func), [0, 1, 2, 3, 4]);
}
#[test]
fn step_three_leaves_the_verifier_nothing_to_complain_about() {
let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
let mut names = Interner::new();
let mut module = Module::new(names.intern("test.c"), &target);
let mut func = taking_a_condition();
let (carried, arms) = arms(&mut func);
let forwarder = func.create_block();
let param = func.append_param(forwarder, Type::int(32));
let exit = func.create_block();
let arrived = func.append_param(exit, Type::int(32));
let mut build = Builder::new(&mut func, arms[0]);
let mine = build.iconst(Type::int(32), 9);
build.jump(exit, &[mine]);
Builder::new(&mut func, arms[1]).jump(forwarder, &[carried]);
Builder::new(&mut func, forwarder).jump(exit, &[param]);
let mut build = Builder::new(&mut func, exit);
build.icmp(IntPred::Eq, arrived, arrived);
build.ret(&[]);
simplify(&mut func);
module.add_func(func);
rucc_ir::verify(&module, &names).expect("step three left the function verifiable");
}
#[test]
fn out_of_fuel_leaves_the_function_exactly_as_it_was() {
let (mut func, _) = diamond(|build| build.iconst(Type::int(1), 1));
let before = blocks(&func);
let stats = SimplifyCfg.run(&mut func, &mut Analyses::new(), &mut Fuel::of(0));
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
assert_eq!(terminator(&func, 0), Opcode::BrIf);
assert_eq!(blocks(&func), before);
}
#[test]
fn what_fuel_buys_is_one_whole_change_and_never_half_of_one() {
let mut func = graph(&[&[1, 2], &[3, 4], &[5], &[5], &[5], &[]]);
let stats = SimplifyCfg.run(&mut func, &mut Analyses::new(), &mut Fuel::of(1));
assert_eq!(stats.count(Kind::Optimized, super::FOLDED), 1);
assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
assert_eq!(blocks(&func), [0, 1, 3, 4, 5]);
}
#[test]
fn the_pass_leaves_the_verifier_nothing_to_complain_about() {
let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
let mut names = Interner::new();
let mut module = Module::new(names.intern("test.c"), &target);
let mut func = graph(&[&[1, 2], &[3], &[3], &[4, 1], &[]]);
simplify(&mut func);
module.add_func(func);
rucc_ir::verify(&module, &names).expect("the pass left the function verifiable");
}
#[test]
fn the_pass_says_it_preserves_nothing() {
assert_eq!(SimplifyCfg.preserves(), Preserved::NONE);
}
}