use alloc::vec;
use alloc::vec::Vec;
use ir_lang::{Block, BlockCall, BuildError, Builder, ConvOp, Function, InstData, Value};
use crate::Budget;
use crate::analysis::{
Cfg, DomTree, NONE, def_block, edge_mut, is_tracked, is_unwind_edge, tracked_values,
};
use crate::edit::{move_to_end, rewrite_terminator};
struct Loop {
header: u32,
blocks: Vec<u32>,
depth: u32,
}
struct Analysis {
cfg: Cfg,
loops: Vec<Loop>,
tracked: Option<Vec<bool>>,
}
fn analyze(func: &Function, budget: &mut Budget) -> Option<Analysis> {
let cfg = Cfg::new(func);
let dom = DomTree::new(&cfg);
let tracked = tracked_values(func, &cfg);
let mut latches: Vec<(u32, u32)> = Vec::new();
for &t in &cfg.rpo {
for &h in cfg.succs(t as usize) {
if dom.dominates(h, t) {
latches.push((h, t));
}
}
}
latches.sort_unstable();
latches.dedup();
let mut loops = Vec::new();
let mut stamp = vec![NONE; cfg.len()];
let mut stack: Vec<u32> = Vec::new();
let mut i = 0;
while i < latches.len() {
let h = latches[i].0;
let id = loops.len() as u32;
stamp[h as usize] = id;
let mut blocks = vec![h];
while i < latches.len() && latches[i].0 == h {
let t = latches[i].1;
i += 1;
if stamp[t as usize] != id {
stamp[t as usize] = id;
blocks.push(t);
stack.push(t);
}
}
while let Some(x) = stack.pop() {
let preds = cfg.preds(x as usize);
if !budget.charge(preds.len() as u64 + 1) {
return None;
}
for e in preds {
let p = e.from;
if cfg.reachable(p as usize) && stamp[p as usize] != id {
stamp[p as usize] = id;
blocks.push(p);
stack.push(p);
}
}
}
blocks.sort_unstable_by_key(|&b| cfg.rpo_index[b as usize]);
loops.push(Loop {
header: h,
blocks,
depth: dom.depth.get(h as usize).copied().unwrap_or(0),
});
}
loops.sort_by(|a, b| b.depth.cmp(&a.depth).then(a.header.cmp(&b.header)));
Some(Analysis {
cfg,
loops,
tracked,
})
}
fn hoistable(tracked: &Option<Vec<bool>>, data: &InstData, r: Value) -> bool {
if data.has_side_effects() || is_tracked(tracked, r) {
return false;
}
match data {
InstData::Const { .. }
| InstData::Unary { .. }
| InstData::Binary { .. }
| InstData::Fma { .. }
| InstData::Compare { .. }
| InstData::Select { .. }
| InstData::StackAddr { .. }
| InstData::GlobalAddr { .. }
| InstData::FuncAddr { .. }
| InstData::FieldAddr { .. }
| InstData::ElemAddr { .. }
| InstData::PtrOffset { .. } => true,
InstData::Convert { op, .. } => *op != ConvOp::RefToPtr,
_ => false,
}
}
fn candidates(
func: &Function,
a: &Analysis,
lp: &Loop,
stamp: &mut [u32],
id: u32,
hoisted: &mut Vec<bool>,
) -> Vec<ir_lang::Inst> {
for &b in &lp.blocks {
stamp[b as usize] = id;
}
if hoisted.len() < func.value_count() {
hoisted.resize(func.value_count(), false);
}
let mut out = Vec::new();
for &b in &lp.blocks {
for inst in func.insts(Block::from_u32(b)) {
let Some(data) = func.inst(inst) else {
continue;
};
let results = func.results(inst);
let Some(r) = results.first() else {
continue;
};
if results.len() != 1 || !hoistable(&a.tracked, data, r) {
continue;
}
let mut invariant = true;
data.for_each_operand(|v| {
let v = func.resolve(v);
let inside =
def_block(func, v).is_none_or(|db| stamp.get(db.index()).copied() == Some(id));
if inside && !hoisted.get(v.index()).copied().unwrap_or(false) {
invariant = false;
}
});
if invariant {
if let Some(h) = hoisted.get_mut(r.index()) {
*h = true;
}
out.push(inst);
}
}
}
for &inst in &out {
if let Some(r) = func.results(inst).first() {
if let Some(h) = hoisted.get_mut(r.index()) {
*h = false;
}
}
}
out
}
fn existing_preheader(
func: &Function,
a: &Analysis,
lp: &Loop,
stamp: &[u32],
id: u32,
) -> Option<Block> {
let mut found = None;
for e in a.cfg.preds(lp.header as usize) {
if !a.cfg.reachable(e.from as usize) || stamp[e.from as usize] == id {
continue;
}
if found.is_some() {
return None;
}
found = Some(e.from);
}
let p = Block::from_u32(found?);
matches!(func.terminator(p), Some(ir_lang::Terminator::Jump(_))).then_some(p)
}
fn make_preheader(
b: &mut Builder<'_>,
a: &Analysis,
lp: &Loop,
stamp: &[u32],
id: u32,
) -> Result<bool, BuildError> {
let func = b.func();
let header = Block::from_u32(lp.header);
let mut sources: Vec<(u32, u32)> = Vec::new();
for e in a.cfg.preds(lp.header as usize) {
if !a.cfg.reachable(e.from as usize) || stamp[e.from as usize] == id {
continue;
}
let Some(term) = func.terminator(Block::from_u32(e.from)) else {
continue;
};
if is_unwind_edge(term, e.idx as usize) {
return Ok(false);
}
sources.push((e.from, e.idx));
}
if sources.is_empty() {
return Ok(false);
}
let types: Vec<_> = func
.block_params(header)
.iter()
.filter_map(|&p| func.value_type(p))
.collect();
let ph = b.create_block(&types)?;
let args: Vec<Value> = b.func().block_params(ph).to_vec();
b.switch_to(ph)?;
b.jump(header, &args)?;
let mut i = 0;
while i < sources.len() {
let from = sources[i].0;
let mut idxs = Vec::new();
while i < sources.len() && sources[i].0 == from {
idxs.push(sources[i].1);
i += 1;
}
rewrite_terminator(b, Block::from_u32(from), |term| {
for k in idxs {
if let Some(dest) = edge_mut(term, k as usize) {
if dest.block == header {
*dest = BlockCall::with_args(ph, &dest.args);
}
}
}
})?;
}
Ok(true)
}
pub(crate) fn run(b: &mut Builder<'_>, budget: &mut Budget) -> Result<bool, BuildError> {
let cost = 3 * (b.func().inst_count() + b.func().block_count() + b.func().value_count()) as u64;
if !budget.charge(cost) {
return Ok(false);
}
let Some(first) = analyze(b.func(), budget) else {
return Ok(false);
};
if first.loops.is_empty() {
return Ok(false);
}
let n = first.cfg.len();
let mut stamp = vec![NONE; n];
let mut hoisted: Vec<bool> = Vec::new();
let mut created = false;
for (id, lp) in first.loops.iter().enumerate() {
let id = id as u32;
let func = b.func();
if !budget.charge(lp.blocks.len() as u64) {
return Ok(created);
}
if candidates(func, &first, lp, &mut stamp, id, &mut hoisted).is_empty() {
continue;
}
if existing_preheader(func, &first, lp, &stamp, id).is_none() {
created |= make_preheader(b, &first, lp, &stamp, id)?;
}
}
let a = if created {
match analyze(b.func(), budget) {
Some(a) => a,
None => return Ok(true),
}
} else {
first
};
let mut stamp = vec![NONE; a.cfg.len()];
let mut changed = created;
for (id, lp) in a.loops.iter().enumerate() {
let id = id as u32;
if !budget.charge(lp.blocks.len() as u64) {
break;
}
let func = b.func();
let cands = candidates(func, &a, lp, &mut stamp, id, &mut hoisted);
if cands.is_empty() {
continue;
}
let Some(ph) = existing_preheader(func, &a, lp, &stamp, id) else {
continue;
};
for inst in cands {
let _ = move_to_end(b, inst, ph)?;
}
changed = true;
}
Ok(changed)
}