use std::collections::{HashMap, HashSet};
use rucc_base::{Interner, Symbol};
use rucc_ir::{AttrSet, Extra, Flags, Func, Inst, InstData, MemOrder, Module, Opcode, Value};
use crate::alias::{Escapes, Origin, origin};
use crate::callgraph::{CallGraph, Node};
use crate::cfg::Cfg;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Purity {
Const,
LoopingConst,
Pure,
LoopingPure,
Opaque,
}
impl Purity {
pub const ALL: [Self; 5] =
[Self::Const, Self::LoopingConst, Self::Pure, Self::LoopingPure, Self::Opaque];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Const => "const",
Self::LoopingConst => "const, may not return",
Self::Pure => "pure",
Self::LoopingPure => "pure, may not return",
Self::Opaque => "opaque",
}
}
#[must_use]
pub const fn reads_memory(self) -> bool {
match self {
Self::Const | Self::LoopingConst => false,
Self::Pure | Self::LoopingPure | Self::Opaque => true,
}
}
#[must_use]
pub const fn writes_memory(self) -> bool {
matches!(self, Self::Opaque)
}
#[must_use]
pub const fn terminates(self) -> bool {
matches!(self, Self::Const | Self::Pure)
}
#[must_use]
pub const fn depends_only_on_arguments(self) -> bool {
!self.reads_memory() && !self.writes_memory()
}
#[must_use]
pub const fn can_be_deleted_when_unused(self) -> bool {
!self.writes_memory() && self.terminates()
}
#[must_use]
pub const fn stronger(self, other: Self) -> Self {
match (self, other) {
(Self::Opaque, it) | (it, Self::Opaque) => it,
(one, two) => Self::of(
one.reads_memory() && two.reads_memory(),
one.terminates() || two.terminates(),
),
}
}
#[must_use]
pub const fn weaker(self, other: Self) -> Self {
match (self, other) {
(Self::Opaque, _) | (_, Self::Opaque) => Self::Opaque,
(one, two) => Self::of(
one.reads_memory() || two.reads_memory(),
one.terminates() && two.terminates(),
),
}
}
const fn of(reads: bool, terminates: bool) -> Self {
match (reads, terminates) {
(false, true) => Self::Const,
(false, false) => Self::LoopingConst,
(true, true) => Self::Pure,
(true, false) => Self::LoopingPure,
}
}
}
impl std::fmt::Display for Purity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Callee {
Direct(Symbol),
Indirect,
Intrinsic(Symbol),
Asm,
}
impl Callee {
#[must_use]
pub fn of(func: &Func, inst: Inst) -> Option<Self> {
let data = &func[inst];
match data.opcode {
Opcode::Call | Opcode::TailCall | Opcode::CallIndirect => match data.extra {
Extra::Call(at) => Some(match func[at].callee {
Some(name) => Self::Direct(name),
None => Self::Indirect,
}),
_ => Some(Self::Indirect),
},
Opcode::TargetIntrinsic => match data.extra {
Extra::Symbol(name) => Some(Self::Intrinsic(name)),
_ => Some(Self::Asm),
},
Opcode::InlineAsm => Some(Self::Asm),
_ => None,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Facts {
declared: HashMap<Symbol, AttrSet>,
inferred: HashMap<Symbol, Purity>,
from_the_library: HashMap<Symbol, Purity>,
}
impl Facts {
#[must_use]
pub fn nothing() -> Self {
Self::default()
}
#[must_use]
pub fn of_module(module: &Module, names: &Interner) -> Self {
let mut facts = Self::default();
let mut defined = HashSet::new();
for id in module.funcs() {
let func = &module[id];
facts.declared.insert(func.name, func.attrs.set);
if !func.is_declaration() {
defined.insert(func.name);
}
}
for &name in facts.declared.keys() {
if defined.contains(&name) {
continue;
}
if let Some(purity) = library_purity(names.resolve(name)) {
facts.from_the_library.insert(name, purity);
}
}
facts
}
pub fn without_the_library(&mut self) {
self.from_the_library.clear();
}
pub fn not_the_library_name(&mut self, name: Symbol) {
self.from_the_library.remove(&name);
}
pub fn record_inferred(&mut self, name: Symbol, purity: Purity) {
self.inferred.insert(name, purity);
}
#[must_use]
pub fn declared(&self, name: Symbol) -> Purity {
match self.declared.get(&name) {
Some(&set) => from_attributes(set),
None => Purity::Opaque,
}
}
#[must_use]
pub fn inferred(&self, name: Symbol) -> Purity {
self.inferred.get(&name).copied().unwrap_or(Purity::Opaque)
}
#[must_use]
pub fn purity_of(&self, callee: Callee) -> Purity {
match callee {
Callee::Direct(name) => self.of_name(name),
Callee::Indirect => Purity::Opaque,
Callee::Intrinsic(_) => Purity::Opaque,
Callee::Asm => Purity::Opaque,
}
}
fn of_name(&self, name: Symbol) -> Purity {
self.what_was_said_about(name).stronger(self.inferred(name))
}
fn what_was_said_about(&self, name: Symbol) -> Purity {
match self.from_the_library.get(&name) {
Some(&known) => self.declared(name).stronger(known),
None => self.declared(name),
}
}
}
pub fn infer(module: &Module, graph: &CallGraph, facts: &mut Facts) {
let answers = graph.solve(
|_| Purity::Const,
|node, answers| match graph.trusted_body(node) {
Some(id) if !graph.reaches_unknown(node) => {
let purity = what_the_body_does(&module[id], graph, answers, facts);
match in_a_cycle(graph, node) {
true => purity.weaker(Purity::LoopingConst),
false => purity,
}
}
_ => Purity::Opaque,
},
);
for node in graph.nodes() {
let purity = answers[node.index()];
if purity != Purity::Opaque {
facts.record_inferred(graph.name(node), purity);
}
}
}
fn what_the_body_does(func: &Func, graph: &CallGraph, answers: &[Purity], facts: &Facts) -> Purity {
let mut so_far = Purity::Const;
let mut escapes: Option<Escapes> = None;
for block in func.blocks() {
for inst in func.insts(block) {
let data = func[inst];
so_far = so_far.weaker(if let Some(callee) = Callee::of(func, inst) {
what_that_call_does(callee, graph, answers, facts)
} else if !data.opcode.has_effects() || data.opcode.is_terminator() {
Purity::Const
} else {
match data.opcode {
Opcode::Alloca => Purity::Const,
Opcode::Load if plain(func, data) => {
let escapes = escapes.get_or_insert_with(|| Escapes::of(func));
match ours(func, escapes, func[data.args][0]) {
true => Purity::Const,
false => Purity::Pure,
}
}
Opcode::Store if plain(func, data) => {
let escapes = escapes.get_or_insert_with(|| Escapes::of(func));
match ours(func, escapes, func[data.args][1]) {
true => Purity::Const,
false => Purity::Opaque,
}
}
_ => Purity::Opaque,
}
});
if so_far == Purity::Opaque {
return Purity::Opaque;
}
}
}
match has_a_cycle(func) {
true => so_far.weaker(Purity::LoopingConst),
false => so_far,
}
}
fn what_that_call_does(
callee: Callee,
graph: &CallGraph,
answers: &[Purity],
facts: &Facts,
) -> Purity {
let Callee::Direct(name) = callee else {
return Purity::Opaque;
};
let said = facts.what_was_said_about(name);
match graph.node(name) {
Some(node) => said.stronger(answers[node.index()]),
None => said,
}
}
fn plain(func: &Func, data: InstData) -> bool {
if data.flags.contains(Flags::VOLATILE) {
return false;
}
match data.extra {
Extra::Mem(mem) => func[mem].order == MemOrder::NotAtomic,
_ => false,
}
}
fn ours(func: &Func, escapes: &Escapes, pointer: Value) -> bool {
matches!(origin(func, pointer).0, Origin::Local(local) if !escapes.escaped(local))
}
fn in_a_cycle(graph: &CallGraph, node: Node) -> bool {
graph.components()[graph.component_of(node)].len() > 1 || graph.calls(node).contains(&node)
}
fn has_a_cycle(func: &Func) -> bool {
let cfg = Cfg::new(func);
func.blocks().any(|block| {
let Some(from) = cfg.rank(block) else { return false };
cfg.successors(block).iter().any(|&to| cfg.rank(to).is_some_and(|to| to <= from))
})
}
fn from_attributes(set: AttrSet) -> Purity {
let terminates = !set.contains(AttrSet::NORETURN);
if set.contains(AttrSet::READNONE) {
return Purity::of(false, terminates);
}
if set.contains(AttrSet::READONLY) {
return Purity::of(true, terminates);
}
Purity::Opaque
}
const LIBRARY: &[(&str, Purity)] = &[
("abs", Purity::Const),
("imaxabs", Purity::Const),
("labs", Purity::Const),
("llabs", Purity::Const),
("memchr", Purity::Pure),
("memcmp", Purity::Pure),
("strchr", Purity::Pure),
("strcmp", Purity::Pure),
("strcspn", Purity::Pure),
("strlen", Purity::Pure),
("strncmp", Purity::Pure),
("strnlen", Purity::Pure),
("strpbrk", Purity::Pure),
("strrchr", Purity::Pure),
("strspn", Purity::Pure),
("strstr", Purity::Pure),
];
fn library_purity(name: &str) -> Option<Purity> {
let name = name.strip_prefix("__builtin_").unwrap_or(name);
LIBRARY.binary_search_by_key(&name, |&(named, _)| named).ok().map(|at| LIBRARY[at].1)
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{
AsmInfo, AttrSet, BlockCallList, Builder, CallInfo, Extra, Flags, Func, InstData, IntPred,
MemInfo, MemOrder, Module, Opcode, Pic, Restrict, Signature, Type, Value,
};
use rucc_target::{TargetInfo, Triple};
use super::{CallGraph, Callee, Facts, LIBRARY, Purity, infer};
fn module(named: &[(&str, bool, AttrSet)]) -> (Interner, Module) {
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, defined, attrs) in named {
let mut func = Func::new(names.intern(name), Signature::new());
func.attrs.set = attrs;
if defined {
let block = func.create_block();
let mut build = Builder::new(&mut func, block);
let zero = build.iconst(Type::int(32), 0);
build.ret(&[zero]);
}
module.add_func(func);
}
(names, module)
}
fn purity(names: &mut Interner, module: &Module, name: &str) -> Purity {
let facts = Facts::of_module(module, names);
let symbol = names.intern(name);
facts.purity_of(Callee::Direct(symbol))
}
#[test]
fn a_function_nobody_promised_anything_about_is_opaque() {
let (mut names, module) = module(&[("f", true, AttrSet::NONE)]);
assert_eq!(purity(&mut names, &module, "f"), Purity::Opaque);
}
#[test]
fn a_name_this_module_never_heard_of_is_opaque_as_well() {
let (mut names, module) = module(&[("f", true, AttrSet::NONE)]);
let facts = Facts::of_module(&module, &names);
assert_eq!(facts.purity_of(Callee::Direct(names.intern("g"))), Purity::Opaque);
}
#[test]
fn the_const_attribute_is_honoured_because_the_user_asserted_it() {
let (mut names, module) = module(&[("f", false, AttrSet::READNONE)]);
let purity = purity(&mut names, &module, "f");
assert_eq!(purity, Purity::Const);
assert!(purity.depends_only_on_arguments());
assert!(purity.can_be_deleted_when_unused());
}
#[test]
fn the_pure_attribute_reads_memory_and_writes_none() {
let (mut names, module) = module(&[("f", false, AttrSet::READONLY)]);
let purity = purity(&mut names, &module, "f");
assert_eq!(purity, Purity::Pure);
assert!(purity.reads_memory());
assert!(!purity.writes_memory());
assert!(!purity.depends_only_on_arguments());
assert!(purity.can_be_deleted_when_unused());
}
#[test]
fn a_const_function_that_does_not_come_back_may_not_be_deleted() {
let (mut names, module) =
module(&[("f", false, AttrSet::READNONE.union(AttrSet::NORETURN))]);
let purity = purity(&mut names, &module, "f");
assert_eq!(purity, Purity::LoopingConst);
assert!(purity.depends_only_on_arguments());
assert!(!purity.can_be_deleted_when_unused());
}
#[test]
fn nothing_that_is_not_a_direct_call_is_anything_but_opaque() {
let (mut names, module) = module(&[("f", true, AttrSet::READNONE)]);
let facts = Facts::of_module(&module, &names);
assert_eq!(facts.purity_of(Callee::Indirect), Purity::Opaque);
assert_eq!(facts.purity_of(Callee::Asm), Purity::Opaque);
let vector = names.intern("__builtin_ia32_paddb");
assert_eq!(facts.purity_of(Callee::Intrinsic(vector)), Purity::Opaque);
}
#[test]
fn the_library_names_are_known_under_both_spellings() {
let (mut names, module) = module(&[
("strlen", false, AttrSet::NONE),
("abs", false, AttrSet::NONE),
("__builtin_strlen", false, AttrSet::NONE),
("printf", false, AttrSet::NONE),
]);
assert_eq!(purity(&mut names, &module, "strlen"), Purity::Pure);
assert_eq!(purity(&mut names, &module, "__builtin_strlen"), Purity::Pure);
assert_eq!(purity(&mut names, &module, "abs"), Purity::Const);
assert_eq!(purity(&mut names, &module, "printf"), Purity::Opaque);
}
#[test]
fn a_module_that_defines_strlen_means_its_own() {
let (mut names, module) = module(&[("strlen", true, AttrSet::NONE)]);
assert_eq!(purity(&mut names, &module, "strlen"), Purity::Opaque);
}
#[test]
fn no_builtin_takes_the_table_away_and_the_named_form_takes_one_entry() {
let (mut names, module) =
module(&[("strlen", false, AttrSet::NONE), ("abs", false, AttrSet::NONE)]);
let mut facts = Facts::of_module(&module, &names);
let strlen = names.intern("strlen");
let abs = names.intern("abs");
facts.not_the_library_name(strlen);
assert_eq!(facts.purity_of(Callee::Direct(strlen)), Purity::Opaque);
assert_eq!(facts.purity_of(Callee::Direct(abs)), Purity::Const);
facts.without_the_library();
assert_eq!(facts.purity_of(Callee::Direct(abs)), Purity::Opaque);
}
#[test]
fn what_the_user_wrote_and_what_analysis_worked_out_are_kept_apart() {
let (mut names, module) =
module(&[("f", true, AttrSet::READNONE.union(AttrSet::NORETURN))]);
let mut facts = Facts::of_module(&module, &names);
let f = names.intern("f");
assert_eq!(facts.declared(f), Purity::LoopingConst);
assert_eq!(facts.inferred(f), Purity::Opaque);
facts.record_inferred(f, Purity::Pure);
assert_eq!(facts.declared(f), Purity::LoopingConst);
assert_eq!(facts.inferred(f), Purity::Pure);
assert_eq!(facts.purity_of(Callee::Direct(f)), Purity::Const);
}
#[test]
fn what_an_instruction_calls_is_read_off_the_instruction() {
let mut names = Interner::new();
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("f"), signature, &[]);
let varargs = build.func().push_abis(&[]);
let info = build.func().add_call(CallInfo { callee: None, signature, varargs });
let indirect = build.inst(
InstData { extra: Extra::Call(info), ..InstData::new(Opcode::CallIndirect) },
&[],
);
let asm = build.inline_asm(
AsmInfo {
template: names.intern("nop"),
constraints: names.intern(""),
clobbers: names.intern(""),
targets: BlockCallList::EMPTY,
},
&[],
&[],
Flags::NONE,
);
let nothing = build.ret(&[]);
let f = names.intern("f");
assert_eq!(Callee::of(&func, nothing), None);
assert_eq!(Callee::of(&func, direct), Some(Callee::Direct(f)));
assert_eq!(Callee::of(&func, indirect), Some(Callee::Indirect));
assert_eq!(Callee::of(&func, asm), Some(Callee::Asm));
}
#[test]
fn the_two_ways_of_combining_are_the_lattice_they_claim_to_be() {
for one in Purity::ALL {
assert_eq!(one.stronger(one), one, "{one} is not idempotent");
assert_eq!(one.weaker(one), one, "{one} is not idempotent");
assert_eq!(one.stronger(Purity::Opaque), one, "opaque should say nothing");
assert_eq!(one.weaker(Purity::Opaque), Purity::Opaque, "opaque covers everything");
for two in Purity::ALL {
assert_eq!(one.stronger(two), two.stronger(one), "{one} and {two} disagree");
assert_eq!(one.weaker(two), two.weaker(one), "{one} and {two} disagree");
let both = one.weaker(two);
assert!(both.reads_memory() >= one.reads_memory());
assert!(both.writes_memory() >= one.writes_memory());
assert!(both.terminates() <= one.terminates());
}
}
}
#[test]
fn only_an_opaque_call_may_write_memory() {
for purity in Purity::ALL {
assert_eq!(purity.writes_memory(), purity == Purity::Opaque, "{purity}");
assert_eq!(purity.can_be_deleted_when_unused(), purity.terminates(), "{purity}");
}
}
#[test]
fn the_library_table_is_sorted_says_each_name_once_and_writes_no_memory() {
for pair in LIBRARY.windows(2) {
assert!(pair[0].0 < pair[1].0, "{} and {} are out of order", pair[0].0, pair[1].0);
}
for &(name, purity) in LIBRARY {
assert!(!purity.writes_memory(), "{name} would not be worth an entry");
assert!(purity.terminates(), "{name} is in the table to be deletable");
assert!(!name.starts_with("__builtin_"), "{name} is reached under both spellings");
}
}
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 calls(build: &mut Builder<'_>, names: &mut Interner, name: &str) {
let name = names.intern(name);
let signature = build.func().add_signature(Signature::new());
build.call(name, signature, &[]);
}
type Body = fn(&mut Interner, &mut Func);
struct Worked {
names: Interner,
facts: Facts,
}
impl Worked {
fn out(bodies: &[(&str, Body)]) -> Self {
Self::linked(Pic::Executable, bodies)
}
fn linked(pic: Pic, bodies: &[(&str, 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, body) in bodies {
let mut func = Func::new(names.intern(name), Signature::new());
body(&mut names, &mut func);
module.add_func(func);
}
let mut facts = Facts::of_module(&module, &names);
infer(&module, &CallGraph::of(&module, pic), &mut facts);
Self { names, facts }
}
fn about(&mut self, name: &str) -> Purity {
let name = self.names.intern(name);
self.facts.inferred(name)
}
fn at_a_call_site(&mut self, name: &str) -> Purity {
let name = self.names.intern(name);
self.facts.purity_of(Callee::Direct(name))
}
}
fn only_arithmetic(_: &mut Interner, func: &mut Func) {
let block = func.create_block();
let mut build = Builder::new(func, block);
let a = build.iconst(Type::int(32), 2);
let b = build.iconst(Type::int(32), 3);
let sum = build.binary(Opcode::Add, a, b, Flags::NONE);
build.ret(&[sum]);
}
fn empty(_: &mut Interner, func: &mut Func) {
let block = func.create_block();
Builder::new(func, block).ret(&[]);
}
fn declared(_: &mut Interner, _: &mut Func) {}
#[test]
fn a_function_that_only_computes_is_const() {
assert_eq!(Worked::out(&[("f", only_arithmetic)]).about("f"), Purity::Const);
}
#[test]
fn a_function_that_reads_memory_it_did_not_make_is_pure() {
fn reads(names: &mut Interner, func: &mut Func) {
let block = func.create_block();
let mut build = Builder::new(func, block);
let at = somewhere(&mut build, names);
let value = build.load(Type::int(32), at, access(), Flags::NONE);
build.ret(&[value]);
}
let mut worked = Worked::out(&[("f", reads)]);
assert_eq!(worked.about("f"), Purity::Pure);
assert!(worked.about("f").can_be_deleted_when_unused());
}
#[test]
fn a_function_that_writes_memory_it_did_not_make_is_opaque() {
fn writes(names: &mut Interner, func: &mut Func) {
let block = func.create_block();
let mut build = Builder::new(func, block);
let at = somewhere(&mut build, names);
let zero = build.iconst(Type::int(32), 0);
build.store(zero, at, access(), Flags::NONE);
build.ret(&[]);
}
assert_eq!(Worked::out(&[("f", writes)]).about("f"), Purity::Opaque);
}
#[test]
fn a_local_this_function_kept_to_itself_is_not_memory() {
fn temporary(_: &mut Interner, func: &mut Func) {
let block = func.create_block();
let mut build = Builder::new(func, block);
let at = stack(&mut build);
let zero = build.iconst(Type::int(32), 0);
build.store(zero, at, access(), Flags::NONE);
let back = build.load(Type::int(32), at, access(), Flags::NONE);
build.ret(&[back]);
}
assert_eq!(Worked::out(&[("f", temporary)]).about("f"), Purity::Const);
}
#[test]
fn a_local_whose_address_the_function_hands_back_is_memory_like_any_other() {
fn handed_back(_: &mut Interner, func: &mut Func) {
let block = func.create_block();
let mut build = Builder::new(func, block);
let at = stack(&mut build);
let zero = build.iconst(Type::int(32), 0);
build.store(zero, at, access(), Flags::NONE);
build.ret(&[at]);
}
assert_eq!(Worked::out(&[("f", handed_back)]).about("f"), Purity::Opaque);
}
#[test]
fn a_volatile_read_is_opaque_however_private_the_storage_is() {
fn volatile(_: &mut Interner, func: &mut Func) {
let block = func.create_block();
let mut build = Builder::new(func, block);
let at = stack(&mut build);
let value = build.load(Type::int(32), at, access(), Flags::VOLATILE);
build.ret(&[value]);
}
assert_eq!(Worked::out(&[("f", volatile)]).about("f"), Purity::Opaque);
}
#[test]
fn a_caller_is_what_the_function_it_calls_is() {
fn calls_g(names: &mut Interner, func: &mut Func) {
let block = func.create_block();
let mut build = Builder::new(func, block);
calls(&mut build, names, "g");
build.ret(&[]);
}
let mut worked = Worked::out(&[("f", calls_g), ("g", only_arithmetic)]);
assert_eq!(worked.about("g"), Purity::Const);
assert_eq!(worked.about("f"), Purity::Const);
}
#[test]
fn a_caller_of_something_nobody_can_see_is_opaque() {
fn calls_g(names: &mut Interner, func: &mut Func) {
let block = func.create_block();
let mut build = Builder::new(func, block);
calls(&mut build, names, "g");
build.ret(&[]);
}
let mut worked = Worked::out(&[("f", calls_g), ("g", declared)]);
assert_eq!(worked.about("g"), Purity::Opaque);
assert_eq!(worked.about("f"), Purity::Opaque);
}
#[test]
fn what_the_library_says_about_a_callee_reaches_the_caller() {
fn calls_strlen(names: &mut Interner, func: &mut Func) {
let block = func.create_block();
let mut build = Builder::new(func, block);
calls(&mut build, names, "strlen");
build.ret(&[]);
}
let mut worked = Worked::out(&[("f", calls_strlen), ("strlen", declared)]);
assert_eq!(worked.about("f"), Purity::Pure);
}
#[test]
fn two_functions_that_call_each_other_and_do_nothing_else_are_not_opaque() {
fn calls_g(names: &mut Interner, func: &mut Func) {
let block = func.create_block();
let mut build = Builder::new(func, block);
calls(&mut build, names, "g");
build.ret(&[]);
}
fn calls_f(names: &mut Interner, func: &mut Func) {
let block = func.create_block();
let mut build = Builder::new(func, block);
calls(&mut build, names, "f");
build.ret(&[]);
}
let mut worked = Worked::out(&[("f", calls_g), ("g", calls_f)]);
assert_eq!(worked.about("f"), Purity::LoopingConst);
assert_eq!(worked.about("g"), Purity::LoopingConst);
assert!(worked.about("f").depends_only_on_arguments());
assert!(!worked.about("f").can_be_deleted_when_unused());
}
#[test]
fn a_function_that_calls_itself_may_not_come_back() {
fn calls_itself(names: &mut Interner, func: &mut Func) {
let block = func.create_block();
let mut build = Builder::new(func, block);
calls(&mut build, names, "f");
build.ret(&[]);
}
assert_eq!(Worked::out(&[("f", calls_itself)]).about("f"), Purity::LoopingConst);
}
#[test]
fn a_function_with_a_loop_in_it_may_not_come_back() {
fn loops(_: &mut Interner, func: &mut Func) {
let entry = func.create_block();
let head = func.create_block();
let done = func.create_block();
let mut build = Builder::new(func, entry);
build.jump(head, &[]);
let mut build = Builder::new(func, head);
let zero = build.iconst(Type::int(32), 0);
let cond = build.icmp(IntPred::Eq, zero, zero);
build.br_if(cond, head, &[], done, &[]);
Builder::new(func, done).ret(&[]);
}
let mut worked = Worked::out(&[("f", loops)]);
assert_eq!(worked.about("f"), Purity::LoopingConst);
assert!(!worked.about("f").can_be_deleted_when_unused());
}
#[test]
fn a_branch_that_joins_again_is_not_a_loop() {
fn branches(_: &mut Interner, func: &mut Func) {
let entry = func.create_block();
let arm = func.create_block();
let join = func.create_block();
let mut build = Builder::new(func, entry);
let zero = build.iconst(Type::int(32), 0);
let cond = build.icmp(IntPred::Eq, zero, zero);
build.br_if(cond, arm, &[], join, &[]);
Builder::new(func, arm).jump(join, &[]);
Builder::new(func, join).ret(&[]);
}
assert_eq!(Worked::out(&[("f", branches)]).about("f"), Purity::Const);
}
#[test]
fn a_declaration_has_nothing_worked_out_about_it() {
assert_eq!(Worked::out(&[("f", declared)]).about("f"), Purity::Opaque);
}
#[test]
fn a_body_this_link_may_replace_has_nothing_worked_out_about_it() {
let mut worked = Worked::linked(Pic::Library, &[("f", only_arithmetic)]);
assert_eq!(worked.about("f"), Purity::Opaque);
let mut worked = Worked::linked(Pic::Executable, &[("f", only_arithmetic)]);
assert_eq!(worked.about("f"), Purity::Const);
}
#[test]
fn nothing_is_written_down_for_a_function_that_came_out_opaque() {
let mut worked = Worked::out(&[("f", declared)]);
assert!(worked.facts.inferred.is_empty());
assert_eq!(worked.about("f"), Purity::Opaque);
}
#[test]
fn what_the_user_declared_and_what_the_body_says_are_both_kept() {
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 mut func = Func::new(names.intern("f"), Signature::new());
func.attrs.set = AttrSet::READNONE;
let entry = func.create_block();
let head = func.create_block();
Builder::new(&mut func, entry).jump(head, &[]);
Builder::new(&mut func, head).jump(head, &[]);
module.add_func(func);
let mut facts = Facts::of_module(&module, &names);
infer(&module, &CallGraph::of(&module, Pic::Executable), &mut facts);
let name = names.intern("f");
assert_eq!(facts.inferred(name), Purity::LoopingConst);
assert_eq!(facts.declared(name), Purity::Const);
assert_eq!(facts.purity_of(Callee::Direct(name)), Purity::Const);
}
#[test]
fn an_answer_travels_as_far_up_the_chain_as_it_holds() {
fn calls_g(names: &mut Interner, func: &mut Func) {
let block = func.create_block();
let mut build = Builder::new(func, block);
calls(&mut build, names, "g");
build.ret(&[]);
}
fn calls_h(names: &mut Interner, func: &mut Func) {
let block = func.create_block();
let mut build = Builder::new(func, block);
calls(&mut build, names, "h");
build.ret(&[]);
}
let mut worked = Worked::out(&[("f", calls_g), ("g", calls_h), ("h", empty)]);
assert_eq!(worked.about("h"), Purity::Const);
assert_eq!(worked.about("g"), Purity::Const);
assert_eq!(worked.about("f"), Purity::Const);
assert_eq!(worked.at_a_call_site("f"), Purity::Const);
}
#[test]
fn a_reader_under_a_writer_makes_the_caller_opaque_and_not_pure() {
fn writes(names: &mut Interner, func: &mut Func) {
let block = func.create_block();
let mut build = Builder::new(func, block);
let at = somewhere(&mut build, names);
let zero = build.iconst(Type::int(32), 0);
build.store(zero, at, access(), Flags::NONE);
build.ret(&[]);
}
fn calls_both(names: &mut Interner, func: &mut Func) {
let block = func.create_block();
let mut build = Builder::new(func, block);
calls(&mut build, names, "g");
calls(&mut build, names, "h");
build.ret(&[]);
}
let mut worked = Worked::out(&[("f", calls_both), ("g", only_arithmetic), ("h", writes)]);
assert_eq!(worked.about("f"), Purity::Opaque);
}
}