use rucc_cost::{AddrMode, Cost, CostTable, Cycles, RegClass, Width, heuristics};
use rucc_ir::{
Block, BlockCall, Def, Extra, Flags, Func, Inst, InstData, IntPred, Opcode, Type, Value,
};
use crate::analysis::Analysis;
use crate::cfg::Cfg;
use crate::dom::Dominators;
use crate::loops::{LoopId, Loops};
use crate::machine::Machine;
use crate::scev::{Chrec, Count, Evolution, Invariant, Scev};
use crate::{Analyses, Fuel, Pass, Preserved, Stats};
const NO_TARGET: &str =
"left alone, nobody has priced this machine and the answer is a fact about the machine";
const POPULATION: &str = "loop with at least one induction variable use in it";
const USE_ADDRESS: &str = "address of a read or a write that moves by a fixed step";
const USE_COMPARE: &str = "comparison against something that moves by a fixed step";
const USE_GENERIC: &str = "other use of something that moves by a fixed step";
const GROUPED: &str = "address uses sharing one variable, apart in a constant offset";
const CANDIDATE: &str = "induction variable considered for the loop to keep";
const CHOSEN: &str = "induction variable chosen for the loop to keep";
const KEPT: &str = "loop whose own induction variables are the ones worth keeping";
const CHANGED: &str = "loop that would be cheaper with a different set of induction variables";
const TOO_MANY_USES: &str = "left alone, more uses in it than the search is allowed to look at";
const PRUNED: &str =
"candidate dropped before the search, no use wants it and it is not the loop's";
const ADDED: &str = "pointer given to the loop for a group of addresses to walk on";
const REWRITTEN: &str = "address use rewritten to read off a pointer of the loop's own";
const NO_PREHEADER: &str =
"not rewritten, the loop has no one place outside it to start a pointer from";
const NOT_A_WALK: &str =
"not rewritten, the group does not step through memory by a number of bytes known here";
const OUT_OF_REACH: &str =
"not rewritten, what the group is measured from is not available before the loop";
const OUT_OF_FUEL: &str = "not rewritten, the fuel for this compilation ran out first";
const RETARGETED: &str = "exit test asked of the pointer the loop walks, so the counter goes";
const COUNTER_WANTED: &str =
"exit test left alone, something else in the loop still wants the counter";
const LIMIT_TOO_FAR: &str =
"exit test left alone, the address it would compare against is further off than fits";
const MANY_EXITS: &str = "exit test left alone, the loop leaves in more than one place";
const NOT_EVERY_TURN: &str =
"exit test left alone, there is a way round the loop that does not ask it";
const NOT_A_TEST: &str =
"exit test left alone, the loop does not leave on a comparison of one moving value";
const NOT_A_COUNT: &str =
"exit test left alone, how many turns the loop takes is not a number known here";
#[derive(Debug)]
pub struct Ivopts;
impl Pass for Ivopts {
fn name(&self) -> &'static str {
"ivopts"
}
fn describe(&self) -> &'static str {
"chooses the induction variables a loop should keep, and gives a group of addresses one"
}
fn preserves(&self) -> Preserved {
Preserved::ALL.without(Analysis::Liveness).without(Analysis::Pressure)
}
fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
let mut stats = Stats::new();
if func.entry().is_none() {
return stats;
}
let machine = an.machine();
let Some(table) = machine.table() else {
stats.missed(NO_TARGET);
return stats;
};
let cfg = an.cfg(func).clone();
let loops = an.loops(func).clone();
let doms = an.dominators(func).clone();
let mut plans = Vec::new();
{
let mut scev = Scev::new(func, &cfg, &loops);
let it = Loop { func, loops: &loops, doms: &doms, machine, table };
for id in loops.all() {
consider(&it, &mut scev, id, &mut stats, &mut plans);
}
}
for plan in plans {
let Some(walk) = rewrite(func, &cfg, &loops, &doms, &plan, fuel, &mut stats) else {
continue;
};
if let Some(aim) = plan.aim {
retarget(func, &walk, &aim, fuel, &mut stats);
}
}
stats
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Kind {
Address,
Compare,
Generic,
}
impl Kind {
const fn remark(self) -> &'static str {
match self {
Self::Address => USE_ADDRESS,
Self::Compare => USE_COMPARE,
Self::Generic => USE_GENERIC,
}
}
}
#[derive(Clone, Copy, Debug)]
struct Want {
kind: Kind,
chrec: Chrec,
at: Inst,
position: usize,
}
#[derive(Clone, Copy, Debug)]
struct Use {
at: Inst,
position: usize,
offset: i128,
}
#[derive(Debug)]
struct Group {
kind: Kind,
chrec: Chrec,
uses: Vec<Use>,
exit: bool,
}
impl Group {
fn takes(&self, want: &Want) -> bool {
self.kind == Kind::Address
&& want.kind == Kind::Address
&& self.chrec.ty == want.chrec.ty
&& self.chrec.step == want.chrec.step
&& self.chrec.base.value == want.chrec.base.value
&& self.chrec.base.scale == want.chrec.base.scale
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Origin {
Original,
Derived,
Countdown,
}
#[derive(Clone, Copy, Debug)]
struct Cand {
chrec: Chrec,
origin: Origin,
}
impl Cand {
fn same(&self, other: &Self) -> bool {
self.chrec.ty == other.chrec.ty
&& self.chrec.base == other.chrec.base
&& self.chrec.step == other.chrec.step
}
}
#[derive(Debug)]
struct Plan {
id: LoopId,
chrec: Chrec,
uses: Vec<Use>,
aim: Option<Aim>,
}
#[derive(Clone, Copy, Debug)]
struct Aim {
at: Inst,
branch: Inst,
count: u128,
stays: bool,
}
struct Loop<'a> {
func: &'a Func,
loops: &'a Loops,
doms: &'a Dominators,
machine: Machine,
table: &'a CostTable,
}
fn consider(
it: &Loop<'_>,
scev: &mut Scev<'_>,
id: LoopId,
stats: &mut Stats,
plans: &mut Vec<Plan>,
) {
let Loop { func, loops, doms, machine, table } = *it;
let wants = collect(func, loops, scev, id);
if wants.is_empty() {
return;
}
stats.note(POPULATION);
for want in &wants {
stats.note(want.kind.remark());
}
if wants.len() > heuristics::IV_MAX_CONSIDERED_USES {
stats.missed(TOO_MANY_USES);
return;
}
let aimed = aim(func, loops, doms, scev, id);
let mut groups = group(wants);
for one in &mut groups {
let is_exit = |at: Aim| one.kind == Kind::Compare && one.uses.iter().any(|u| u.at == at.at);
one.exit = aimed.is_ok_and(is_exit);
}
let groups = groups;
for one in &groups {
if one.uses.len() > 1 {
stats.note(GROUPED);
}
}
let mut cands = candidates(func, loops, scev, id, &groups);
prune(&mut cands, table, &groups, stats);
for _ in &cands {
stats.note(CANDIDATE);
}
let room = machine.allocatable(RegClass::Integer).unwrap_or(0);
let chosen = select(table, &groups, &cands, room);
for _ in &chosen {
stats.note(CHOSEN);
}
let untouched = chosen.iter().all(|&at| cands[at].origin == Origin::Original)
&& chosen.len() == cands.iter().filter(|c| c.origin == Origin::Original).count();
stats.note(if untouched { KEPT } else { CHANGED });
let mut walks = 0;
for one in &groups {
if one.kind != Kind::Address {
continue;
}
let own = chosen.iter().any(|&at| {
cands[at].origin == Origin::Derived
&& cands[at].chrec.base == one.chrec.base
&& cands[at].chrec.step == one.chrec.step
&& cands[at].chrec.ty == one.chrec.ty
});
if own {
walks += 1;
plans.push(Plan { id, chrec: one.chrec, uses: one.uses.clone(), aim: None });
}
}
if walks == 0 {
return;
}
let aimed = aimed.and_then(|at| {
let Some(counter) = groups.iter().find(|one| one.exit) else { return Err(NOT_A_TEST) };
let wanted = chosen
.iter()
.any(|&had| cands[had].origin == Origin::Original && counts(counter, &cands[had]));
if wanted { Err(COUNTER_WANTED) } else { Ok(at) }
});
match aimed {
Ok(at) => {
let first = plans.len() - walks;
plans[first].aim = Some(at);
}
Err(why) => stats.missed(why),
}
}
fn counts(group: &Group, cand: &Cand) -> bool {
group.chrec.ty == cand.chrec.ty && ratio(cand.chrec.step, group.chrec.step).is_some()
}
fn collect(func: &Func, loops: &Loops, scev: &mut Scev<'_>, id: LoopId) -> Vec<Want> {
let mut wants = Vec::new();
for &block in loops.blocks(id) {
if loops.innermost(block) != Some(id) {
continue;
}
for inst in func.insts(block) {
let data = func[inst];
if data.opcode.is_terminator() {
continue;
}
if moves(func, scev, id, inst) {
continue;
}
let args = &func[data.args];
for (position, &arg) in args.iter().enumerate() {
let Some(chrec) = affine(scev, id, arg) else { continue };
let kind = classify(data.opcode, position);
wants.push(Want { kind, chrec, at: inst, position });
}
}
}
wants
}
fn aim(
func: &Func,
loops: &Loops,
doms: &Dominators,
scev: &mut Scev<'_>,
id: LoopId,
) -> Result<Aim, &'static str> {
let [exit] = loops.exits(id) else { return Err(MANY_EXITS) };
if !loops.latches(id).iter().all(|&latch| doms.dominates(exit.from, latch)) {
return Err(NOT_EVERY_TURN);
}
let Some(branch) = func.terminator(exit.from) else { return Err(NOT_A_TEST) };
if func[branch].opcode != Opcode::BrIf {
return Err(NOT_A_TEST);
}
let Some(&cond) = func[func[branch].args].first() else { return Err(NOT_A_TEST) };
let calls = &func[func.target_list(branch)];
let (Some(&taken), Some(&other)) = (calls.first(), calls.get(1)) else {
return Err(NOT_A_TEST);
};
let stays = match (loops.contains(id, taken.block), loops.contains(id, other.block)) {
(true, false) => true,
(false, true) => false,
_ => return Err(NOT_A_TEST),
};
let Def::Result { inst, .. } = func[cond].def else { return Err(NOT_A_TEST) };
if func[inst].opcode != Opcode::ICmp || func.block_of(inst) != Some(exit.from) {
return Err(NOT_A_TEST);
}
let operands = &func[func[inst].args];
let (Some(&lhs), Some(&rhs)) = (operands.first(), operands.get(1)) else {
return Err(NOT_A_TEST);
};
let moving = [affine(scev, id, lhs).is_some(), affine(scev, id, rhs).is_some()];
if moving[0] == moving[1] {
return Err(NOT_A_TEST);
}
let Some(bound) = scev.bound(id) else { return Err(NOT_A_COUNT) };
let Some(Count::Exact(count)) = bound.under_undefined_overflow() else {
return Err(NOT_A_COUNT);
};
Ok(Aim { at: inst, branch, count, stays })
}
fn moves(func: &Func, scev: &mut Scev<'_>, id: LoopId, inst: Inst) -> bool {
func[inst].results().any(|result| affine(scev, id, result).is_some())
}
fn affine(scev: &mut Scev<'_>, id: LoopId, value: Value) -> Option<Chrec> {
match scev.evolution(id, value) {
Evolution::Affine(chrec) if !chrec.step.is_zero() => Some(chrec),
_ => None,
}
}
fn classify(opcode: Opcode, position: usize) -> Kind {
match (opcode, position) {
(Opcode::Load, 0) | (Opcode::Store, 1) => Kind::Address,
(Opcode::ICmp, _) => Kind::Compare,
_ => Kind::Generic,
}
}
fn group(wants: Vec<Want>) -> Vec<Group> {
let mut groups: Vec<Group> = Vec::new();
for want in wants {
let (at, position) = (want.at, want.position);
match groups.iter_mut().find(|one| one.takes(&want)) {
Some(one) => {
let offset = want.chrec.base.offset - one.chrec.base.offset;
one.uses.push(Use { at, position, offset });
}
None => groups.push(Group {
kind: want.kind,
chrec: want.chrec,
uses: vec![Use { at, position, offset: 0 }],
exit: false,
}),
}
}
groups
}
fn candidates(
func: &Func,
loops: &Loops,
scev: &mut Scev<'_>,
id: LoopId,
groups: &[Group],
) -> Vec<Cand> {
let mut cands: Vec<Cand> = Vec::new();
let mut add = |cand: Cand| {
if !cands.iter().any(|had| had.same(&cand)) {
cands.push(cand);
}
};
for at in 0..func[loops.header(id)].params.len() {
let param = func[loops.header(id)].params[at];
if let Some(chrec) = affine(scev, id, param) {
add(Cand { chrec, origin: Origin::Original });
}
}
for one in groups {
add(Cand { chrec: one.chrec, origin: Origin::Derived });
}
if let Some(bound) = scev.bound(id) {
if let Some(count) = bound.under_undefined_overflow() {
if let Some(chrec) = countdown(count, groups) {
add(Cand { chrec, origin: Origin::Countdown });
}
}
}
cands
}
fn countdown(count: Count, groups: &[Group]) -> Option<Chrec> {
let Count::Exact(iterations) = count else { return None };
let iterations = i128::try_from(iterations).ok()?;
let ty = groups.iter().map(|one| one.chrec.ty).find(|ty| ty.is_int())?;
Some(Chrec {
base: Invariant::number(iterations),
step: Invariant::number(-1),
ty,
flags: Flags::NONE,
})
}
fn prune(cands: &mut Vec<Cand>, table: &CostTable, groups: &[Group], stats: &mut Stats) {
if cands.len() <= heuristics::IV_ALWAYS_PRUNE_CAND_SET_BOUND {
return;
}
let mut wanted = vec![false; cands.len()];
for one in groups {
let best = (0..cands.len())
.filter(|&at| !serve(table, one, &cands[at]).is_infinite())
.min_by_key(|&at| serve(table, one, &cands[at]));
if let Some(at) = best {
wanted[at] = true;
}
}
let mut at = 0;
cands.retain(|cand| {
let keep = wanted[at] || cand.origin == Origin::Original;
at += 1;
if !keep {
stats.note(PRUNED);
}
keep
});
}
fn serve(table: &CostTable, group: &Group, cand: &Cand) -> Cost {
if group.exit && cand.chrec.step.as_number().is_some_and(|step| step != 0) {
return Cost::ZERO;
}
if group.chrec.ty != cand.chrec.ty {
return Cost::INFINITE;
}
let Some(scale) = ratio(cand.chrec.step, group.chrec.step) else { return Cost::INFINITE };
let scaled = match cand.chrec.base.times(Invariant::number(scale)) {
Some(scaled) => scaled,
None => return Cost::INFINITE,
};
let Some(rest) = group.chrec.base.minus(scaled) else { return Cost::INFINITE };
match group.kind {
Kind::Address => address_cost(table, scale, rest),
Kind::Compare | Kind::Generic => value_cost(table, group.chrec.ty, scale, rest),
}
}
fn ratio(step: Invariant, wanted: Invariant) -> Option<i128> {
let (step, wanted) = (step.as_number()?, wanted.as_number()?);
if step == 0 || wanted % step != 0 {
return None;
}
Some(wanted / step)
}
fn address_cost(table: &CostTable, scale: i128, rest: Invariant) -> Cost {
let indexed = scale != 1;
let displaced = rest.offset != 0;
let symbolic = rest.value.is_some() && rest.scale != 0;
if indexed && !legal_scale(scale) {
let mult = table.mult_of(width(rest)).max(Cycles::ONE);
return Cost::cycles(mult) + address_cost(table, 1, rest);
}
if symbolic && rest.scale != 1 {
let mult = table.mult_of(width(rest)).max(Cycles::ONE);
return Cost::cycles(mult) + address_cost(table, scale, Invariant { scale: 1, ..rest });
}
let mode = match (indexed, symbolic, displaced) {
(false, false, false) => AddrMode::Base,
(false, false, true) => AddrMode::BaseDisp,
(false, true, false) => AddrMode::BaseIndex,
(false, true, true) => AddrMode::BaseIndexScaleDisp,
(true, _, false) => AddrMode::BaseIndexScale,
(true, _, true) => AddrMode::BaseIndexScaleDisp,
};
let priced = table.addr_cost(mode);
if priced.is_infinite() {
let parts = i64::from(mode.complexity());
return Cost::cycles(table.lea) * parts + table.addr_cost(AddrMode::Base);
}
priced
}
fn value_cost(table: &CostTable, ty: Type, scale: i128, rest: Invariant) -> Cost {
let mut cost = Cost::ZERO;
if scale != 1 {
let bits = ty.bits();
let mult = match Width::from_bits(bits) {
Some(width) if scale <= 0 || !scale.unsigned_abs().is_power_of_two() => {
table.mult[width.index()]
}
Some(_) => table.shift_const,
None => return Cost::INFINITE,
};
cost += Cost::cycles(mult);
}
if rest.offset != 0 || (rest.value.is_some() && rest.scale != 0) {
cost += Cost::cycles(table.add);
}
cost
}
fn legal_scale(scale: i128) -> bool {
matches!(scale, 1 | 2 | 4 | 8)
}
fn width(_rest: Invariant) -> Width {
Width::W64
}
fn upkeep(table: &CostTable, cand: &Cand) -> Cost {
let step = Cost::cycles(table.add);
match cand.origin {
Origin::Original => step,
Origin::Derived | Origin::Countdown => {
step + Cost::cycles(Cycles::ONE * i64::from(heuristics::IVOPTS_NEW_VARIABLE_BIAS))
}
}
}
fn total(
table: &CostTable,
groups: &[Group],
cands: &[Cand],
set: &[usize],
room: u32,
) -> Option<Cost> {
let mut cost = Cost::ZERO;
for group in groups {
let best = set.iter().map(|&at| serve(table, group, &cands[at])).min()?;
if best.is_infinite() {
return None;
}
cost += best * i64::try_from(group.uses.len()).unwrap_or(1);
}
for &at in set {
cost += upkeep(table, &cands[at]);
}
let spare = room.saturating_sub(heuristics::LOOP_RESERVED_REGS);
let over = u32::try_from(set.len()).unwrap_or(u32::MAX).saturating_sub(spare);
cost += Cost::cycles(Cycles::ONE * i64::from(over) * i64::from(heuristics::IVOPTS_SET_PENALTY));
Some(cost)
}
fn select(table: &CostTable, groups: &[Group], cands: &[Cand], room: u32) -> Vec<usize> {
let mut set: Vec<usize> =
(0..cands.len()).filter(|&at| cands[at].origin == Origin::Original).collect();
for group in groups {
let served = set.iter().any(|&at| !serve(table, group, &cands[at]).is_infinite());
if served {
continue;
}
let own = (0..cands.len()).find(|&at| {
cands[at].origin == Origin::Derived && cands[at].chrec.base == group.chrec.base
});
if let Some(at) = own {
set.push(at);
}
}
set.sort_unstable();
set.dedup();
let Some(mut best) = total(table, groups, cands, &set, room) else { return set };
loop {
let mut moved = None;
for at in 0..cands.len() {
let mut tried = set.clone();
match tried.iter().position(|&had| had == at) {
Some(there) => {
tried.remove(there);
}
None => tried.push(at),
}
let Some(cost) = total(table, groups, cands, &tried, room) else { continue };
if cost < best {
best = cost;
moved = Some(tried);
}
}
match moved {
Some(tried) => set = tried,
None => return set,
}
}
}
fn rewrite(
func: &mut Func,
cfg: &Cfg,
loops: &Loops,
doms: &Dominators,
plan: &Plan,
fuel: &mut Fuel,
stats: &mut Stats,
) -> Option<Walk> {
let Some(pre) = loops.preheader(cfg, plan.id) else {
stats.missed(NO_PREHEADER);
return None;
};
let header = loops.header(plan.id);
let base = plan.chrec.base;
let step = plan.chrec.step.as_number();
let from = base.value;
let (Some(step), Some(from), Type::PTR, 1) = (step, from, plan.chrec.ty, base.scale) else {
stats.missed(NOT_A_WALK);
return None;
};
let Some(home) = home(func, from) else {
stats.missed(OUT_OF_REACH);
return None;
};
if !doms.dominates(home, pre) {
stats.missed(OUT_OF_REACH);
return None;
}
if !fuel.take() {
stats.missed(OUT_OF_FUEL);
return None;
}
let term = func.terminator(pre).expect("a preheader ends in a jump to the header");
let start = past(func, term, from, base.offset);
let param = func.append_param(header, Type::PTR);
let mut preds: Vec<Block> = cfg.predecessors(header).to_vec();
preds.sort_unstable();
preds.dedup();
for block in preds {
let term = func.terminator(block).expect("a block with a successor ends in a branch");
let carry = if block == pre { start } else { past(func, term, param, step) };
for at in func.target_list(term).iter() {
let call = func[at];
if call.block != header {
continue;
}
let args = func.append_arg(call.args, carry);
func.set_block_call(at, BlockCall { block: call.block, args });
}
}
let mut ready: Vec<(Block, i128, Value)> = Vec::new();
for one in &plan.uses {
let Some(block) = func.block_of(one.at) else { continue };
let had = ready.iter().find(|&&(at, offset, _)| at == block && offset == one.offset);
let value = match had {
Some(&(_, _, value)) => value,
None => {
let value = past(func, one.at, param, one.offset);
ready.push((block, one.offset, value));
value
}
};
set_arg(func, one.at, one.position, value);
stats.optimized(REWRITTEN);
}
stats.optimized(ADDED);
Some(Walk { pre, param, start, step })
}
#[derive(Clone, Copy, Debug)]
struct Walk {
pre: Block,
param: Value,
start: Value,
step: i128,
}
fn retarget(func: &mut Func, walk: &Walk, aim: &Aim, fuel: &mut Fuel, stats: &mut Stats) {
let far = i128::try_from(aim.count).ok().and_then(|count| count.checked_mul(walk.step));
let Some(far) = far.filter(|&far| i64::try_from(far).is_ok()) else {
stats.missed(LIMIT_TOO_FAR);
return;
};
if !fuel.take() {
stats.missed(OUT_OF_FUEL);
return;
}
let term = func.terminator(walk.pre).expect("a preheader ends in a jump to the header");
let limit = past(func, term, walk.start, far);
let pred = if aim.stays { IntPred::Ne } else { IntPred::Eq };
let span = func.span(aim.at);
let args = func.push_values(&[walk.param, limit]);
let data = InstData { args, extra: Extra::IntPred(pred), ..InstData::new(Opcode::ICmp) };
let ty = func[walk.param].ty.with_lane(Type::I1);
let inst = func.create_inst(data, &[ty], span);
func.insert_before(inst, aim.at);
let cond = func[inst].first_result.expect("one result was asked for");
set_arg(func, aim.branch, 0, cond);
stats.optimized(RETARGETED);
}
fn past(func: &mut Func, before: Inst, from: Value, offset: i128) -> Value {
if offset == 0 {
return from;
}
let by = number(func, before, Type::int(64), offset);
let span = func.span(before);
let args = func.push_values(&[from, by]);
let data = InstData { args, ..InstData::new(Opcode::PtrAdd) };
let inst = func.create_inst(data, &[Type::PTR], span);
func.insert_before(inst, before);
func[inst].first_result.expect("one result was asked for")
}
fn number(func: &mut Func, before: Inst, ty: Type, value: i128) -> Value {
let imm = func.add_imm(rucc_ir::Imm::int(value, ty.lane()));
let data = InstData { extra: Extra::Imm(imm), ..InstData::new(Opcode::IConst) };
let span = func.span(before);
let inst = func.create_inst(data, &[ty], span);
func.insert_before(inst, before);
func[inst].first_result.expect("one result was asked for")
}
fn set_arg(func: &mut Func, at: Inst, position: usize, value: Value) {
let args = func[at].args;
let mut seen = 0;
func.rewrite(args, |had| {
let here = seen;
seen += 1;
if here == position { value } else { had }
});
}
fn home(func: &Func, value: Value) -> Option<Block> {
match func[value].def {
Def::Result { inst, .. } => func.block_of(inst),
Def::Param { block, .. } => Some(block),
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use rucc_base::Interner;
use rucc_ir::{
Block, Builder, Extra, Flags, Func, IntPred, MemInfo, MemOrder, Module, Opcode, Restrict,
Signature, Type, Value, verify_func,
};
use rucc_target::{TargetInfo, Triple};
use super::{
ADDED, AddrMode, CANDIDATE, CHANGED, CHOSEN, COUNTER_WANTED, Cost, Cycles, GROUPED,
Invariant, Ivopts, KEPT, LIMIT_TOO_FAR, MANY_EXITS, NO_TARGET, NOT_EVERY_TURN, OUT_OF_FUEL,
POPULATION, RETARGETED, REWRITTEN, USE_ADDRESS, USE_COMPARE, USE_GENERIC, address_cost,
width,
};
use crate::stats::Kind;
use crate::{Analyses, Fuel, Pass, Stats};
fn choose(func: &mut Func) -> Stats {
Ivopts.run(func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
}
fn sound(func: &Func, names: &mut Interner) {
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:#?}");
}
}
fn params(func: &Func, block: Block) -> usize {
func[block].params.len()
}
fn plain() -> MemInfo {
MemInfo {
size: 4,
align: 4,
order: MemOrder::NotAtomic,
tbaa: None,
restrict: Restrict::NONE,
}
}
struct Counted {
head: Block,
body: Block,
out: Block,
counter: Value,
}
fn counted(func: &mut Func, into: Block, limit: i128) -> Counted {
let head = func.create_block();
let body = func.create_block();
let out = func.create_block();
let i = func.append_param(head, Type::int(64));
let carried = func.append_param(body, Type::int(64));
let mut build = Builder::new(func, into);
let zero = build.iconst(Type::int(64), 0);
build.jump(head, &[zero]);
let mut build = Builder::new(func, head);
let stop = build.iconst(Type::int(64), limit);
let test = build.icmp(IntPred::Slt, i, stop);
build.br_if(test, body, &[i], out, &[]);
Counted { head, body, out, counter: carried }
}
fn close(func: &mut Func, it: &Counted, at: Block) {
let mut build = Builder::new(func, at);
let one = build.iconst(Type::int(64), 1);
let next = build.binary(Opcode::Add, it.counter, one, Flags::NSW);
build.jump(it.head, &[next]);
}
fn shell(names: &mut Interner) -> (Func, Block, Value) {
let signature = Signature::new().with_params(&[Type::PTR]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let base = func.append_param(entry, Type::PTR);
(func, entry, base)
}
fn counted_leaving(func: &mut Func, into: Block, limit: i128) -> Counted {
let head = func.create_block();
let body = func.create_block();
let out = func.create_block();
let i = func.append_param(head, Type::int(64));
let carried = func.append_param(body, Type::int(64));
let mut build = Builder::new(func, into);
let zero = build.iconst(Type::int(64), 0);
build.jump(head, &[zero]);
let mut build = Builder::new(func, head);
let stop = build.iconst(Type::int(64), limit);
let test = build.icmp(IntPred::Sge, i, stop);
build.br_if(test, out, &[], body, &[i]);
Counted { head, body, out, counter: carried }
}
struct Bottom {
head: Block,
out: Block,
counter: Value,
}
fn bottom(func: &mut Func, into: Block) -> Bottom {
let head = func.create_block();
let out = func.create_block();
let counter = func.append_param(head, Type::int(64));
let mut build = Builder::new(func, into);
let zero = build.iconst(Type::int(64), 0);
build.jump(head, &[zero]);
Bottom { head, out, counter }
}
fn bottom_close(func: &mut Func, it: &Bottom, limit: i128) {
let mut build = Builder::new(func, it.head);
let one = build.iconst(Type::int(64), 1);
let next = build.binary(Opcode::Add, it.counter, one, Flags::NSW);
let stop = build.iconst(Type::int(64), limit);
let test = build.icmp(IntPred::Slt, next, stop);
build.br_if(test, it.head, &[next], it.out, &[]);
}
fn stores(func: &Func) -> Vec<i128> {
let mut values: HashMap<Value, i128> = HashMap::new();
let mut block = func.entry().expect("a function with blocks in it");
for ¶m in &func[block].params {
values.insert(param, 0);
}
let mut wrote = Vec::new();
for _ in 0..10_000 {
let mut end = None;
for inst in func.insts(block) {
if func.is_terminator(inst) {
end = Some(inst);
break;
}
let data = func[inst];
let args: Vec<i128> = func[data.args].iter().map(|arg| values[arg]).collect();
if data.opcode == Opcode::Store {
wrote.push(args[1]);
continue;
}
let result = data.first_result.expect("one result");
let it = match data.opcode {
Opcode::IConst => {
let (imm, ty) =
crate::fold::constant(func, result).expect("a constant is one");
imm.signed(ty)
}
Opcode::ICmp => {
let Extra::IntPred(pred) = data.extra else {
panic!("a comparison carries its predicate");
};
i128::from(match pred {
IntPred::Slt => args[0] < args[1],
IntPred::Sge => args[0] >= args[1],
IntPred::Ne => args[0] != args[1],
IntPred::Eq => args[0] == args[1],
other => panic!("nothing here compares with {other:?}"),
})
}
Opcode::Add | Opcode::PtrAdd => args[0] + args[1],
Opcode::Mul => args[0] * args[1],
Opcode::Load => 0,
other => panic!("nothing here writes a {other:?}"),
};
values.insert(result, it);
}
let end = end.expect("every block here ends in a terminator");
let data = func[end];
let call = match data.opcode {
Opcode::Jump => func.successors(end).next().expect("a jump has one edge"),
Opcode::BrIf => {
let cond = values[&func[data.args][0]];
let mut edges = func.successors(end);
let then = edges.next().expect("a branch has two edges");
let other = edges.next().expect("a branch has two edges");
if cond == 0 { other } else { then }
}
Opcode::Return => return wrote,
other => panic!("nothing here ends a block with a {other:?}"),
};
let carried: Vec<i128> = func[call.args].iter().map(|arg| values[arg]).collect();
for (¶m, arg) in func[call.block].params.iter().zip(carried) {
values.insert(param, arg);
}
block = call.block;
}
panic!("the loop never ended");
}
fn leaves_on(func: &Func, block: Block) -> IntPred {
let term = func.terminator(block).expect("every block here has one");
let cond = func[func[term].args][0];
let rucc_ir::Def::Result { inst, .. } = func[cond].def else {
panic!("the condition came out of a comparison");
};
let Extra::IntPred(pred) = func[inst].extra else {
panic!("a comparison carries its predicate");
};
pred
}
fn element(build: &mut Builder<'_>, base: Value, counter: Value, away: i128) -> Value {
let four = build.iconst(Type::int(64), 4);
let by = build.binary(Opcode::Mul, counter, four, Flags::NSW);
let at = build.binary(Opcode::PtrAdd, base, by, Flags::NONE);
if away == 0 {
return at;
}
let past = build.iconst(Type::int(64), away * 4);
build.binary(Opcode::PtrAdd, at, past, Flags::NONE)
}
fn some_value() -> Value {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
func.append_param(entry, Type::PTR)
}
fn priced() -> crate::machine::Machine {
crate::machine::fixtures::machine()
}
#[test]
fn an_array_read_is_one_addressing_mode_rather_than_an_addition_in_front_of_one() {
let machine = priced();
let table = machine.table().unwrap();
let array = Invariant::of(some_value());
assert_eq!(address_cost(table, 4, array), table.addr_cost(AddrMode::BaseIndexScale));
let past = Invariant { offset: 8, ..array };
assert_eq!(address_cost(table, 4, past), table.addr_cost(AddrMode::BaseIndexScaleDisp));
}
#[test]
fn a_pointer_the_loop_walks_costs_a_plain_base_and_that_is_what_it_competes_with() {
let machine = priced();
let table = machine.table().unwrap();
let walked = address_cost(table, 1, Invariant::number(0));
assert_eq!(walked, table.addr_cost(AddrMode::Base));
let indexed = address_cost(table, 4, Invariant::of(some_value()));
assert!(walked < indexed, "an index costs something or the two would never be compared");
assert_eq!(
indexed.cycles,
walked.cycles + table.add,
"an index costs exactly what an addition costs here, which is the number the whole \
trade turns on",
);
}
#[test]
fn a_base_that_is_a_multiple_of_a_register_is_multiplied_out_first() {
let machine = priced();
let table = machine.table().unwrap();
let value = some_value();
let doubled = Invariant { value: Some(value), scale: 2, offset: 0 };
let mult = table.mult_of(width(doubled)).max(Cycles::ONE);
let want = Cost::cycles(mult) + address_cost(table, 4, Invariant::of(value));
assert_eq!(address_cost(table, 4, doubled), want);
}
#[test]
fn a_scale_no_mode_holds_is_multiplied_out_and_the_rest_is_still_an_address() {
let machine = priced();
let table = machine.table().unwrap();
let array = Invariant::of(some_value());
let mult = table.mult_of(width(array)).max(Cycles::ONE);
let want = Cost::cycles(mult) + address_cost(table, 1, array);
assert_eq!(address_cost(table, 3, array), want);
}
#[test]
fn a_loop_walking_one_array_has_one_address_use_and_one_comparison() {
let mut names = Interner::new();
let (mut func, entry, base) = shell(&mut names);
let it = counted(&mut func, entry, 100);
let mut build = Builder::new(&mut func, it.body);
let addr = element(&mut build, base, it.counter, 0);
let zero = build.iconst(Type::int(32), 0);
build.store(zero, addr, plain(), Flags::NONE);
close(&mut func, &it, it.body);
Builder::new(&mut func, it.out).ret(&[]);
let before = params(&func, it.head);
let stats = choose(&mut func);
assert_eq!(stats.count(Kind::Note, POPULATION), 1);
assert_eq!(stats.count(Kind::Note, USE_ADDRESS), 1);
assert_eq!(
stats.count(Kind::Note, USE_COMPARE),
1,
"the exit test is a use of the counter"
);
assert_eq!(stats.count(Kind::Note, USE_GENERIC), 0, "the increment is the variable itself");
assert_eq!(stats.count(Kind::Optimized, ADDED), 1);
assert_eq!(stats.count(Kind::Optimized, REWRITTEN), 1);
assert_eq!(params(&func, it.head), before + 1, "the loop carries the pointer round");
sound(&func, &mut names);
}
#[test]
fn three_accesses_a_constant_apart_are_one_group_wanting_one_variable() {
let mut names = Interner::new();
let (mut func, entry, base) = shell(&mut names);
let it = counted(&mut func, entry, 100);
let mut build = Builder::new(&mut func, it.body);
let zero = build.iconst(Type::int(32), 0);
for away in [0, 1, 2] {
let addr = element(&mut build, base, it.counter, away);
build.store(zero, addr, plain(), Flags::NONE);
}
close(&mut func, &it, it.body);
Builder::new(&mut func, it.out).ret(&[]);
let before = params(&func, it.head);
let stats = choose(&mut func);
assert_eq!(stats.count(Kind::Note, USE_ADDRESS), 3);
assert_eq!(stats.count(Kind::Note, GROUPED), 1, "one group, not three");
assert_eq!(stats.count(Kind::Note, CHOSEN), 1);
assert_eq!(stats.count(Kind::Note, CHANGED), 1);
assert_eq!(stats.count(Kind::Optimized, ADDED), 1, "one pointer, not three");
assert_eq!(stats.count(Kind::Optimized, REWRITTEN), 3);
assert_eq!(params(&func, it.head), before + 1);
sound(&func, &mut names);
}
#[test]
fn a_walk_of_two_arrays_takes_a_variable_it_did_not_have() {
let mut names = Interner::new();
let (mut func, entry, base) = shell(&mut names);
let it = counted(&mut func, entry, 100);
let mut build = Builder::new(&mut func, it.body);
let from = element(&mut build, base, it.counter, 0);
let read = build.load(Type::int(32), from, plain(), Flags::NONE);
let into = element(&mut build, base, it.counter, 4096);
build.store(read, into, plain(), Flags::NONE);
close(&mut func, &it, it.body);
Builder::new(&mut func, it.out).ret(&[]);
let stats = choose(&mut func);
assert_eq!(stats.count(Kind::Note, USE_ADDRESS), 2);
assert_eq!(stats.count(Kind::Note, GROUPED), 1, "a constant apart, so one group");
assert_eq!(stats.count(Kind::Note, CHOSEN), 1, "one address variable, and no counter");
assert_eq!(stats.count(Kind::Note, CHANGED), 1);
assert_eq!(stats.count(Kind::Optimized, ADDED), 1);
assert_eq!(stats.count(Kind::Optimized, REWRITTEN), 2, "the read and the write");
sound(&func, &mut names);
}
#[test]
fn a_loop_that_only_reads_a_fixed_address_is_not_asked_about() {
let mut names = Interner::new();
let (mut func, entry, base) = shell(&mut names);
let it = counted(&mut func, entry, 100);
let mut build = Builder::new(&mut func, it.body);
let read = build.load(Type::int(32), base, plain(), Flags::NONE);
build.store(read, base, plain(), Flags::NONE);
close(&mut func, &it, it.body);
Builder::new(&mut func, it.out).ret(&[]);
let stats = choose(&mut func);
assert_eq!(stats.count(Kind::Note, USE_ADDRESS), 0);
assert_eq!(stats.count(Kind::Note, USE_COMPARE), 1);
assert_eq!(stats.count(Kind::Note, POPULATION), 1);
assert_eq!(stats.count(Kind::Note, KEPT), 1);
assert_eq!(stats.count(Kind::Note, CHOSEN), 1);
assert!(!stats.changed(), "a loop with nothing to rewrite is not rewritten");
}
#[test]
fn a_machine_nobody_priced_is_told_so_rather_than_guessed_at() {
let mut names = Interner::new();
let (mut func, entry, base) = shell(&mut names);
let it = counted(&mut func, entry, 100);
let mut build = Builder::new(&mut func, it.body);
let addr = element(&mut build, base, it.counter, 0);
let zero = build.iconst(Type::int(32), 0);
build.store(zero, addr, plain(), Flags::NONE);
close(&mut func, &it, it.body);
Builder::new(&mut func, it.out).ret(&[]);
let mut an = Analyses::new(crate::machine::Machine::unknown());
let stats = Ivopts.run(&mut func, &mut an, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Missed, NO_TARGET), 1);
assert_eq!(stats.count(Kind::Note, POPULATION), 0);
assert_eq!(stats.count(Kind::Note, CANDIDATE), 0);
assert!(!stats.changed(), "an unpriced machine gets no rewrite either");
}
#[test]
fn the_rewrite_stops_when_the_fuel_does_and_says_so() {
let mut names = Interner::new();
let (mut func, entry, base) = shell(&mut names);
let it = counted(&mut func, entry, 100);
let mut build = Builder::new(&mut func, it.body);
let addr = element(&mut build, base, it.counter, 0);
let zero = build.iconst(Type::int(32), 0);
build.store(zero, addr, plain(), Flags::NONE);
close(&mut func, &it, it.body);
Builder::new(&mut func, it.out).ret(&[]);
let before = params(&func, it.head);
let mut an = crate::machine::fixtures::analyses();
let stats = Ivopts.run(&mut func, &mut an, &mut Fuel::of(0));
assert_eq!(stats.count(Kind::Missed, OUT_OF_FUEL), 1);
assert_eq!(stats.count(Kind::Optimized, ADDED), 0);
assert_eq!(params(&func, it.head), before, "nothing was half done");
assert_eq!(
stats.count(Kind::Note, POPULATION),
1,
"the choosing still happened and still reported"
);
sound(&func, &mut names);
}
#[test]
fn a_loop_that_only_walks_an_array_stops_counting_and_tests_the_pointer() {
let mut names = Interner::new();
let (mut func, entry, base) = shell(&mut names);
let it = counted(&mut func, entry, 100);
let mut build = Builder::new(&mut func, it.body);
let addr = element(&mut build, base, it.counter, 0);
let zero = build.iconst(Type::int(32), 0);
build.store(zero, addr, plain(), Flags::NONE);
close(&mut func, &it, it.body);
Builder::new(&mut func, it.out).ret(&[]);
let before = stores(&func);
let stats = choose(&mut func);
assert_eq!(stats.count(Kind::Optimized, ADDED), 1);
assert_eq!(stats.count(Kind::Optimized, RETARGETED), 1);
assert_eq!(
stats.count(Kind::Note, CHOSEN),
1,
"the pointer, and nothing that only the test wanted"
);
assert_eq!(
leaves_on(&func, it.head),
IntPred::Ne,
"the loop keeps going while the pointer has not landed on the limit"
);
assert_eq!(stores(&func), before, "the same hundred addresses, in the same order");
assert_eq!(before.len(), 100, "and the loop under test really did run a hundred times");
sound(&func, &mut names);
}
#[test]
fn a_loop_that_leaves_when_its_test_holds_gets_the_comparison_the_other_way_round() {
let mut names = Interner::new();
let (mut func, entry, base) = shell(&mut names);
let it = counted_leaving(&mut func, entry, 100);
let mut build = Builder::new(&mut func, it.body);
let addr = element(&mut build, base, it.counter, 0);
let zero = build.iconst(Type::int(32), 0);
build.store(zero, addr, plain(), Flags::NONE);
close(&mut func, &it, it.body);
Builder::new(&mut func, it.out).ret(&[]);
let before = stores(&func);
let stats = choose(&mut func);
assert_eq!(stats.count(Kind::Optimized, RETARGETED), 1);
assert_eq!(leaves_on(&func, it.head), IntPred::Eq, "it leaves when the pointer lands");
assert_eq!(stores(&func), before);
assert_eq!(before.len(), 100);
sound(&func, &mut names);
}
#[test]
fn a_loop_that_tests_at_the_bottom_gets_a_limit_that_is_one_further_on() {
let mut names = Interner::new();
let (mut func, entry, base) = shell(&mut names);
let it = bottom(&mut func, entry);
let mut build = Builder::new(&mut func, it.head);
let addr = element(&mut build, base, it.counter, 0);
let zero = build.iconst(Type::int(32), 0);
build.store(zero, addr, plain(), Flags::NONE);
bottom_close(&mut func, &it, 7);
Builder::new(&mut func, it.out).ret(&[]);
let before = stores(&func);
assert_eq!(before, vec![0, 4, 8, 12, 16, 20, 24], "seven turns, and the last one counts");
let stats = choose(&mut func);
assert_eq!(stats.count(Kind::Optimized, ADDED), 1);
assert_eq!(stats.count(Kind::Optimized, RETARGETED), 1);
assert_eq!(stores(&func), before, "all seven, not six and not eight");
sound(&func, &mut names);
}
#[test]
fn a_loop_that_still_wants_its_counter_keeps_both_it_and_the_test_it_is_in() {
let mut names = Interner::new();
let (mut func, entry, base) = shell(&mut names);
let it = counted(&mut func, entry, 100);
let mut build = Builder::new(&mut func, it.body);
let addr = element(&mut build, base, it.counter, 0);
let zero = build.iconst(Type::int(32), 0);
build.store(zero, addr, plain(), Flags::NONE);
build.store(it.counter, base, plain(), Flags::NONE);
close(&mut func, &it, it.body);
Builder::new(&mut func, it.out).ret(&[]);
let before = stores(&func);
let stats = choose(&mut func);
assert_eq!(stats.count(Kind::Note, USE_GENERIC), 1, "the counter, written out");
assert_eq!(stats.count(Kind::Optimized, ADDED), 1, "the walk is still worth making");
assert_eq!(stats.count(Kind::Optimized, RETARGETED), 0);
assert_eq!(stats.count(Kind::Missed, COUNTER_WANTED), 1);
assert_eq!(leaves_on(&func, it.head), IntPred::Slt, "the test is the one it arrived as");
assert_eq!(stores(&func), before);
sound(&func, &mut names);
}
#[test]
fn a_loop_with_two_ways_out_keeps_the_test_it_has() {
let mut names = Interner::new();
let (mut func, entry, base) = shell(&mut names);
let it = counted(&mut func, entry, 100);
let more = func.create_block();
let carried = func.append_param(more, Type::int(64));
let mut build = Builder::new(&mut func, it.body);
let seen = build.load(Type::int(32), base, plain(), Flags::NONE);
let zero = build.iconst(Type::int(32), 0);
let done = build.icmp(IntPred::Slt, seen, zero);
build.br_if(done, it.out, &[], more, &[it.counter]);
let mut build = Builder::new(&mut func, more);
let addr = element(&mut build, base, carried, 0);
let nothing = build.iconst(Type::int(32), 0);
build.store(nothing, addr, plain(), Flags::NONE);
let one = build.iconst(Type::int(64), 1);
let next = build.binary(Opcode::Add, carried, one, Flags::NSW);
build.jump(it.head, &[next]);
Builder::new(&mut func, it.out).ret(&[]);
let before = stores(&func);
let stats = choose(&mut func);
assert_eq!(stats.count(Kind::Optimized, ADDED), 1);
assert_eq!(stats.count(Kind::Optimized, RETARGETED), 0);
assert_eq!(stats.count(Kind::Missed, MANY_EXITS), 1);
assert_eq!(stores(&func), before);
sound(&func, &mut names);
}
#[test]
fn a_test_the_loop_can_get_past_without_asking_is_left_where_it_is() {
let mut names = Interner::new();
let (mut func, entry, base) = shell(&mut names);
let head = func.create_block();
let check = func.create_block();
let body = func.create_block();
let out = func.create_block();
let counter = func.append_param(head, Type::int(64));
let mut build = Builder::new(&mut func, entry);
let zero = build.iconst(Type::int(64), 0);
build.jump(head, &[zero]);
let mut build = Builder::new(&mut func, head);
let seen = build.load(Type::int(32), base, plain(), Flags::NONE);
let none = build.iconst(Type::int(32), 0);
let ask = build.icmp(IntPred::Slt, seen, none);
build.br_if(ask, check, &[], body, &[]);
let mut build = Builder::new(&mut func, check);
let stop = build.iconst(Type::int(64), 100);
let test = build.icmp(IntPred::Slt, counter, stop);
build.br_if(test, body, &[], out, &[]);
let mut build = Builder::new(&mut func, body);
let addr = element(&mut build, base, counter, 0);
let nothing = build.iconst(Type::int(32), 0);
build.store(nothing, addr, plain(), Flags::NONE);
let one = build.iconst(Type::int(64), 1);
let next = build.binary(Opcode::Add, counter, one, Flags::NSW);
build.jump(head, &[next]);
Builder::new(&mut func, out).ret(&[]);
let stats = choose(&mut func);
assert_eq!(stats.count(Kind::Optimized, ADDED), 1);
assert_eq!(stats.count(Kind::Optimized, RETARGETED), 0);
assert_eq!(stats.count(Kind::Missed, NOT_EVERY_TURN), 1);
assert_eq!(leaves_on(&func, check), IntPred::Slt);
sound(&func, &mut names);
}
#[test]
fn one_unit_of_fuel_buys_the_walk_and_not_the_test() {
let mut names = Interner::new();
let (mut func, entry, base) = shell(&mut names);
let it = counted(&mut func, entry, 100);
let mut build = Builder::new(&mut func, it.body);
let addr = element(&mut build, base, it.counter, 0);
let zero = build.iconst(Type::int(32), 0);
build.store(zero, addr, plain(), Flags::NONE);
close(&mut func, &it, it.body);
Builder::new(&mut func, it.out).ret(&[]);
let before = stores(&func);
let mut an = crate::machine::fixtures::analyses();
let stats = Ivopts.run(&mut func, &mut an, &mut Fuel::of(1));
assert_eq!(stats.count(Kind::Optimized, ADDED), 1);
assert_eq!(stats.count(Kind::Optimized, RETARGETED), 0);
assert_eq!(stats.count(Kind::Missed, OUT_OF_FUEL), 1);
assert_eq!(leaves_on(&func, it.head), IntPred::Slt, "the counter is still what is tested");
assert_eq!(stores(&func), before);
sound(&func, &mut names);
}
#[test]
fn a_walk_too_long_to_measure_leaves_the_test_alone() {
let mut names = Interner::new();
let (mut func, entry, base) = shell(&mut names);
let it = counted(&mut func, entry, i128::from(i64::MAX) / 2);
let mut build = Builder::new(&mut func, it.body);
let addr = element(&mut build, base, it.counter, 0);
let zero = build.iconst(Type::int(32), 0);
build.store(zero, addr, plain(), Flags::NONE);
close(&mut func, &it, it.body);
Builder::new(&mut func, it.out).ret(&[]);
let stats = choose(&mut func);
assert_eq!(stats.count(Kind::Optimized, ADDED), 1);
assert_eq!(stats.count(Kind::Optimized, RETARGETED), 0);
assert_eq!(stats.count(Kind::Missed, LIMIT_TOO_FAR), 1);
sound(&func, &mut names);
}
#[test]
fn a_function_with_no_loop_in_it_is_left_entirely_alone() {
let mut names = Interner::new();
let (mut func, entry, base) = shell(&mut names);
let mut build = Builder::new(&mut func, entry);
let zero = build.iconst(Type::int(32), 0);
build.store(zero, base, plain(), Flags::NONE);
build.ret(&[]);
let stats = choose(&mut func);
assert_eq!(stats.events().len(), 0);
assert!(!stats.changed());
}
}