use std::collections::BTreeMap;
use rucc_ir::{
Block, Extra, Func, Inst, InstData, MemInfo, MemOrder, Opcode, Restrict, Type, Value,
};
pub const SCOPE: u64 = 112;
pub const ALIGN: u32 = 8;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Kept {
pub promised: usize,
pub scoped: usize,
}
pub fn promise(func: &mut Func, width: u64) -> Kept {
let mut kept = Kept::default();
let mut accesses: Vec<Inst> = Vec::new();
let mut bases: BTreeMap<u16, u16> = BTreeMap::new();
let blocks: Vec<Block> = func.blocks().collect();
for block in blocks {
let insts: Vec<Inst> = func.insts(block).collect();
for inst in insts {
let Some(named) = through(func, inst) else { continue };
accesses.push(inst);
let seen = bases.entry(named.clique).or_default();
*seen = (*seen).max(named.base);
}
}
if accesses.is_empty() {
return kept;
}
let Some(entry) = func.entry() else { return kept };
let mut slots: BTreeMap<u16, Value> = BTreeMap::new();
for (&clique, &count) in &bases {
let Some(slot) = opened(func, entry, clique, count) else { continue };
slots.insert(clique, slot);
kept.scoped += 1;
}
closed(func, &slots);
for access in accesses {
let Some(named) = through(func, access) else { continue };
if !slots.contains_key(&named.clique) {
continue;
}
if promised(func, access, width) {
kept.promised += 1;
}
}
kept
}
fn through(func: &Func, inst: Inst) -> Option<Restrict> {
if !matches!(func[inst].opcode, Opcode::Load | Opcode::Store) {
return None;
}
let Extra::Mem(at) = func[inst].extra else { return None };
let named = func[at].restrict;
(named.clique != 0 && named.base != 0).then_some(named)
}
fn opened(func: &mut Func, entry: Block, clique: u16, count: u16) -> Option<Value> {
let first = func.insts(entry).next()?;
let span = func.span(first);
let empty = MemInfo {
size: SCOPE,
align: ALIGN,
order: MemOrder::NotAtomic,
tbaa: None,
owns: 0,
restrict: Restrict::NONE,
};
let mem = func.add_mem(empty);
let data = InstData { extra: Extra::Mem(mem), ..InstData::new(Opcode::Alloca) };
let slot = func.create_inst(data, &[Type::PTR], span);
func.insert_before(slot, first);
let address = func[slot].results().next()?;
let told = MemInfo { restrict: Restrict { clique, base: count }, ..empty };
let mem = func.add_mem(told);
let args = func.push_values(&[address]);
let data = InstData { args, extra: Extra::Mem(mem), ..InstData::new(Opcode::RestrictEnter) };
let enter = func.create_inst(data, &[], span);
func.insert_after(enter, slot);
Some(address)
}
fn closed(func: &mut Func, slots: &BTreeMap<u16, Value>) {
let blocks: Vec<Block> = func.blocks().collect();
for block in blocks {
let Some(end) = func.terminator(block) else { continue };
if !matches!(func[end].opcode, Opcode::Return | Opcode::TailCall) {
continue;
}
let span = func.span(end);
for &slot in slots.values().rev() {
let args = func.push_values(&[slot]);
let data = InstData { args, ..InstData::new(Opcode::RestrictLeave) };
let leave = func.create_inst(data, &[], span);
func.insert_before(leave, end);
}
}
}
fn promised(func: &mut Func, access: Inst, width: u64) -> bool {
let Some(pointer) = crate::pointer_of(func, access) else { return false };
let Extra::Mem(at) = func[access].extra else { return false };
let mut info = func[at];
info.size = crate::covered(func, access, info.size, width);
if info.size == 0 {
return false;
}
info.owns = 0;
info.tbaa = None;
let write = func[access].opcode == Opcode::Store;
let opcode = if write { Opcode::CheckRestrictWrite } else { Opcode::CheckRestrictRead };
let span = func.span(access);
let args = func.push_values(&[pointer]);
let extra = Extra::Mem(func.add_mem(info));
let data = InstData { args, extra, ..InstData::new(opcode) };
let check = func.create_inst(data, &[], span);
func.insert_before(check, access);
true
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{Builder, Flags, IntPred, Module, Signature, print_func, verify_func};
use rucc_target::{Arch, Env, Os, TargetInfo, Triple};
use super::*;
fn module(names: &mut Interner, unit: &str) -> Module {
let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
Module::new(names.intern(unit), &target)
}
fn through(named: Restrict) -> MemInfo {
MemInfo {
size: 0,
align: 4,
order: MemOrder::NotAtomic,
tbaa: None,
owns: 0,
restrict: named,
}
}
fn combine(names: &mut Interner, to: Restrict, from: Restrict) -> Func {
let word = Type::int(32);
let mut func = Func::new(
names.intern("combine"),
Signature::new().with_params(&[Type::PTR, Type::PTR]),
);
let entry = func.create_block();
let out = func.append_param(entry, Type::PTR);
let inp = func.append_param(entry, Type::PTR);
let mut b = Builder::new(&mut func, entry);
let value = b.load(word, inp, through(from), Flags::default());
b.store(value, out, through(to), Flags::default());
b.ret(&[]);
func
}
#[test]
fn a_block_that_declares_restrict_pointers_opens_a_scope_and_closes_it() {
let mut names = Interner::new();
let unit = module(&mut names, "combine.c");
let mut func =
combine(&mut names, Restrict { clique: 1, base: 1 }, Restrict { clique: 1, base: 2 });
assert_eq!(promise(&mut func, 8), Kept { promised: 2, scoped: 1 });
assert_eq!(
print_func(&unit, &func, &names),
"func @combine(ptr, ptr), linkage(external) {\n\
block0(%0: ptr, %1: ptr):\n \
%2 = alloca, size 112, align 8\n \
restrict_enter %2, size 112, align 8, restrict(1, 2)\n \
check_restrict_read %1, size 4, align 4, restrict(1, 2)\n \
%3 = load.i32 %1, align 4, restrict(1, 2)\n \
check_restrict_write %0, size 4, align 4, restrict(1, 1)\n \
store %3 -> %0, align 4, restrict(1, 1)\n \
restrict_leave %2\n \
return\n\
}\n"
);
if let Err(errors) = verify_func(&unit, &func, &names) {
panic!("that was expected to be believed: {errors:#?}");
}
}
#[test]
fn a_function_with_no_restrict_pointers_pays_nothing() {
let mut names = Interner::new();
let unit = module(&mut names, "plain.c");
let mut func = combine(&mut names, Restrict::NONE, Restrict::NONE);
let before = print_func(&unit, &func, &names);
assert_eq!(promise(&mut func, 8), Kept::default());
assert_eq!(print_func(&unit, &func, &names), before);
}
#[test]
fn an_access_the_names_said_nothing_about_asks_nothing() {
let mut names = Interner::new();
let unit = module(&mut names, "untraced.c");
let mut func =
combine(&mut names, Restrict { clique: 1, base: 0 }, Restrict { clique: 1, base: 2 });
assert_eq!(promise(&mut func, 8), Kept { promised: 1, scoped: 1 });
let printed = print_func(&unit, &func, &names);
assert!(printed.contains("check_restrict_read"), "{printed}");
assert!(!printed.contains("check_restrict_write"), "{printed}");
}
#[test]
fn a_block_nothing_is_accessed_through_opens_no_scope() {
let mut names = Interner::new();
let unit = module(&mut names, "passed-on.c");
let mut func =
combine(&mut names, Restrict { clique: 4, base: 0 }, Restrict { clique: 4, base: 0 });
assert_eq!(promise(&mut func, 8), Kept::default());
let printed = print_func(&unit, &func, &names);
assert!(!printed.contains("restrict_enter"), "{printed}");
}
#[test]
fn how_many_pointers_the_block_declares_is_what_the_scope_is_told() {
let mut names = Interner::new();
let unit = module(&mut names, "three.c");
let mut func =
combine(&mut names, Restrict { clique: 7, base: 3 }, Restrict { clique: 7, base: 1 });
assert_eq!(promise(&mut func, 8), Kept { promised: 2, scoped: 1 });
let printed = print_func(&unit, &func, &names);
assert!(
printed.contains("restrict_enter %2, size 112, align 8, restrict(7, 3)"),
"{printed}"
);
}
#[test]
fn the_scope_is_closed_on_every_path_that_leaves() {
let mut names = Interner::new();
let unit = module(&mut names, "two-ways.c");
let word = Type::int(32);
let mut func =
Func::new(names.intern("pick"), Signature::new().with_params(&[Type::PTR, Type::PTR]));
let entry = func.create_block();
let out = func.append_param(entry, Type::PTR);
let inp = func.append_param(entry, Type::PTR);
let yes = func.create_block();
let no = func.create_block();
let mut b = Builder::new(&mut func, entry);
let value = b.load(word, inp, through(Restrict { clique: 1, base: 2 }), Flags::default());
let zero = b.iconst(word, 0);
let taken = b.icmp(IntPred::Ne, value, zero);
b.br_if(taken, yes, &[], no, &[]);
let mut b = Builder::new(&mut func, yes);
b.store(value, out, through(Restrict { clique: 1, base: 1 }), Flags::default());
b.ret(&[]);
let mut b = Builder::new(&mut func, no);
b.ret(&[]);
assert_eq!(promise(&mut func, 8), Kept { promised: 2, scoped: 1 });
let printed = print_func(&unit, &func, &names);
assert_eq!(printed.matches("restrict_enter").count(), 1, "{printed}");
assert_eq!(printed.matches("restrict_leave").count(), 2, "{printed}");
if let Err(errors) = verify_func(&unit, &func, &names) {
panic!("that was expected to be believed: {errors:#?}");
}
}
#[test]
fn two_blocks_of_one_function_get_a_scope_each() {
let mut names = Interner::new();
let unit = module(&mut names, "two-cliques.c");
let mut func =
combine(&mut names, Restrict { clique: 2, base: 1 }, Restrict { clique: 5, base: 1 });
assert_eq!(promise(&mut func, 8), Kept { promised: 2, scoped: 2 });
let printed = print_func(&unit, &func, &names);
assert!(printed.contains("restrict(2, 1)"), "{printed}");
assert!(printed.contains("restrict(5, 1)"), "{printed}");
let leaves: Vec<&str> = printed.lines().filter(|line| line.contains("restrict_")).collect();
assert_eq!(leaves.len(), 6, "{printed}");
}
}