use std::collections::{HashMap, HashSet};
use rucc_ir::{
Block, BlockCall, Def, Flags, Func, Inst, InstData, MemOrder, Module, Opcode, Type, Value,
};
use crate::alias::{Access, Alias, Answer, Options};
use crate::cfg::Cfg;
use crate::dom::Dominators;
pub const MAX_ALIAS_QUERIES_PER_ACCESS: u32 = 1000;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Clobber {
Exact(Inst),
Partial(Inst),
Maybe(Inst),
NoClobber,
Unknown,
}
impl Clobber {
#[must_use]
pub const fn inst(self) -> Option<Inst> {
match self {
Self::Exact(inst) | Self::Partial(inst) | Self::Maybe(inst) => Some(inst),
Self::NoClobber | Self::Unknown => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Step {
Stop,
Retry(Access),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Counts {
walks: u64,
steps: u64,
exhausted: u64,
}
impl Counts {
#[must_use]
pub const fn walks(&self) -> u64 {
self.walks
}
#[must_use]
pub const fn steps(&self) -> u64 {
self.steps
}
#[must_use]
pub const fn exhausted(&self) -> u64 {
self.exhausted
}
}
pub fn build(func: &mut Func) -> bool {
let Some(entry) = func.entry() else {
return false;
};
let cfg = Cfg::new(func);
let doms = Dominators::new(&cfg);
let mut defs = vec![entry];
let mut any = false;
for block in func.blocks() {
if !cfg.reaches(block) {
return false;
}
let mut writes = false;
for inst in func.insts(block) {
if func.carries_mem(inst) {
return false;
}
let opcode = func[inst].opcode;
any |= opcode.touches_memory();
writes |= opcode.writes_memory();
}
if writes && block != entry {
defs.push(block);
}
}
let Some(first) = func.insts(entry).next() else {
return false;
};
if !any {
return false;
}
let joins = iterated_frontier(&cfg, &doms, &defs);
let mut params = HashMap::new();
for block in func.blocks().collect::<Vec<_>>() {
if joins.contains(&block) {
params.insert(block, func.append_param(block, Type::MEM));
}
}
let start = start_of_chain(func, first);
let ends = thread(func, &doms, ¶ms, entry, start);
pass_it_on(func, ¶ms, &ends);
true
}
fn start_of_chain(func: &mut Func, first: Inst) -> Value {
let span = func.span(first);
let inst = func.create_inst(InstData::new(Opcode::MemEntry), &[Type::MEM], span);
func.insert_before(inst, first);
func[inst].results().next().expect("mem_entry produces one value")
}
fn thread(
func: &mut Func,
doms: &Dominators,
params: &HashMap<Block, Value>,
entry: Block,
start: Value,
) -> HashMap<Block, Value> {
let mut forward: Vec<(Value, Value)> = Vec::new();
let mut ends = HashMap::new();
let mut stack = vec![(entry, start)];
while let Some((block, incoming)) = stack.pop() {
let mut current = params.get(&block).copied().unwrap_or(incoming);
for inst in func.insts(block).collect::<Vec<_>>() {
if !func[inst].opcode.touches_memory() {
continue;
}
let fresh = func.with_mem(inst, current);
func.insert_before(fresh, inst);
for (old, new) in func[inst].results().zip(func[fresh].results()) {
forward.push((old, new));
}
func.remove_inst(inst);
if let Some(next) = func.mem_out(fresh) {
current = next;
}
}
ends.insert(block, current);
stack.extend(doms.children(block).map(|child| (child, current)));
}
let forward: HashMap<Value, Value> = forward.into_iter().collect();
if !forward.is_empty() {
substitute(func, &forward);
}
ends
}
fn substitute(func: &mut Func, forward: &HashMap<Value, Value>) {
let with = |value: Value| forward.get(&value).copied().unwrap_or(value);
for block in func.blocks().collect::<Vec<_>>() {
for inst in func.insts(block).collect::<Vec<_>>() {
let args = func[inst].args;
func.rewrite(args, with);
for call in func.successors(inst).collect::<Vec<_>>() {
func.rewrite(call.args, with);
}
}
}
}
fn pass_it_on(func: &mut Func, params: &HashMap<Block, Value>, ends: &HashMap<Block, Value>) {
for block in func.blocks().collect::<Vec<_>>() {
let Some(terminator) = func.terminator(block) else {
continue;
};
let Some(&value) = ends.get(&block) else {
continue;
};
for at in func.target_list(terminator).iter() {
let call = func[at];
if !params.contains_key(&call.block) {
continue;
}
let args = func.append_arg(call.args, value);
func.set_block_call(at, BlockCall { block: call.block, args });
}
}
}
fn iterated_frontier(cfg: &Cfg, doms: &Dominators, defs: &[Block]) -> HashSet<Block> {
let frontier = frontiers(cfg, doms);
let mut placed = HashSet::new();
let mut seen: HashSet<Block> = defs.iter().copied().collect();
let mut work: Vec<Block> = defs.to_vec();
while let Some(block) = work.pop() {
let Some(targets) = frontier.get(&block) else {
continue;
};
for &target in targets {
if placed.insert(target) && seen.insert(target) {
work.push(target);
}
}
}
placed
}
fn frontiers(cfg: &Cfg, doms: &Dominators) -> HashMap<Block, Vec<Block>> {
let mut frontier: HashMap<Block, Vec<Block>> = HashMap::new();
for block in cfg.reverse_postorder() {
let preds = cfg.predecessors(block);
if preds.len() < 2 {
continue;
}
let Some(top) = doms.immediate_dominator(block) else {
continue;
};
for &pred in preds {
let mut runner = pred;
while runner != top {
let at = frontier.entry(runner).or_default();
if !at.contains(&block) {
at.push(block);
}
let Some(next) = doms.immediate_dominator(runner) else {
break;
};
runner = next;
}
}
}
frontier
}
#[derive(Debug)]
pub struct Walk<'a> {
func: &'a Func,
cfg: Cfg,
alias: Alias<'a>,
limit: u32,
counts: Counts,
}
impl<'a> Walk<'a> {
#[must_use]
pub fn new(func: &'a Func, module: &'a Module) -> Self {
Self::with(func, module, Options::default(), MAX_ALIAS_QUERIES_PER_ACCESS)
}
#[must_use]
pub fn with(func: &'a Func, module: &'a Module, options: Options, limit: u32) -> Self {
Self {
func,
cfg: Cfg::new(func),
alias: Alias::with(func, module, options),
limit,
counts: Counts::default(),
}
}
#[must_use]
pub const fn counts(&self) -> &Counts {
&self.counts
}
#[must_use]
pub const fn alias(&self) -> &Alias<'a> {
&self.alias
}
pub fn clobber(&mut self, load: Inst) -> Clobber {
self.clobber_with(load, &mut |_, _| Step::Stop)
}
pub fn clobber_with(
&mut self,
load: Inst,
translate: &mut dyn FnMut(&Access, Inst) -> Step,
) -> Clobber {
let (Some(reference), Some(version)) = (self.alias.reads(load), self.func.mem_in(load))
else {
return Clobber::Unknown;
};
self.counts.walks += 1;
let mut budget = self.limit;
let mut seen = HashSet::new();
let answer = self.back(reference, version, &mut budget, &mut seen, translate);
answer.unwrap_or(Clobber::NoClobber)
}
fn back(
&mut self,
reference: Access,
version: Value,
budget: &mut u32,
seen: &mut HashSet<Value>,
translate: &mut dyn FnMut(&Access, Inst) -> Step,
) -> Option<Clobber> {
if !seen.insert(version) {
return None;
}
match self.func[version].def {
Def::Param { block, index } => {
let mut answer = None;
for pred in self.cfg.predecessors(block).to_vec() {
let Some(terminator) = self.func.terminator(pred) else {
continue;
};
for call in self.func.successors(terminator).collect::<Vec<_>>() {
if call.block != block {
continue;
}
let Some(&incoming) = self.func[call.args].get(index as usize) else {
continue;
};
let one = self.back(reference, incoming, budget, seen, translate);
answer = combine(answer, one);
if answer == Some(Clobber::Unknown) {
return answer;
}
}
}
answer
}
Def::Result { inst, .. } => {
if self.func[inst].opcode == Opcode::MemEntry {
return Some(Clobber::NoClobber);
}
if *budget == 0 {
self.counts.exhausted += 1;
return Some(Clobber::Unknown);
}
*budget -= 1;
self.counts.steps += 1;
let past = match self.wrote(&reference, inst) {
None => reference,
Some(answer) => match translate(&reference, inst) {
Step::Stop => return Some(answer),
Step::Retry(next) => {
seen.clear();
next
}
},
};
let next = self.func.mem_in(inst)?;
self.back(past, next, budget, seen, translate)
}
}
}
fn wrote(&mut self, reference: &Access, inst: Inst) -> Option<Clobber> {
if reference.volatile || self.func[inst].flags.contains(Flags::VOLATILE) {
return Some(Clobber::Maybe(inst));
}
if self.ordered(inst) {
return Some(Clobber::Maybe(inst));
}
if let Some(write) = self.alias.writes(inst) {
return match self.alias.query(reference, &write) {
Answer::No(_) => None,
Answer::May => Some(self.extent(reference, &write, inst)),
};
}
match self.alias.clobbered_by(reference, inst) {
Answer::No(_) => None,
Answer::May => Some(Clobber::Maybe(inst)),
}
}
fn extent(&self, reference: &Access, write: &Access, inst: Inst) -> Clobber {
if reference.origin != write.origin {
return Clobber::Maybe(inst);
}
let (Some(want), Some(wrote)) = (reference.range(), write.range()) else {
return Clobber::Maybe(inst);
};
if want == wrote {
Clobber::Exact(inst)
} else if wrote.0 < want.1 && want.0 < wrote.1 {
Clobber::Partial(inst)
} else {
Clobber::Maybe(inst)
}
}
fn ordered(&self, inst: Inst) -> bool {
use rucc_ir::Extra;
let order = match self.func[inst].extra {
Extra::Mem(at) => self.func[at].order,
Extra::Rmw(_, at) => self.func[at].order,
Extra::Order(order) => order,
_ => return false,
};
order != MemOrder::NotAtomic
}
}
fn combine(a: Option<Clobber>, b: Option<Clobber>) -> Option<Clobber> {
match (a, b) {
(None, other) | (other, None) => other,
(Some(one), Some(other)) if one == other => Some(one),
_ => Some(Clobber::Unknown),
}
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{Builder, MemInfo, Restrict, Signature, parse, verify_func};
use super::*;
fn read(text: &str) -> (Module, Interner) {
let mut names = Interner::new();
let module = parse(text, &mut names).expect("the text parses");
(module, names)
}
const HEADER: &str = "\
; ModuleID = 'mem.c'
; format 0
target triple = \"x86_64-unknown-linux-gnu\"
target datalayout = \"e-p:64:64-i64:64-f80:128-S128\"
";
fn wrap(signature: &str, body: &str) -> String {
format!("{HEADER}\nfunc @f{signature}, linkage(external) {{\n{body}}}\n")
}
fn built(text: &str) -> (Module, bool) {
let (mut module, names) = read(text);
let id = module.funcs().next().expect("one function");
let changed = build(&mut module[id]);
if let Err(errors) = verify_func(&module, &module[id], &names) {
panic!("{errors:#?}");
}
(module, changed)
}
fn one(module: &Module) -> &Func {
&module[module.funcs().next().expect("one function")]
}
fn nth(func: &Func, opcode: Opcode, want: usize) -> Inst {
func.blocks()
.flat_map(|block| func.insts(block).collect::<Vec<_>>())
.filter(|&inst| func[inst].opcode == opcode)
.nth(want)
.expect("that many of them")
}
#[test]
fn a_function_with_no_memory_in_it_gets_no_chain() {
let text = wrap(
"(i32) -> i32",
"block0(%0: i32):
%1 = add %0, %0
return %1
",
);
let (module, changed) = built(&text);
assert!(!changed);
assert_eq!(one(&module).blocks().count(), 1);
}
#[test]
fn a_straight_line_is_threaded_in_order() {
let text = wrap(
"(ptr) -> i32",
"block0(%0: ptr):
%1 = iconst.i32 7
store %1 -> %0, align 4
%2 = load.i32 %0, align 4
return %2
",
);
let (module, changed) = built(&text);
assert!(changed);
let func = one(&module);
let start = nth(func, Opcode::MemEntry, 0);
let store = nth(func, Opcode::Store, 0);
let load = nth(func, Opcode::Load, 0);
assert_eq!(func.mem_in(store), func.mem_out(start));
assert_eq!(func.mem_in(load), func.mem_out(store));
assert_eq!(func.mem_out(load), None);
}
#[test]
fn a_join_gets_a_memory_parameter_and_every_branch_passes_one() {
let text = wrap(
"(ptr, i1) -> i32",
"block0(%0: ptr, %1: i1):
br_if %1, block1, block2
block1:
%2 = iconst.i32 7
store %2 -> %0, align 4
jump block3
block2:
jump block3
block3:
%3 = load.i32 %0, align 4
return %3
",
);
let (module, _) = built(&text);
let func = one(&module);
let join = func.blocks().nth(3).expect("four blocks");
assert_eq!(func[join].params.len(), 1);
let param = func[join].params[0];
assert!(func[param].ty.is_mem());
assert_eq!(func.mem_in(nth(func, Opcode::Load, 0)), Some(param));
}
#[test]
fn a_block_that_only_reads_needs_no_parameter() {
let text = wrap(
"(ptr, i1) -> i32",
"block0(%0: ptr, %1: i1):
br_if %1, block1, block2
block1:
%2 = load.i32 %0, align 4
jump block3
block2:
jump block3
block3:
%3 = load.i32 %0, align 4
return %3
",
);
let (module, _) = built(&text);
let func = one(&module);
for block in func.blocks() {
assert!(func[block].params.iter().all(|¶m| !func[param].ty.is_mem()));
}
}
#[test]
fn every_arm_of_a_switch_passes_its_own_version_along() {
let text = wrap(
"(ptr, i32) -> i32",
"block0(%0: ptr, %1: i32):
switch %1, block1, [0 => block2, 1 => block3]
block1:
%2 = iconst.i32 1
store %2 -> %0, align 4
jump block4
block2:
%3 = iconst.i32 2
store %3 -> %0, align 4
jump block4
block3:
jump block4
block4:
%4 = load.i32 %0, align 4
return %4
",
);
let (module, _) = built(&text);
let func = one(&module);
let join = func.blocks().nth(4).expect("five blocks");
let param = *func[join].params.last().expect("a parameter");
assert!(func[param].ty.is_mem());
for (arm, want) in [(1, Some(0)), (2, Some(1)), (3, None)] {
let block = func.blocks().nth(arm).expect("that block");
let jump = func.terminator(block).expect("a terminator");
let call = func.successors(jump).next().expect("one target");
let sent = *func[call.args].last().expect("an argument");
let expect = match want {
Some(store) => func.mem_out(nth(func, Opcode::Store, store)),
None => func.mem_out(nth(func, Opcode::MemEntry, 0)),
};
assert_eq!(Some(sent), expect, "arm {arm} passed the wrong version");
}
}
#[test]
fn a_function_with_a_block_nothing_reaches_is_left_alone() {
let text = wrap(
"(ptr) -> i32",
"block0(%0: ptr):
%1 = iconst.i32 7
store %1 -> %0, align 4
jump block2
block1:
%2 = iconst.i32 9
store %2 -> %0, align 4
jump block2
block2:
%3 = load.i32 %0, align 4
return %3
",
);
let (mut module, _) = read(&text);
let id = module.funcs().next().expect("one function");
assert!(!build(&mut module[id]));
assert_eq!(module[id].blocks().filter(|&b| !module[id][b].params.is_empty()).count(), 1);
}
fn last_load(func: &Func) -> Inst {
func.blocks()
.flat_map(|block| func.insts(block).collect::<Vec<_>>())
.filter(|&inst| func[inst].opcode == Opcode::Load)
.last()
.expect("a load")
}
fn walked(text: &str) -> (Clobber, Counts) {
let (module, changed) = built(text);
assert!(changed, "the function has memory in it");
let func = one(&module);
let mut walk = Walk::new(func, &module);
let answer = walk.clobber(last_load(func));
(answer, *walk.counts())
}
#[test]
fn a_load_sees_the_store_before_it() {
let text = wrap(
"(ptr) -> i32",
"block0(%0: ptr):
%1 = iconst.i32 7
store %1 -> %0, align 4
%2 = load.i32 %0, align 4
return %2
",
);
let (answer, counts) = walked(&text);
assert!(matches!(answer, Clobber::Exact(_)));
assert_eq!(counts.walks(), 1);
assert_eq!(counts.steps(), 1);
assert_eq!(counts.exhausted(), 0);
}
#[test]
fn a_load_walks_past_a_store_to_another_object() {
let text = wrap(
"() -> i32",
"block0:
%0 = alloca, size 8, align 8
%1 = alloca, size 8, align 8
%2 = iconst.i32 7
store %2 -> %0, align 4
%3 = load.i32 %1, align 4
return %3
",
);
let (answer, counts) = walked(&text);
assert_eq!(answer, Clobber::NoClobber);
assert_eq!(counts.steps(), 1);
}
#[test]
fn a_load_of_one_byte_of_a_wider_store_is_partial() {
let text = wrap(
"() -> i8",
"block0:
%0 = alloca, size 8, align 8
%1 = iconst.i32 7
store %1 -> %0, align 4
%2 = iconst.i64 1
%3 = ptr_add %0, %2
%4 = load.i8 %3, align 1
return %4
",
);
let (answer, _) = walked(&text);
assert!(matches!(answer, Clobber::Partial(_)), "{answer:?}");
}
#[test]
fn a_load_after_a_call_that_cannot_reach_it_walks_past_the_call() {
let text = wrap(
"() -> i32",
"block0:
%0 = alloca, size 8, align 8
%1 = iconst.i32 7
store %1 -> %0, align 4
call @g() : ()
%2 = load.i32 %0, align 4
return %2
",
);
let (answer, _) = walked(&text);
assert!(matches!(answer, Clobber::Exact(_)), "{answer:?}");
}
#[test]
fn a_load_after_a_call_that_could_have_the_address_sees_the_call() {
let text = wrap(
"(ptr) -> i32",
"block0(%0: ptr):
%1 = iconst.i32 7
store %1 -> %0, align 4
call @g() : ()
%2 = load.i32 %0, align 4
return %2
",
);
let (answer, _) = walked(&text);
assert!(matches!(answer, Clobber::Maybe(_)), "{answer:?}");
}
#[test]
fn a_load_after_an_atomic_store_sees_it_whatever_it_wrote() {
let text = wrap(
"() -> i32",
"block0:
%0 = alloca, size 8, align 8
%1 = alloca, size 8, align 8
%2 = iconst.i32 7
atomic_store %2 -> %0, align 4, release
%3 = load.i32 %1, align 4
return %3
",
);
let (answer, _) = walked(&text);
assert!(matches!(answer, Clobber::Maybe(_)), "{answer:?}");
}
#[test]
fn a_load_after_a_volatile_store_sees_it_whatever_it_wrote() {
let text = wrap(
"() -> i32",
"block0:
%0 = alloca, size 8, align 8
%1 = alloca, size 8, align 8
%2 = iconst.i32 7
store.volatile %2 -> %0, align 4
%3 = load.i32 %1, align 4
return %3
",
);
let (answer, _) = walked(&text);
assert!(matches!(answer, Clobber::Maybe(_)), "{answer:?}");
}
#[test]
fn paths_that_disagree_are_unknown_rather_than_the_weaker_of_the_two() {
let text = wrap(
"(i1) -> i32",
"block0(%0: i1):
%1 = alloca, size 8, align 8
br_if %0, block1, block2
block1:
%2 = iconst.i32 7
store %2 -> %1, align 4
jump block3
block2:
jump block3
block3:
%3 = load.i32 %1, align 4
return %3
",
);
let (answer, _) = walked(&text);
assert_eq!(answer, Clobber::Unknown);
}
#[test]
fn a_loop_that_writes_nothing_relevant_walks_out_of_it() {
let text = wrap(
"(i32) -> i32",
"block0(%0: i32):
%1 = alloca, size 8, align 8
%2 = alloca, size 8, align 8
%3 = iconst.i32 7
store %3 -> %1, align 4
jump block1(%0)
block1(%4: i32):
%5 = iconst.i32 1
%6 = sub %4, %5
store %5 -> %2, align 4
%7 = icmp sgt %6, %5
br_if %7, block1(%6), block2
block2:
%8 = load.i32 %1, align 4
return %8
",
);
let (answer, counts) = walked(&text);
assert!(matches!(answer, Clobber::Exact(_)), "{answer:?}");
assert_eq!(counts.exhausted(), 0);
}
#[test]
fn a_budget_of_nothing_gives_unknown_and_says_so() {
let text = wrap(
"(ptr) -> i32",
"block0(%0: ptr):
%1 = iconst.i32 7
store %1 -> %0, align 4
%2 = load.i32 %0, align 4
return %2
",
);
let (module, _) = built(&text);
let func = one(&module);
let load = nth(func, Opcode::Load, 0);
let mut walk = Walk::with(func, &module, Options::default(), 0);
assert_eq!(walk.clobber(load), Clobber::Unknown);
assert_eq!(walk.counts().exhausted(), 1);
}
#[test]
fn translate_carries_the_walk_past_a_def_it_would_have_stopped_at() {
let text = wrap(
"(ptr) -> i32",
"block0(%0: ptr):
%1 = iconst.i32 7
store %1 -> %0, align 4
memcpy %0, %0, size 4, align 4
%2 = load.i32 %0, align 4
return %2
",
);
let (module, _) = built(&text);
let func = one(&module);
let load = nth(func, Opcode::Load, 0);
let mut walk = Walk::new(func, &module);
let stopped_at = walk.clobber(load).inst().expect("something wrote it");
assert_eq!(func[stopped_at].opcode, Opcode::Memcpy);
let mut walk = Walk::new(func, &module);
let mut seen = Vec::new();
let answer = walk.clobber_with(load, &mut |reference, inst| {
seen.push(func[inst].opcode);
if func[inst].opcode == Opcode::Memcpy { Step::Retry(*reference) } else { Step::Stop }
});
assert_eq!(seen, [Opcode::Memcpy, Opcode::Store]);
assert_eq!(answer.inst().map(|inst| func[inst].opcode), Some(Opcode::Store));
}
#[test]
fn building_twice_changes_nothing_the_second_time() {
let text = wrap(
"(ptr) -> i32",
"block0(%0: ptr):
%1 = load.i32 %0, align 4
return %1
",
);
let (mut module, _) = read(&text);
let id = module.funcs().next().expect("one function");
let func = &mut module[id];
assert!(build(func));
let before = func.counts().insts;
assert!(!build(func));
assert_eq!(func.counts().insts, before);
}
#[test]
fn a_function_built_by_hand_threads_the_same_way() {
let mut names = Interner::new();
let i32_ = Type::int(32);
let mut func = Func::new(
names.intern("f"),
Signature::new().with_params(&[Type::PTR]).with_returns(&[i32_]),
);
let entry = func.create_block();
let addr = func.append_param(entry, Type::PTR);
let info = MemInfo {
size: 4,
align: 4,
order: MemOrder::NotAtomic,
tbaa: None,
restrict: Restrict::NONE,
};
let mut b = Builder::new(&mut func, entry);
let seven = b.iconst(i32_, 7);
b.store(seven, addr, info, Flags::NONE);
let read = b.load(i32_, addr, info, Flags::NONE);
b.ret(&[read]);
assert!(build(&mut func));
let store = nth(&func, Opcode::Store, 0);
let load = nth(&func, Opcode::Load, 0);
assert_eq!(func.mem_in(load), func.mem_out(store));
}
}