use std::collections::{HashMap, HashSet};
use rucc_base::{Interner, Symbol};
use rucc_ir::{AttrSet, Extra, Func, Inst, Module, Opcode};
#[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 {
let mut purity = self.declared(name).stronger(self.inferred(name));
if let Some(&known) = self.from_the_library.get(&name) {
purity = purity.stronger(known);
}
purity
}
}
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, Module,
Opcode, Signature, Type,
};
use rucc_target::{TargetInfo, Triple};
use super::{Callee, Facts, LIBRARY, Purity};
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");
}
}
}