use std::collections::HashMap;
use rucc_base::Symbol;
use rucc_ir::{AttrSet, Block, Def, Func, Inst, Module, Opcode, Value};
use crate::alias::{Escapes, Origin, keeps_address, origin};
use crate::callgraph::{CallGraph, Node};
use crate::purity::Callee;
const MAX_STEPS: usize = 25_000;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum Effect {
#[default]
Nothing,
Reads,
Writes,
}
impl Effect {
#[must_use]
pub fn and_then(self, other: Self) -> Self {
self.max(other)
}
#[must_use]
pub fn as_well_as(self, other: Self) -> Self {
self.min(other)
}
#[must_use]
pub fn reads(self) -> bool {
self != Self::Nothing
}
#[must_use]
pub fn writes(self) -> bool {
self == Self::Writes
}
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::Nothing => "nothing",
Self::Reads => "reads",
Self::Writes => "writes",
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Touch {
pub effect: Effect,
pub escapes: bool,
}
impl Touch {
#[must_use]
pub fn nothing() -> Self {
Self::default()
}
#[must_use]
pub fn everything() -> Self {
Self { effect: Effect::Writes, escapes: true }
}
#[must_use]
pub fn and_then(self, other: Self) -> Self {
Self { effect: self.effect.and_then(other.effect), escapes: self.escapes || other.escapes }
}
#[must_use]
pub fn as_well_as(self, other: Self) -> Self {
Self {
effect: self.effect.as_well_as(other.effect),
escapes: self.escapes && other.escapes,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Summary {
outside: Effect,
params: Box<[Touch]>,
}
impl Summary {
#[must_use]
pub fn nothing(arity: usize) -> Self {
Self { outside: Effect::Nothing, params: vec![Touch::nothing(); arity].into() }
}
#[must_use]
pub fn everything(arity: usize) -> Self {
Self { outside: Effect::Writes, params: vec![Touch::everything(); arity].into() }
}
#[must_use]
pub fn doing(outside: Effect, params: &[Touch]) -> Self {
Self { outside, params: params.into() }
}
#[must_use]
pub fn reading(arity: usize) -> Self {
Self::doing(Effect::Reads, &vec![Touch { effect: Effect::Reads, escapes: true }; arity])
}
#[must_use]
pub fn through_arguments(arity: usize) -> Self {
Self::doing(Effect::Nothing, &vec![Touch::everything(); arity])
}
#[must_use]
pub fn outside(&self) -> Effect {
self.outside
}
#[must_use]
pub fn param(&self, index: usize) -> Touch {
self.params.get(index).copied().unwrap_or_else(Touch::everything)
}
#[must_use]
pub fn arity(&self) -> usize {
self.params.len()
}
#[must_use]
pub fn only_through_arguments(&self) -> bool {
self.outside == Effect::Nothing
}
#[must_use]
pub fn writes_nothing(&self) -> bool {
!self.outside.writes() && self.params.iter().all(|touch| !touch.effect.writes())
}
#[must_use]
pub fn touches_nothing(&self) -> bool {
self.outside == Effect::Nothing
&& self.params.iter().all(|touch| touch.effect == Effect::Nothing)
}
#[must_use]
fn as_well_as(&self, other: &Self) -> Self {
let arity = self.params.len().max(other.params.len());
let params = (0..arity).map(|at| self.param(at).as_well_as(other.param(at))).collect();
Self { outside: self.outside.as_well_as(other.outside), params }
}
fn touch_everything(&mut self) {
self.outside = Effect::Writes;
for touch in &mut self.params {
*touch = Touch::everything();
}
}
}
#[derive(Clone, Debug, Default)]
pub struct Summaries {
known: HashMap<Symbol, Summary>,
}
impl Summaries {
#[must_use]
pub fn nothing() -> Self {
Self::default()
}
#[must_use]
pub fn of_module(module: &Module) -> Self {
let mut summaries = Self::default();
for id in module.funcs() {
let func = &module[id];
let arity = func.signature().params.len();
if let Some(summary) = from_attributes(func.attrs.set, arity) {
summaries.known.insert(func.name, summary);
}
}
summaries
}
#[must_use]
pub fn of(&self, name: Symbol) -> Option<&Summary> {
self.known.get(&name)
}
#[must_use]
pub fn at(&self, func: &Func, call: Inst) -> Option<&Summary> {
match Callee::of(func, call)? {
Callee::Direct(name) => self.of(name),
Callee::Indirect | Callee::Intrinsic(_) | Callee::Asm => None,
}
}
pub fn record(&mut self, name: Symbol, summary: Summary) {
let merged = match self.known.get(&name) {
Some(said) => said.as_well_as(&summary),
None => summary,
};
self.known.insert(name, merged);
}
#[must_use]
pub fn len(&self) -> usize {
self.known.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.known.is_empty()
}
}
fn from_attributes(set: AttrSet, arity: usize) -> Option<Summary> {
if set.contains(AttrSet::READNONE) {
let params = vec![Touch { effect: Effect::Nothing, escapes: true }; arity];
return Some(Summary { outside: Effect::Nothing, params: params.into() });
}
if set.contains(AttrSet::READONLY) {
return Some(Summary::reading(arity));
}
if set.contains(AttrSet::ARGMEM_ONLY) {
return Some(Summary::through_arguments(arity));
}
None
}
pub fn summarize(module: &Module, graph: &CallGraph, summaries: &mut Summaries) {
let arity = |node: Node| match graph.func(node) {
Some(id) => module[id].signature().params.len(),
None => 0,
};
let answers = graph.solve(
|node| Summary::nothing(arity(node)),
|node, answers| match graph.trusted_body(node) {
Some(id) => what_the_body_does(&module[id], graph, answers, summaries),
None => match summaries.of(graph.name(node)) {
Some(said) => said.clone(),
None => Summary::everything(arity(node)),
},
},
);
for node in graph.nodes() {
if graph.trusted_body(node).is_none() {
continue;
}
summaries.record(graph.name(node), answers[node.index()].clone());
}
}
fn what_the_body_does(
func: &Func,
graph: &CallGraph,
answers: &[Summary],
said: &Summaries,
) -> Summary {
let arity = func.signature().params.len();
let Some(entry) = func.entry() else { return Summary::everything(arity) };
if func[entry].params.len() != arity {
return Summary::everything(arity);
}
let mut callees: HashMap<Inst, Summary> = HashMap::new();
let mut steps = 0;
for block in func.blocks() {
for inst in func.insts(block) {
steps += 1;
if steps > MAX_STEPS {
return Summary::everything(arity);
}
if let Some(summary) = what_that_call_does(func, inst, graph, answers, said) {
callees.insert(inst, summary);
}
}
}
let escapes = Escapes::with(func, |inst, index| {
callees.get(&inst).is_some_and(|summary| !summary.param(index).escapes)
});
let mut summary = Summary::nothing(arity);
for block in func.blocks() {
for inst in func.insts(block) {
add_block_escapes(func, entry, &mut summary, inst);
if let Some(callee) = callees.get(&inst) {
add_call(func, entry, &escapes, &mut summary, inst, callee);
continue;
}
add_access(func, entry, &escapes, &mut summary, inst);
add_operand_escapes(func, entry, &mut summary, inst);
}
}
summary
}
fn what_that_call_does(
func: &Func,
inst: Inst,
graph: &CallGraph,
answers: &[Summary],
said: &Summaries,
) -> Option<Summary> {
let callee = Callee::of(func, inst)?;
let direct = matches!(func[inst].opcode, Opcode::Call | Opcode::TailCall);
let Callee::Direct(name) = callee else {
return Some(Summary::everything(0));
};
if !direct {
return Some(Summary::everything(0));
}
let walked = graph.node(name).map(|node| answers[node.index()].clone());
let promised = said.of(name).cloned();
Some(match (walked, promised) {
(Some(walked), Some(promised)) => walked.as_well_as(&promised),
(Some(only), None) | (None, Some(only)) => only,
(None, None) => Summary::everything(0),
})
}
fn add_call(
func: &Func,
entry: Block,
escapes: &Escapes,
summary: &mut Summary,
inst: Inst,
callee: &Summary,
) {
summary.outside = summary.outside.and_then(callee.outside());
let args = &func[func[inst].args];
for (at, &arg) in args.iter().enumerate() {
if !func[arg].ty.is_ptr() {
continue;
}
let touch = callee.param(at);
if touch == Touch::nothing() {
continue;
}
match behind(func, entry, escapes, arg) {
Behind::Param(at) => summary.params[at] = summary.params[at].and_then(touch),
Behind::Private => {}
Behind::Outside => summary.outside = summary.outside.and_then(touch.effect),
}
}
}
fn add_access(func: &Func, entry: Block, escapes: &Escapes, summary: &mut Summary, inst: Inst) {
let data = func[inst];
if !data.opcode.has_effects() || data.opcode.is_terminator() {
return;
}
if data.opcode.touches_only_planes() {
return;
}
let args = &func[data.args];
let mut through = |at: usize, effect: Effect| match behind(func, entry, escapes, args[at]) {
Behind::Param(at) => {
summary.params[at].effect = summary.params[at].effect.and_then(effect);
}
Behind::Private => {}
Behind::Outside => summary.outside = summary.outside.and_then(effect),
};
match data.opcode {
Opcode::Alloca => {}
Opcode::Load | Opcode::AtomicLoad | Opcode::Prefetch => through(0, Effect::Reads),
Opcode::Store | Opcode::AtomicStore => through(1, Effect::Writes),
Opcode::AtomicRmw | Opcode::Cmpxchg | Opcode::Memset => through(0, Effect::Writes),
Opcode::Memcpy | Opcode::Memmove => {
through(0, Effect::Writes);
through(1, Effect::Reads);
}
_ => summary.touch_everything(),
}
}
fn add_operand_escapes(func: &Func, entry: Block, summary: &mut Summary, inst: Inst) {
let data = func[inst];
for (at, &arg) in func[data.args].iter().enumerate() {
if keeps_address(data.opcode, at) {
continue;
}
if let Some(at) = param_behind(func, entry, arg) {
summary.params[at].escapes = true;
}
}
}
fn add_block_escapes(func: &Func, entry: Block, summary: &mut Summary, inst: Inst) {
for call in func.successors(inst) {
for &arg in &func[call.args] {
if let Some(at) = param_behind(func, entry, arg) {
summary.params[at].escapes = true;
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Behind {
Param(usize),
Private,
Outside,
}
fn behind(func: &Func, entry: Block, escapes: &Escapes, pointer: Value) -> Behind {
match origin(func, pointer).0 {
Origin::Local(local) if !escapes.escaped(local) => Behind::Private,
Origin::Unknown(value) => match param_of(func, entry, value) {
Some(at) => Behind::Param(at),
None => Behind::Outside,
},
_ => Behind::Outside,
}
}
fn param_behind(func: &Func, entry: Block, pointer: Value) -> Option<usize> {
let Origin::Unknown(value) = origin(func, pointer).0 else { return None };
param_of(func, entry, value)
}
fn param_of(func: &Func, entry: Block, value: Value) -> Option<usize> {
match func[value].def {
Def::Param { block, index } if block == entry => Some(index as usize),
_ => None,
}
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{
Builder, Extra, Flags, InstData, MemInfo, MemOrder, Pic, Restrict, Signature, Type,
};
use rucc_target::{TargetInfo, Triple};
use super::{
AttrSet, CallGraph, Effect, Func, Module, Opcode, Summaries, Summary, Touch, Value,
summarize,
};
fn access() -> MemInfo {
MemInfo {
size: 4,
align: 4,
owns: 4,
order: MemOrder::NotAtomic,
tbaa: None,
restrict: Restrict::NONE,
}
}
fn somewhere(build: &mut Builder<'_>, names: &mut Interner) -> Value {
let name = names.intern("v");
build.value(
InstData { extra: Extra::Symbol(name), ..InstData::new(Opcode::GlobalAddr) },
Type::PTR,
)
}
fn stack(build: &mut Builder<'_>) -> Value {
let mem = build.func().add_mem(access());
build.value(InstData { extra: Extra::Mem(mem), ..InstData::new(Opcode::Alloca) }, Type::PTR)
}
fn reads(build: &mut Builder<'_>, addr: Value) -> Value {
build.load(Type::int(32), addr, access(), Flags::NONE)
}
fn writes(build: &mut Builder<'_>, addr: Value) {
let zero = build.iconst(Type::int(32), 0);
build.store(zero, addr, access(), Flags::NONE);
}
fn calls(build: &mut Builder<'_>, names: &mut Interner, name: &str, args: &[Value]) {
let name = names.intern(name);
let params = vec![Type::PTR; args.len()];
let signature = build.func().add_signature(Signature::new().with_params(¶ms));
build.call(name, signature, args);
}
type Body = fn(&mut Interner, &mut Builder<'_>, &[Value]);
struct Worked {
names: Interner,
summaries: Summaries,
}
impl Worked {
fn out(bodies: &[(&str, usize, AttrSet, Option<Body>)]) -> Self {
let mut names = Interner::new();
let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
let mut module = Module::new(names.intern("t.c"), &target);
for &(name, arity, attrs, body) in bodies {
let params = vec![Type::PTR; arity];
let mut func = Func::new(names.intern(name), Signature::new().with_params(¶ms));
func.attrs.set = attrs;
if let Some(body) = body {
let entry = func.create_block();
let args: Vec<Value> =
(0..arity).map(|_| func.append_param(entry, Type::PTR)).collect();
let mut build = Builder::new(&mut func, entry);
body(&mut names, &mut build, &args);
}
module.add_func(func);
}
let mut summaries = Summaries::of_module(&module);
summarize(&module, &CallGraph::of(&module, Pic::Executable), &mut summaries);
Self { names, summaries }
}
fn about(&mut self, name: &str) -> Summary {
let name = self.names.intern(name);
self.summaries.of(name).expect("a defined function has a summary").clone()
}
}
fn nothing(_: &mut Interner, build: &mut Builder<'_>, _: &[Value]) {
build.ret(&[]);
}
#[test]
fn a_body_that_goes_nowhere_near_memory_says_so() {
let mut worked = Worked::out(&[("f", 2, AttrSet::NONE, Some(nothing))]);
let f = worked.about("f");
assert!(f.touches_nothing());
assert!(f.writes_nothing());
assert!(f.only_through_arguments());
assert_eq!(f.arity(), 2);
assert_eq!(f.param(0), Touch::nothing());
assert_eq!(f.param(1), Touch::nothing());
}
#[test]
fn a_load_through_one_parameter_is_a_read_of_that_one() {
fn body(_: &mut Interner, build: &mut Builder<'_>, args: &[Value]) {
let value = reads(build, args[0]);
build.ret(&[value]);
}
let mut worked = Worked::out(&[("f", 2, AttrSet::NONE, Some(body))]);
let f = worked.about("f");
assert_eq!(f.param(0).effect, Effect::Reads);
assert_eq!(f.param(1).effect, Effect::Nothing);
assert_eq!(f.outside(), Effect::Nothing);
assert!(f.writes_nothing());
assert!(f.only_through_arguments());
assert!(!f.param(0).escapes, "dereferencing an address is not keeping it");
}
#[test]
fn a_store_through_one_parameter_is_a_write_of_that_one() {
fn body(_: &mut Interner, build: &mut Builder<'_>, args: &[Value]) {
writes(build, args[1]);
build.ret(&[]);
}
let mut worked = Worked::out(&[("f", 2, AttrSet::NONE, Some(body))]);
let f = worked.about("f");
assert_eq!(f.param(0).effect, Effect::Nothing);
assert_eq!(f.param(1).effect, Effect::Writes);
assert!(!f.writes_nothing());
assert!(f.only_through_arguments(), "the only thing it wrote, it was handed");
}
#[test]
fn a_global_is_not_anybody_s_parameter() {
fn body(names: &mut Interner, build: &mut Builder<'_>, _: &[Value]) {
let global = somewhere(build, names);
writes(build, global);
build.ret(&[]);
}
let mut worked = Worked::out(&[("f", 1, AttrSet::NONE, Some(body))]);
let f = worked.about("f");
assert_eq!(f.outside(), Effect::Writes);
assert_eq!(f.param(0), Touch::nothing());
assert!(!f.only_through_arguments());
}
#[test]
fn what_a_function_did_to_its_own_stack_is_nobody_else_s_business() {
fn body(_: &mut Interner, build: &mut Builder<'_>, _: &[Value]) {
let local = stack(build);
writes(build, local);
let value = reads(build, local);
build.ret(&[value]);
}
let mut worked = Worked::out(&[("f", 1, AttrSet::NONE, Some(body))]);
assert!(worked.about("f").touches_nothing());
}
#[test]
fn a_copy_writes_the_one_it_writes_and_reads_the_one_it_reads() {
fn body(_: &mut Interner, build: &mut Builder<'_>, args: &[Value]) {
let mem = build.func().add_mem(access());
let list = build.func().push_values(&[args[0], args[1]]);
build.inst(
InstData { args: list, extra: Extra::Mem(mem), ..InstData::new(Opcode::Memcpy) },
&[],
);
build.ret(&[]);
}
let mut worked = Worked::out(&[("f", 2, AttrSet::NONE, Some(body))]);
let f = worked.about("f");
assert_eq!(f.param(0).effect, Effect::Writes);
assert_eq!(f.param(1).effect, Effect::Reads);
assert!(f.only_through_arguments());
assert!(!f.param(0).escapes);
assert!(!f.param(1).escapes);
}
#[test]
fn an_opcode_this_was_not_written_for_did_everything() {
fn body(_: &mut Interner, build: &mut Builder<'_>, args: &[Value]) {
let mem = build.func().add_mem(access());
let list = build.func().push_values(&[args[0]]);
build.inst(
InstData { args: list, extra: Extra::Mem(mem), ..InstData::new(Opcode::VaStart) },
&[],
);
build.ret(&[]);
}
let mut worked = Worked::out(&[("f", 1, AttrSet::NONE, Some(body))]);
let f = worked.about("f");
assert_eq!(f.outside(), Effect::Writes);
assert_eq!(f.param(0), Touch::everything());
}
#[test]
fn what_the_callee_does_to_what_it_was_handed_is_what_the_caller_does() {
fn callee(_: &mut Interner, build: &mut Builder<'_>, args: &[Value]) {
writes(build, args[0]);
build.ret(&[]);
}
fn caller(names: &mut Interner, build: &mut Builder<'_>, args: &[Value]) {
calls(build, names, "callee", &[args[1]]);
build.ret(&[]);
}
let mut worked = Worked::out(&[
("callee", 1, AttrSet::NONE, Some(callee)),
("caller", 2, AttrSet::NONE, Some(caller)),
]);
let caller = worked.about("caller");
assert_eq!(caller.param(0), Touch::nothing());
assert_eq!(caller.param(1).effect, Effect::Writes);
assert_eq!(caller.outside(), Effect::Nothing);
assert!(caller.only_through_arguments());
}
#[test]
fn a_parameter_handed_to_something_that_does_not_keep_it_has_not_got_out() {
fn callee(_: &mut Interner, build: &mut Builder<'_>, args: &[Value]) {
let value = reads(build, args[0]);
build.ret(&[value]);
}
fn caller(names: &mut Interner, build: &mut Builder<'_>, args: &[Value]) {
calls(build, names, "callee", &[args[0]]);
build.ret(&[]);
}
let mut worked = Worked::out(&[
("callee", 1, AttrSet::NONE, Some(callee)),
("caller", 1, AttrSet::NONE, Some(caller)),
]);
assert!(!worked.about("callee").param(0).escapes);
assert!(!worked.about("caller").param(0).escapes);
}
#[test]
fn a_parameter_written_down_somewhere_has_got_out() {
fn body(names: &mut Interner, build: &mut Builder<'_>, args: &[Value]) {
let global = somewhere(build, names);
build.store(args[0], global, access(), Flags::NONE);
build.ret(&[]);
}
let mut worked = Worked::out(&[("f", 1, AttrSet::NONE, Some(body))]);
let f = worked.about("f");
assert!(f.param(0).escapes);
assert_eq!(f.param(0).effect, Effect::Nothing);
assert_eq!(f.outside(), Effect::Writes);
}
#[test]
fn a_parameter_a_caller_cannot_be_told_about_travels_up_as_a_write_of_everything() {
fn callee(names: &mut Interner, build: &mut Builder<'_>, args: &[Value]) {
let global = somewhere(build, names);
build.store(args[0], global, access(), Flags::NONE);
build.ret(&[]);
}
fn caller(names: &mut Interner, build: &mut Builder<'_>, args: &[Value]) {
calls(build, names, "callee", &[args[0]]);
build.ret(&[]);
}
let mut worked = Worked::out(&[
("callee", 1, AttrSet::NONE, Some(callee)),
("caller", 1, AttrSet::NONE, Some(caller)),
]);
let caller = worked.about("caller");
assert!(caller.param(0).escapes, "the callee kept it, so the caller let it go");
assert_eq!(caller.outside(), Effect::Writes);
}
#[test]
fn what_a_callee_did_to_a_local_it_was_only_lent_stays_inside() {
fn callee(_: &mut Interner, build: &mut Builder<'_>, args: &[Value]) {
writes(build, args[0]);
build.ret(&[]);
}
fn caller(names: &mut Interner, build: &mut Builder<'_>, _: &[Value]) {
let place = stack(build);
calls(build, names, "callee", &[place]);
build.ret(&[]);
}
let mut worked = Worked::out(&[
("callee", 1, AttrSet::NONE, Some(callee)),
("caller", 0, AttrSet::NONE, Some(caller)),
]);
assert_eq!(worked.about("callee").param(0).effect, Effect::Writes);
assert!(worked.about("caller").touches_nothing());
}
#[test]
fn a_local_the_callee_wrote_down_is_one_this_function_lost() {
fn callee(names: &mut Interner, build: &mut Builder<'_>, args: &[Value]) {
let global = somewhere(build, names);
build.store(args[0], global, access(), Flags::NONE);
build.ret(&[]);
}
fn caller(names: &mut Interner, build: &mut Builder<'_>, _: &[Value]) {
let place = stack(build);
calls(build, names, "callee", &[place]);
writes(build, place);
build.ret(&[]);
}
let mut worked = Worked::out(&[
("callee", 1, AttrSet::NONE, Some(callee)),
("caller", 0, AttrSet::NONE, Some(caller)),
]);
assert!(worked.about("callee").param(0).escapes);
assert_eq!(worked.about("caller").outside(), Effect::Writes);
}
#[test]
fn a_call_through_an_address_did_everything_to_everything() {
fn body(names: &mut Interner, build: &mut Builder<'_>, args: &[Value]) {
calls(build, names, "unknown", &[args[0]]);
build.ret(&[]);
}
let mut worked = Worked::out(&[
("unknown", 1, AttrSet::NONE, None),
("f", 1, AttrSet::NONE, Some(body)),
]);
let f = worked.about("f");
assert_eq!(f.outside(), Effect::Writes);
assert_eq!(f.param(0), Touch::everything());
}
#[test]
fn two_functions_that_call_each_other_and_touch_nothing_touch_nothing() {
fn ping(names: &mut Interner, build: &mut Builder<'_>, args: &[Value]) {
calls(build, names, "pong", &[args[0]]);
build.ret(&[]);
}
fn pong(names: &mut Interner, build: &mut Builder<'_>, args: &[Value]) {
calls(build, names, "ping", &[args[0]]);
build.ret(&[]);
}
let mut worked = Worked::out(&[
("ping", 1, AttrSet::NONE, Some(ping)),
("pong", 1, AttrSet::NONE, Some(pong)),
]);
assert!(worked.about("ping").touches_nothing());
assert!(worked.about("pong").touches_nothing());
}
#[test]
fn a_write_inside_a_cycle_is_still_found() {
fn ping(names: &mut Interner, build: &mut Builder<'_>, args: &[Value]) {
calls(build, names, "pong", &[args[0]]);
build.ret(&[]);
}
fn pong(names: &mut Interner, build: &mut Builder<'_>, args: &[Value]) {
writes(build, args[0]);
calls(build, names, "ping", &[args[0]]);
build.ret(&[]);
}
let mut worked = Worked::out(&[
("ping", 1, AttrSet::NONE, Some(ping)),
("pong", 1, AttrSet::NONE, Some(pong)),
]);
assert_eq!(worked.about("ping").param(0).effect, Effect::Writes);
assert_eq!(worked.about("pong").param(0).effect, Effect::Writes);
assert!(worked.about("ping").only_through_arguments());
}
#[test]
fn a_declaration_is_whatever_it_promised_and_nothing_more() {
let mut worked = Worked::out(&[
("plain", 1, AttrSet::NONE, None),
("none", 1, AttrSet::READNONE, None),
("only", 1, AttrSet::READONLY, None),
("args", 1, AttrSet::ARGMEM_ONLY, None),
]);
let names = worked.names.intern("plain");
assert!(worked.summaries.of(names).is_none(), "nobody promised anything about it");
assert!(worked.about("none").touches_nothing());
let only = worked.about("only");
assert!(only.writes_nothing());
assert_eq!(only.outside(), Effect::Reads);
assert_eq!(only.param(0).effect, Effect::Reads);
let args = worked.about("args");
assert!(args.only_through_arguments());
assert!(!args.writes_nothing());
assert_eq!(args.param(0), Touch::everything());
}
#[test]
fn no_attribute_promises_the_address_was_not_kept() {
let mut worked = Worked::out(&[
("none", 1, AttrSet::READNONE, None),
("only", 1, AttrSet::READONLY, None),
("args", 1, AttrSet::ARGMEM_ONLY, None),
]);
for name in ["none", "only", "args"] {
assert!(worked.about(name).param(0).escapes, "{name} promised no such thing");
}
}
#[test]
fn a_promise_the_body_does_not_keep_is_still_a_promise() {
fn body(names: &mut Interner, build: &mut Builder<'_>, _: &[Value]) {
let global = somewhere(build, names);
writes(build, global);
build.ret(&[]);
}
let mut worked = Worked::out(&[("f", 1, AttrSet::READNONE, Some(body))]);
assert!(worked.about("f").touches_nothing());
}
#[test]
fn an_entry_block_that_does_not_match_the_signature_gets_no_answer() {
let mut names = Interner::new();
let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
let mut module = Module::new(names.intern("t.c"), &target);
let params = vec![Type::PTR; 2];
let mut func = Func::new(names.intern("f"), Signature::new().with_params(¶ms));
let entry = func.create_block();
let only = func.append_param(entry, Type::PTR);
let mut build = Builder::new(&mut func, entry);
build.ret(&[only]);
module.add_func(func);
let mut summaries = Summaries::of_module(&module);
summarize(&module, &CallGraph::of(&module, Pic::Executable), &mut summaries);
let f = summaries.of(names.intern("f")).expect("a defined function has a summary");
assert_eq!(*f, Summary::everything(2));
}
#[test]
fn a_position_no_parameter_stands_for_is_a_position_anything_happened_to() {
let summary = Summary::nothing(1);
assert_eq!(summary.param(0), Touch::nothing());
assert_eq!(summary.param(1), Touch::everything());
assert_eq!(summary.param(9), Touch::everything());
}
#[test]
fn the_two_ways_of_combining_are_the_lattice_they_claim_to_be() {
let all = [Effect::Nothing, Effect::Reads, Effect::Writes];
for one in all {
assert_eq!(one.and_then(one), one, "{one:?} is not idempotent");
assert_eq!(one.as_well_as(one), one, "{one:?} is not idempotent");
assert_eq!(one.and_then(Effect::Nothing), one, "nothing happening changes nothing");
assert_eq!(one.as_well_as(Effect::Writes), one, "writing promises nothing");
for two in all {
assert_eq!(one.and_then(two), two.and_then(one), "{one:?} and {two:?} disagree");
assert_eq!(one.as_well_as(two), two.as_well_as(one), "{one:?} and {two:?}");
let both = one.and_then(two);
assert!(both.reads() >= one.reads());
assert!(both.writes() >= one.writes());
}
}
assert_eq!(Effect::Nothing.name(), "nothing");
assert_eq!(Effect::Reads.name(), "reads");
assert_eq!(Effect::Writes.name(), "writes");
}
#[test]
fn a_read_is_a_read_and_only_a_write_is_a_write() {
assert!(!Effect::Nothing.reads());
assert!(!Effect::Nothing.writes());
assert!(Effect::Reads.reads());
assert!(!Effect::Reads.writes());
assert!(Effect::Writes.reads(), "a written byte is one the call could have looked at");
assert!(Effect::Writes.writes());
}
#[test]
fn nothing_known_about_anything_is_a_thing_this_can_be() {
let mut names = Interner::new();
let summaries = Summaries::nothing();
assert!(summaries.is_empty());
assert_eq!(summaries.len(), 0);
assert!(summaries.of(names.intern("f")).is_none());
}
#[test]
fn only_a_direct_call_has_a_summary_at_the_call_site() {
let mut worked = Worked::out(&[("callee", 1, AttrSet::READNONE, None)]);
let func = Worked::caller(&mut worked.names);
let direct = func.1;
assert!(worked.summaries.at(&func.0, direct).is_some_and(Summary::touches_nothing));
assert!(worked.summaries.at(&func.0, func.2).is_none(), "through an address");
assert!(worked.summaries.at(&func.0, func.3).is_none(), "not a call at all");
}
impl Worked {
fn caller(names: &mut Interner) -> (Func, super::Inst, super::Inst, super::Inst) {
let mut func = Func::new(names.intern("caller"), Signature::new());
let block = func.create_block();
let mut build = Builder::new(&mut func, block);
let signature = build.func().add_signature(Signature::new());
let direct = build.call(names.intern("callee"), signature, &[]);
let varargs = build.func().push_abis(&[]);
let info =
build.func().add_call(rucc_ir::CallInfo { callee: None, signature, varargs });
let indirect = build.inst(
InstData { extra: Extra::Call(info), ..InstData::new(Opcode::CallIndirect) },
&[],
);
let end = build.ret(&[]);
(func, direct, indirect, end)
}
}
}