use std::collections::{HashMap, HashSet};
use rucc_ir::{Block, Builder, Func, Inst, InstData, Opcode, Type, Value};
use crate::cfg::Cfg;
use crate::dom::Dominators;
use crate::loops::{LoopId, Loops};
use crate::purity::Facts;
use crate::scev::{Bound, Count, Invariant, Scev};
use crate::{Analyses, Fuel, Pass, Preserved, Stats};
const DELETED: &str = "loop taken out, it runs a known number of times and leaves nothing behind";
const WRITTEN: &str = "what the loop was going to leave behind worked out in front of it instead";
const NO_COUNT: &str = "loop left as it was, how many times it runs is not a number known here";
const SHAPE: &str =
"loop left as it was, it has no preheader or it leaves from more than one place";
const EFFECTS: &str = "loop left as it was, something in it does more than work out a value";
const NO_FORM: &str =
"loop left as it was, what it leaves behind is not a thing this can work out in front of it";
const ENTRIES: &str = "loop left as it was, it is reached somewhere other than at its header";
const NO_FUEL: &str = "loop left as it was, the pass ran out of fuel";
#[derive(Debug)]
pub struct LoopDelete;
impl Pass for LoopDelete {
fn name(&self) -> &'static str {
"loop-delete"
}
fn describe(&self) -> &'static str {
"a loop that runs a known number of times and leaves nothing behind is taken out"
}
fn preserves(&self) -> Preserved {
Preserved::NONE
}
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 mut done: HashSet<Block> = HashSet::new();
let mut say = true;
while let Some(job) = plan(func, an, &done, &mut stats, say) {
say = false;
if !fuel.take() {
stats.missed(NO_FUEL);
break;
}
done.insert(job.header);
for _ in 0..apply(func, &job) {
stats.optimized(WRITTEN);
}
stats.optimized(DELETED);
an.clear();
crate::simplify_cfg::sweep(func, an, &mut stats);
}
an.clear();
stats
}
}
#[derive(Debug)]
struct Job {
header: Block,
preheader: Block,
exit: Block,
inside: HashSet<Block>,
args: Vec<Value>,
ends: Vec<(Value, Leaves)>,
}
#[derive(Clone, Copy, Debug)]
struct Leaves {
ty: Type,
end: Invariant,
}
fn plan(
func: &Func,
an: &mut Analyses,
done: &HashSet<Block>,
stats: &mut Stats,
say: bool,
) -> Option<Job> {
let facts = an.purity();
let cfg = an.cfg(func);
let doms = an.dominators(func);
let loops = an.loops(func);
let mut scev = Scev::new(func, cfg, loops);
let mut found: Option<(u32, Job)> = None;
for id in loops.all() {
if done.contains(&loops.header(id)) {
continue;
}
match consider(func, cfg, doms, loops, facts, &mut scev, id) {
Ok(job) => {
let depth = loops.depth(id);
if found.as_ref().is_none_or(|(had, _)| depth > *had) {
found = Some((depth, job));
}
}
Err(why) if say => stats.missed(why),
Err(_) => (),
}
}
found.map(|(_, job)| job)
}
fn consider(
func: &Func,
cfg: &Cfg,
doms: &Dominators,
loops: &Loops,
facts: &Facts,
scev: &mut Scev<'_>,
id: LoopId,
) -> Result<Job, &'static str> {
let header = loops.header(id);
let preheader = loops.preheader(cfg, id).ok_or(SHAPE)?;
let [only] = loops.exits(id) else {
return Err(SHAPE);
};
let blocks = loops.blocks(id).to_vec();
let inside: HashSet<Block> = blocks.iter().copied().collect();
for &block in &blocks {
if block != header && cfg.predecessors(block).iter().any(|at| !inside.contains(at)) {
return Err(ENTRIES);
}
for inst in func.insts(block) {
if func.is_terminator(inst) {
if !matches!(func[inst].opcode, Opcode::Jump | Opcode::BrIf) {
return Err(EFFECTS);
}
continue;
}
if !crate::dce::removable(func, inst, facts) {
return Err(EFFECTS);
}
}
}
let count =
scev.bound(id).as_ref().and_then(Bound::under_undefined_overflow).ok_or(NO_COUNT)?;
let term = func.terminator(only.from).ok_or(SHAPE)?;
let leaving = func.successors(term).find(|call| call.block == only.to).ok_or(SHAPE)?;
let args = func[leaving.args].to_vec();
let mut wanted = read_outside(func, &blocks, &inside);
for &arg in &args {
if loops.is_invariant(func, id, arg) {
debug_assert!(
doms.dominates(defined_in(func, arg), preheader),
"a value outside the loop that reaches the exit test dominates the preheader"
);
continue;
}
if !wanted.contains(&arg) {
wanted.push(arg);
}
}
let mut ends = Vec::with_capacity(wanted.len());
for value in wanted {
let end = ending(func, scev, id, value, count).ok_or(NO_FORM)?;
debug_assert!(
named(end).is_none_or(|on| doms.dominates(defined_in(func, on), preheader)),
"a value the loop does not change is defined outside it and so dominates the preheader"
);
ends.push((value, end));
}
Ok(Job { header, preheader, exit: only.to, inside, args, ends })
}
fn read_outside(func: &Func, blocks: &[Block], inside: &HashSet<Block>) -> Vec<Value> {
let mut defined: HashSet<Value> = HashSet::new();
for &block in blocks {
defined.extend(func[block].params.iter().copied());
for inst in func.insts(block) {
defined.extend(func[inst].results());
}
}
let mut found = Vec::new();
for block in func.blocks() {
if inside.contains(&block) {
continue;
}
for inst in func.insts(block) {
let reads = func[func[inst].args].iter().copied();
let passes = func.successors(inst).flat_map(|call| func[call.args].to_vec());
for value in reads.chain(passes) {
if defined.contains(&value) && !found.contains(&value) {
found.push(value);
}
}
}
}
found
}
fn ending(
func: &Func,
scev: &mut Scev<'_>,
id: LoopId,
value: Value,
count: Count,
) -> Option<Leaves> {
let Count::Exact(trips) = count else {
return None;
};
let trips = i128::try_from(trips).ok()?;
let chrec = scev.evolution(id, value).chrec()?;
let end = chrec.step.times(Invariant::number(trips)).and_then(|all| chrec.base.plus(all))?;
let plain = end.plain()?;
if plain.read.is_some() {
return None;
}
if plain.value.is_some_and(|named| func[named].ty != chrec.ty) {
return None;
}
Some(Leaves { ty: chrec.ty, end })
}
fn named(leaves: Leaves) -> Option<Value> {
leaves.end.plain().and_then(|plain| plain.value.filter(|_| plain.scale != 0))
}
fn defined_in(func: &Func, value: Value) -> Block {
match func[value].def {
rucc_ir::Def::Result { inst, .. } => {
func.block_of(inst).expect("a value in use is defined in a block")
}
rucc_ir::Def::Param { block, .. } => block,
}
}
fn apply(func: &mut Func, job: &Job) -> usize {
let term = func.terminator(job.preheader).expect("a preheader ends in a jump to the header");
let mut instead: HashMap<Value, Value> = HashMap::new();
for &(value, leaves) in &job.ends {
let worked = write(func, term, leaves.ty, leaves.end);
instead.insert(value, worked);
}
swap_in(func, job, &instead);
let args: Vec<Value> =
job.args.iter().map(|arg| instead.get(arg).copied().unwrap_or(*arg)).collect();
func.remove_inst(term);
Builder::new(func, job.preheader).jump(job.exit, &args);
instead.len()
}
fn swap_in(func: &mut Func, job: &Job, instead: &HashMap<Value, Value>) {
if instead.is_empty() {
return;
}
let outside: Vec<Block> = func.blocks().filter(|at| !job.inside.contains(at)).collect();
for block in outside {
for inst in func.insts(block).collect::<Vec<_>>() {
let mut lists = vec![func[inst].args];
lists.extend(func.successors(inst).map(|call| call.args));
for list in lists {
func.rewrite(list, |value| instead.get(&value).copied().unwrap_or(value));
}
}
}
}
fn write(func: &mut Func, before: Inst, ty: Type, end: Invariant) -> Value {
let plain = end.plain().expect("consider refused anything this cannot write");
let Some(value) = plain.value.filter(|_| plain.scale != 0) else {
return crate::ivopts::number(func, before, ty, plain.offset);
};
let mut so_far = value;
if plain.scale != 1 {
let by = crate::ivopts::number(func, before, ty, plain.scale);
so_far = arith(func, before, Opcode::Mul, so_far, by, ty);
}
if plain.offset != 0 {
let by = crate::ivopts::number(func, before, ty, plain.offset);
so_far = arith(func, before, Opcode::Add, so_far, by, ty);
}
so_far
}
fn arith(
func: &mut Func,
before: Inst,
opcode: Opcode,
left: Value,
right: Value,
ty: Type,
) -> Value {
let span = func.span(before);
let args = func.push_values(&[left, right]);
let inst = func.create_inst(InstData { args, ..InstData::new(opcode) }, &[ty], span);
func.insert_before(inst, before);
func[inst].first_result.expect("one result was asked for")
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{
Block, Builder, Def, Flags, Func, IntPred, MemInfo, MemOrder, Module, Opcode, Restrict,
Signature, Type, Value, verify_func,
};
use rucc_target::{TargetInfo, Triple};
use super::{DELETED, EFFECTS, LoopDelete, NO_COUNT, NO_FORM, NO_FUEL, WRITTEN};
use crate::stats::Kind;
use crate::{Fuel, Pass, Stats};
fn delete(func: &mut Func, fuel: &mut Fuel) -> Stats {
LoopDelete.run(func, &mut crate::machine::fixtures::analyses(), fuel)
}
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 tally(func: &Func, opcode: Opcode) -> usize {
func.blocks()
.flat_map(|block| func.insts(block))
.filter(|&inst| func[inst].opcode == opcode)
.count()
}
fn handed_value(func: &Func, done: &Block) -> Option<Value> {
let cfg = crate::cfg::Cfg::new(func);
let [only] = cfg.predecessors(*done) else {
return None;
};
let term = func.terminator(*only)?;
let call = func.successors(term).find(|call| call.block == *done)?;
let args = func[call.args].to_vec();
let [arg] = args[..] else {
return None;
};
Some(arg)
}
fn handed(func: &Func, done: &Block) -> Option<i128> {
let value = handed_value(func, done)?;
let Def::Result { inst, .. } = func[value].def else {
return None;
};
if func[inst].opcode != Opcode::Mul {
return None;
}
let args = func[func[inst].args].to_vec();
let (imm, ty) = crate::fold::constant(func, args[1])?;
Some(imm.signed(ty))
}
fn loops(func: &Func) -> usize {
let cfg = crate::cfg::Cfg::new(func);
let doms = crate::dom::Dominators::new(&cfg);
crate::loops::Loops::new(&cfg, &doms).count()
}
fn plain() -> MemInfo {
MemInfo {
size: 4,
align: 4,
order: MemOrder::NotAtomic,
tbaa: None,
owns: 0,
restrict: Restrict::NONE,
}
}
enum Limit {
Number(i128),
Given,
}
#[derive(Clone, Copy, PartialEq)]
enum What {
Nothing,
Writes,
HandsOut,
HandsOne,
HandsSquare,
ReadAfter,
}
impl What {
fn hands_out(self) -> bool {
matches!(self, What::HandsOut | What::HandsOne | What::HandsSquare)
}
}
struct Shape {
names: Interner,
func: Func,
entry: Block,
done: Block,
}
fn shaped(limit: Limit, what: What) -> Shape {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::PTR, Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let head = func.create_block();
let body = func.create_block();
let done = func.create_block();
let place = func.append_param(entry, Type::PTR);
let given = func.append_param(entry, Type::int(32));
let i = func.append_param(head, Type::int(32));
let sum = func.append_param(head, Type::int(32));
let carried = func.append_param(body, Type::int(32));
let running = func.append_param(body, Type::int(32));
if what.hands_out() {
func.append_param(done, Type::int(32));
}
let mut build = Builder::new(&mut func, entry);
let zero = build.iconst(Type::int(32), 0);
build.jump(head, &[zero, zero]);
Builder::new(&mut func, head).jump(body, &[i, sum]);
let mut build = Builder::new(&mut func, body);
let one = build.iconst(Type::int(32), 1);
let by = match what {
What::HandsOne => one,
What::HandsSquare => carried,
_ => given,
};
let total = build.binary(Opcode::Add, running, by, Flags::NSW);
if what == What::Writes {
build.store(total, place, plain(), Flags::NONE);
}
let next = build.binary(Opcode::Add, carried, one, Flags::NSW);
let stop = match limit {
Limit::Number(n) => build.iconst(Type::int(32), n),
Limit::Given => given,
};
let test = build.icmp(IntPred::Slt, next, stop);
let out: Vec<Value> = if what.hands_out() { vec![total] } else { Vec::new() };
build.br_if(test, head, &[next, total], done, &out);
let mut build = Builder::new(&mut func, done);
if what == What::ReadAfter {
build.store(total, place, plain(), Flags::NONE);
}
build.ret(&[]);
Shape { names, func, entry, done }
}
#[test]
fn a_loop_that_leaves_nothing_behind_is_taken_out() {
let mut it = shaped(Limit::Number(1000), What::Nothing);
let stats = delete(&mut it.func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, DELETED), 1);
assert_eq!(loops(&it.func), 0);
assert_eq!(tally(&it.func, Opcode::Add), 0, "the counter and the total go with it");
assert_eq!(tally(&it.func, Opcode::BrIf), 0);
sound(&it.func, &mut it.names);
}
#[test]
fn the_blocks_it_took_out_are_swept_rather_than_left_unreachable() {
let mut it = shaped(Limit::Number(1000), What::Nothing);
delete(&mut it.func, &mut Fuel::unlimited());
let left: Vec<Block> = it.func.blocks().collect();
assert_eq!(left, vec![it.entry, it.done], "the header and the body are gone");
sound(&it.func, &mut it.names);
}
#[test]
fn a_count_that_rests_on_more_than_signed_overflow_is_not_enough() {
let mut it = shaped(Limit::Given, What::Nothing);
let stats = delete(&mut it.func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, DELETED), 0);
assert_eq!(stats.count(Kind::Missed, NO_COUNT), 1);
assert_eq!(loops(&it.func), 1);
sound(&it.func, &mut it.names);
}
#[test]
fn a_loop_that_writes_to_memory_is_left_alone() {
let mut it = shaped(Limit::Number(1000), What::Writes);
let stats = delete(&mut it.func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, DELETED), 0);
assert_eq!(stats.count(Kind::Missed, EFFECTS), 1);
assert_eq!(loops(&it.func), 1);
assert_eq!(tally(&it.func, Opcode::Store), 1);
sound(&it.func, &mut it.names);
}
#[test]
fn a_total_read_after_the_loop_by_another_road_is_worked_out_too() {
let mut it = shaped(Limit::Number(1000), What::ReadAfter);
let stats = delete(&mut it.func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, DELETED), 1);
assert_eq!(stats.count(Kind::Optimized, WRITTEN), 1);
assert_eq!(loops(&it.func), 0);
assert_eq!(tally(&it.func, Opcode::Mul), 1, "one multiply, by the trip count");
assert_eq!(tally(&it.func, Opcode::Store), 1, "and the store that read it is still there");
sound(&it.func, &mut it.names);
}
#[test]
fn a_total_the_loop_hands_over_is_worked_out_in_front_of_it() {
let mut it = shaped(Limit::Number(1000), What::HandsOut);
let stats = delete(&mut it.func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, DELETED), 1);
assert_eq!(stats.count(Kind::Optimized, WRITTEN), 1);
assert_eq!(loops(&it.func), 0);
assert_eq!(tally(&it.func, Opcode::Mul), 1, "one multiply, by the trip count");
assert_eq!(tally(&it.func, Opcode::Add), 0, "and nothing to add to it");
assert_eq!(handed(&it.func, &it.done), Some(1000), "n times a thousand");
sound(&it.func, &mut it.names);
}
#[test]
fn a_total_that_went_up_by_one_is_left_as_a_number() {
let mut it = shaped(Limit::Number(1000), What::HandsOne);
let stats = delete(&mut it.func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, DELETED), 1);
assert_eq!(stats.count(Kind::Optimized, WRITTEN), 1);
assert_eq!(tally(&it.func, Opcode::Mul), 0);
assert_eq!(tally(&it.func, Opcode::Add), 0);
let handed = handed_value(&it.func, &it.done).expect("the total is handed over");
let (imm, ty) = crate::fold::constant(&it.func, handed).expect("and it is a number");
assert_eq!(imm.signed(ty), 1000);
sound(&it.func, &mut it.names);
}
#[test]
fn a_total_that_went_up_by_a_different_amount_each_time_leaves_the_loop_alone() {
let mut it = shaped(Limit::Number(1000), What::HandsSquare);
let stats = delete(&mut it.func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, DELETED), 0);
assert_eq!(stats.count(Kind::Missed, NO_FORM), 1);
assert_eq!(loops(&it.func), 1);
sound(&it.func, &mut it.names);
}
fn doubling() -> Shape {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::PTR, Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let head = func.create_block();
let done = func.create_block();
func.append_param(entry, Type::PTR);
func.append_param(entry, Type::int(32));
let carried = func.append_param(head, Type::int(32));
let mut build = Builder::new(&mut func, entry);
let one = build.iconst(Type::int(32), 1);
build.jump(head, &[one]);
let mut build = Builder::new(&mut func, head);
let next = build.binary(Opcode::Add, carried, carried, Flags::NSW);
let stop = build.iconst(Type::int(32), 1000);
let test = build.icmp(IntPred::Slt, next, stop);
build.br_if(test, head, &[next], done, &[]);
Builder::new(&mut func, done).ret(&[]);
Shape { names, func, entry, done }
}
#[test]
fn a_loop_whose_count_is_not_known_is_left_alone() {
let mut it = doubling();
let stats = delete(&mut it.func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, DELETED), 0);
assert_eq!(stats.count(Kind::Missed, NO_COUNT), 1);
assert_eq!(loops(&it.func), 1);
sound(&it.func, &mut it.names);
}
#[test]
fn the_pass_stops_when_the_fuel_runs_out() {
let mut it = shaped(Limit::Number(1000), What::Nothing);
let stats = delete(&mut it.func, &mut Fuel::of(0));
assert_eq!(stats.count(Kind::Optimized, DELETED), 0);
assert_eq!(stats.count(Kind::Missed, NO_FUEL), 1);
assert_eq!(loops(&it.func), 1);
sound(&it.func, &mut it.names);
}
#[test]
fn a_function_with_no_body_is_not_a_problem() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let stats = delete(&mut func, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, DELETED), 0);
}
}