use std::collections::HashMap;
use rucc_ir::{Block, Def, Extra, Func, FuncId, Imm, Inst, InstData, Module, Opcode, Type, Value};
use crate::ipa::{self, Sites};
use crate::uses::substitute;
use crate::{CallGraph, Fuel, Stats, fold};
const KNOWN: &str =
"parameter is the same constant at every call and is now a constant in the body";
const NO_FUEL: &str = "parameter left alone, the pass ran out of fuel";
pub const NAME: &str = "ipa-cp";
const SWEEPS: usize = 3;
pub fn propagate(module: &mut Module, graph: &CallGraph, fuel: &mut Fuel) -> Vec<(FuncId, Stats)> {
let closed = ipa::closed(module, graph);
if closed.is_empty() {
return Vec::new();
}
let order = ipa::order(graph, &closed);
let mut stats: HashMap<FuncId, Stats> = HashMap::new();
let mut touched: Vec<FuncId> = Vec::new();
for _ in 0..SWEEPS {
let sites = ipa::sites(module, &closed);
let known = settle(module, &closed, &order, &sites);
let changed = rewrite(module, &closed, &sites, &known, fuel, &mut stats);
if changed.is_empty() {
break;
}
for &id in &changed {
let folded = fold::fold_in(&mut module[id], &mut Fuel::unlimited());
stats.entry(id).or_default().merge(&folded);
}
for id in changed {
if !touched.contains(&id) {
touched.push(id);
}
}
}
module.funcs().filter_map(|id| Some((id, stats.remove(&id)?))).collect()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Held {
Nothing,
Number(Imm, Type),
Anything,
}
impl Held {
fn and(self, other: Self) -> Self {
match (self, other) {
(Self::Nothing, it) | (it, Self::Nothing) => it,
(Self::Number(a, x), Self::Number(b, y)) if a == b && x == y => Self::Number(a, x),
_ => Self::Anything,
}
}
}
fn settle(
module: &Module,
closed: &[FuncId],
order: &[Vec<FuncId>],
sites: &HashMap<FuncId, Sites>,
) -> HashMap<FuncId, Vec<Held>> {
let mut known: HashMap<FuncId, Vec<Held>> = closed
.iter()
.map(|&id| (id, vec![Held::Nothing; module[id].signature().params.len()]))
.collect();
for part in order {
let ceiling = 1000 + part.len() * 64;
let mut rounds = 0usize;
loop {
let mut settled = true;
for &id in part {
let now = row(module, sites, &known, id);
if known.get(&id) != Some(&now) {
known.insert(id, now);
settled = false;
}
}
if settled {
break;
}
rounds += 1;
debug_assert!(rounds < ceiling, "the propagation is not monotone");
}
}
known
}
fn row(
module: &Module,
sites: &HashMap<FuncId, Sites>,
known: &HashMap<FuncId, Vec<Held>>,
id: FuncId,
) -> Vec<Held> {
let count = module[id].signature().params.len();
let Some(sites) = sites.get(&id) else { return vec![Held::Nothing; count] };
if sites.ragged {
return vec![Held::Anything; count];
}
let entry = module[id].entry().expect("a closed function has an entry block");
(0..count)
.map(|index| {
let param = module[id][entry].params[index];
let ty = module[id][param].ty;
sites.calls.iter().fold(Held::Nothing, |so_far, &(caller, inst)| {
so_far.and(passed(module, known, caller, inst, index, ty))
})
})
.collect()
}
fn passed(
module: &Module,
known: &HashMap<FuncId, Vec<Held>>,
caller: FuncId,
inst: Inst,
index: usize,
ty: Type,
) -> Held {
let func = &module[caller];
let Some(&arg) = func[func[inst].args].get(index) else { return Held::Anything };
if func[arg].ty != ty {
return Held::Anything;
}
if let Some((imm, ty)) = number(func, arg) {
return Held::Number(imm, ty);
}
let Def::Param { block, index: at } = func[arg].def else { return Held::Anything };
if func.entry() != Some(block) {
return Held::Anything;
}
match known.get(&caller).and_then(|row| row.get(at as usize)) {
Some(&held) => held,
None => Held::Anything,
}
}
fn number(func: &Func, value: Value) -> Option<(Imm, Type)> {
let Def::Result { inst, .. } = func[value].def else { return None };
let data = &func[inst];
if !matches!(data.opcode, Opcode::IConst | Opcode::FConst) {
return None;
}
let Extra::Imm(at) = data.extra else { return None };
let ty = func[value].ty;
(ty.is_scalar() && (ty.is_int() || ty.is_float())).then(|| (func[at], ty))
}
fn rewrite(
module: &mut Module,
closed: &[FuncId],
sites: &HashMap<FuncId, Sites>,
known: &HashMap<FuncId, Vec<Held>>,
fuel: &mut Fuel,
stats: &mut HashMap<FuncId, Stats>,
) -> Vec<FuncId> {
let mut changed = Vec::new();
for &id in closed {
if sites.get(&id).is_none_or(|sites| sites.calls.is_empty()) {
continue;
}
let Some(row) = known.get(&id) else { continue };
let func = &mut module[id];
let entry = func.entry().expect("a closed function has an entry block");
let read = ipa::operands(func);
let mut same: HashMap<Value, Value> = HashMap::new();
for (index, &held) in row.iter().enumerate() {
let Held::Number(imm, ty) = held else { continue };
let param = func[entry].params[index];
if !read.contains(¶m) || func[param].ty != ty {
continue;
}
if !fuel.take() {
stats.entry(id).or_default().missed(NO_FUEL);
continue;
}
same.insert(param, constant(func, entry, imm, ty));
stats.entry(id).or_default().optimized(KNOWN);
}
if !same.is_empty() {
substitute(func, &same);
changed.push(id);
}
}
changed
}
fn constant(func: &mut Func, entry: Block, imm: Imm, ty: Type) -> Value {
let first = func.insts(entry).next().expect("a block ends in a terminator");
let span = func.span(first);
let at = func.add_imm(imm);
let opcode = if ty.is_int() { Opcode::IConst } else { Opcode::FConst };
let inst =
func.create_inst(InstData { extra: Extra::Imm(at), ..InstData::new(opcode) }, &[ty], span);
func.insert_before(inst, first);
func[inst].results().next().expect("one result was asked for")
}
#[cfg(test)]
mod tests {
use rucc_base::{Interner, Symbol};
use rucc_ir::{Builder, Flags, Float, Linkage, Pic, Signature};
use rucc_target::{TargetInfo, Triple};
use super::*;
use crate::stats::Kind;
const INT: Type = Type::int(32);
struct Unit {
names: Interner,
module: Module,
}
impl Unit {
fn new() -> Self {
let mut names = Interner::new();
let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
let module = Module::new(names.intern("t.c"), &target);
Self { names, module }
}
fn name(&mut self, name: &str) -> Symbol {
self.names.intern(name)
}
fn add(
&mut self,
name: Symbol,
linkage: Linkage,
signature: Signature,
body: impl FnOnce(&mut Builder<'_>, &[Value]),
) {
let params = signature.params.iter().map(|it| it.ty).collect::<Vec<Type>>();
let mut func = Func::new(name, signature);
func.linkage = linkage;
let block = func.create_block();
let values: Vec<Value> =
params.into_iter().map(|ty| func.append_param(block, ty)).collect();
let mut build = Builder::new(&mut func, block);
body(&mut build, &values);
build.ret(&[]);
self.module.add_func(func);
}
fn private(
&mut self,
name: &str,
params: &[Type],
body: impl FnOnce(&mut Builder<'_>, &[Value]),
) -> Symbol {
let name = self.name(name);
self.add(name, Linkage::Internal, Signature::new().with_params(params), body);
name
}
fn outside(&mut self, body: impl FnOnce(&mut Builder<'_>, &[Value])) {
let name = self.name("f");
self.add(name, Linkage::External, Signature::new(), body);
}
fn propagate(&mut self) -> Vec<(FuncId, Stats)> {
self.run(&mut Fuel::unlimited())
}
fn run(&mut self, fuel: &mut Fuel) -> Vec<(FuncId, Stats)> {
let graph = CallGraph::of(&self.module, Pic::Executable);
propagate(&mut self.module, &graph, fuel)
}
fn reads(&self, at: Symbol, opcode: Opcode) -> Option<i128> {
let id = self.module.funcs().find(|&id| self.module[id].name == at);
let func = &self.module[id.expect("the module has a function of that name")];
let entry = func.entry()?;
let at = func.insts(entry).find(|&inst| func[inst].opcode == opcode)?;
let arg = *func[func[at].args].first()?;
let (imm, ty) = number(func, arg)?;
Some(if ty.is_int() { imm.signed(ty) } else { imm.bits() as i128 })
}
fn said(&self, done: &[(FuncId, Stats)], at: Symbol) -> Stats {
done.iter()
.find(|(id, _)| self.module[*id].name == at)
.map_or_else(Stats::new, |(_, stats)| stats.clone())
}
}
fn reads(build: &mut Builder<'_>, value: Value) {
build.binary(Opcode::SDiv, value, value, Flags::NONE);
}
fn call(build: &mut Builder<'_>, at: Symbol, params: &[Type], args: &[Value]) {
let signature = build.func().add_signature(Signature::new().with_params(params));
build.call(at, signature, args);
}
#[test]
fn a_parameter_every_call_passes_the_same_number_to_becomes_that_number() {
let mut unit = Unit::new();
let g = unit.private("g", &[INT], |build, params| reads(build, params[0]));
unit.outside(|build, _| {
let seven = build.iconst(INT, 7);
call(build, g, &[INT], &[seven]);
call(build, g, &[INT], &[seven]);
});
let done = unit.propagate();
assert_eq!(unit.said(&done, g).count(Kind::Optimized, KNOWN), 1);
assert_eq!(unit.reads(g, Opcode::SDiv), Some(7));
}
#[test]
fn a_parameter_two_calls_disagree_about_is_left_alone() {
let mut unit = Unit::new();
let g = unit.private("g", &[INT], |build, params| reads(build, params[0]));
unit.outside(|build, _| {
let seven = build.iconst(INT, 7);
let eight = build.iconst(INT, 8);
call(build, g, &[INT], &[seven]);
call(build, g, &[INT], &[eight]);
});
assert!(unit.propagate().is_empty());
assert_eq!(unit.reads(g, Opcode::SDiv), None);
}
#[test]
fn a_parameter_handed_on_by_a_caller_whose_own_is_known_becomes_the_same_number() {
let mut unit = Unit::new();
let g = unit.private("g", &[INT], |build, params| reads(build, params[0]));
let wrap = unit.private("wrap", &[INT], |build, params| {
call(build, g, &[INT], &[params[0]]);
});
unit.outside(|build, _| {
let seven = build.iconst(INT, 7);
call(build, wrap, &[INT], &[seven]);
});
let done = unit.propagate();
assert_eq!(unit.said(&done, g).count(Kind::Optimized, KNOWN), 1);
assert_eq!(unit.said(&done, wrap).count(Kind::Optimized, KNOWN), 1);
assert_eq!(unit.reads(g, Opcode::SDiv), Some(7));
}
#[test]
fn a_number_the_caller_worked_out_arrives_on_the_sweep_after_the_one_that_made_it() {
let mut unit = Unit::new();
let g = unit.private("g", &[INT], |build, params| reads(build, params[0]));
let wrap = unit.private("wrap", &[INT], |build, params| {
let one = build.iconst(INT, 1);
let sum = build.binary(Opcode::Add, params[0], one, Flags::NONE);
call(build, g, &[INT], &[sum]);
});
unit.outside(|build, _| {
let seven = build.iconst(INT, 7);
call(build, wrap, &[INT], &[seven]);
});
let done = unit.propagate();
assert_eq!(unit.said(&done, wrap).count(Kind::Optimized, KNOWN), 1);
assert_eq!(unit.said(&done, g).count(Kind::Optimized, KNOWN), 1);
assert_eq!(unit.reads(g, Opcode::SDiv), Some(8));
}
#[test]
fn a_recursive_call_handing_on_the_parameter_it_was_given_says_nothing_either_way() {
let mut unit = Unit::new();
let g = unit.name("g");
unit.add(g, Linkage::Internal, Signature::new().with_params(&[INT]), |build, params| {
reads(build, params[0]);
call(build, g, &[INT], &[params[0]]);
});
unit.outside(|build, _| {
let seven = build.iconst(INT, 7);
call(build, g, &[INT], &[seven]);
});
let done = unit.propagate();
assert_eq!(unit.said(&done, g).count(Kind::Optimized, KNOWN), 1);
assert_eq!(unit.reads(g, Opcode::SDiv), Some(7));
}
#[test]
fn a_function_another_object_can_call_is_left_alone() {
let mut unit = Unit::new();
let g = unit.name("g");
unit.add(g, Linkage::External, Signature::new().with_params(&[INT]), |build, params| {
reads(build, params[0]);
});
unit.outside(|build, _| {
let seven = build.iconst(INT, 7);
call(build, g, &[INT], &[seven]);
});
assert!(unit.propagate().is_empty());
assert_eq!(unit.reads(g, Opcode::SDiv), None);
}
#[test]
fn a_function_whose_address_this_unit_hands_out_is_left_alone() {
let mut unit = Unit::new();
let g = unit.private("g", &[INT], |build, params| reads(build, params[0]));
unit.outside(|build, _| {
let seven = build.iconst(INT, 7);
call(build, g, &[INT], &[seven]);
let extra = Extra::Symbol(g);
build.value(InstData { extra, ..InstData::new(Opcode::GlobalAddr) }, Type::PTR);
});
assert!(unit.propagate().is_empty());
assert_eq!(unit.reads(g, Opcode::SDiv), None);
}
#[test]
fn a_function_nothing_in_the_unit_calls_is_left_alone() {
let mut unit = Unit::new();
let g = unit.private("g", &[INT], |build, params| reads(build, params[0]));
assert!(unit.propagate().is_empty());
assert_eq!(unit.reads(g, Opcode::SDiv), None);
}
#[test]
fn a_variadic_function_is_left_alone() {
let mut unit = Unit::new();
let g = unit.name("g");
let mut signature = Signature::new().with_params(&[INT]);
signature.variadic = true;
unit.add(g, Linkage::Internal, signature, |build, params| reads(build, params[0]));
unit.outside(|build, _| {
let seven = build.iconst(INT, 7);
call(build, g, &[INT], &[seven]);
});
assert!(unit.propagate().is_empty());
assert_eq!(unit.reads(g, Opcode::SDiv), None);
}
#[test]
fn a_call_passing_the_wrong_number_of_arguments_stops_the_parameter() {
let mut unit = Unit::new();
let g = unit.private("g", &[INT], |build, params| reads(build, params[0]));
unit.outside(|build, _| {
let seven = build.iconst(INT, 7);
call(build, g, &[INT], &[seven]);
call(build, g, &[INT, INT], &[seven, seven]);
});
assert!(unit.propagate().is_empty());
assert_eq!(unit.reads(g, Opcode::SDiv), None);
}
#[test]
fn a_floating_point_parameter_becomes_a_constant_too() {
let mut unit = Unit::new();
let half: u64 = 0x3fe0_0000_0000_0000;
let g = unit.private("g", &[Type::float(Float::F64)], |build, params| {
build.binary(Opcode::FAdd, params[0], params[0], Flags::NONE);
});
unit.outside(|build, _| {
let it = build.fconst(Type::float(Float::F64), u128::from(half));
call(build, g, &[Type::float(Float::F64)], &[it]);
call(build, g, &[Type::float(Float::F64)], &[it]);
});
let done = unit.propagate();
assert_eq!(unit.said(&done, g).count(Kind::Optimized, KNOWN), 1);
assert_eq!(unit.reads(g, Opcode::FAdd), Some(i128::from(half)));
}
#[test]
fn a_parameter_nothing_in_the_body_reads_is_not_given_a_constant() {
let mut unit = Unit::new();
let g = unit.private("g", &[INT], |_, _| ());
unit.outside(|build, _| {
let seven = build.iconst(INT, 7);
call(build, g, &[INT], &[seven]);
});
assert!(unit.propagate().is_empty());
}
#[test]
fn without_fuel_the_parameter_stays_and_the_chance_is_still_counted() {
let mut unit = Unit::new();
let g = unit.private("g", &[INT], |build, params| reads(build, params[0]));
unit.outside(|build, _| {
let seven = build.iconst(INT, 7);
call(build, g, &[INT], &[seven]);
});
let done = unit.run(&mut Fuel::of(0));
assert_eq!(unit.said(&done, g).count(Kind::Missed, NO_FUEL), 1);
assert_eq!(unit.said(&done, g).count(Kind::Optimized, KNOWN), 0);
assert_eq!(unit.reads(g, Opcode::SDiv), None);
}
}