use std::collections::{HashMap, HashSet};
use rucc_base::Interner;
use rucc_ir::{
Block, Def, Extra, Flags, Func, Imm, Inst, InstData, MemInfo, MemOrder, Opcode, Restrict, Type,
Value,
};
pub const BYTES: u64 = 32;
pub const ALIGN: u32 = 8;
const WORD: u64 = 8;
pub fn frames(func: &mut Func, names: &mut Interner, word: Type) {
prune(func);
if !placeable(func) {
return;
}
let mut moved: HashMap<Value, Value> = HashMap::new();
for inst in walk(func) {
match func[inst].opcode {
Opcode::CapNull => nulled(func, word, inst, &mut moved),
Opcode::CapOf => allocated(func, names, inst, &mut moved),
_ => {}
}
}
if moved.is_empty() {
return;
}
substitute(func, &moved);
for inst in walk(func) {
if func[inst].opcode == Opcode::CapStore {
stored(func, names, inst);
}
}
}
fn walk(func: &Func) -> Vec<Inst> {
func.blocks()
.collect::<Vec<Block>>()
.into_iter()
.flat_map(|block| func.insts(block).collect::<Vec<Inst>>())
.collect()
}
fn prune(func: &mut Func) {
loop {
let mut read: HashSet<Value> = HashSet::new();
for inst in walk(func) {
operands(func, inst, |value| {
read.insert(value);
});
}
let mut again = false;
for inst in walk(func) {
if !func[inst].opcode.makes_capability() {
continue;
}
if func[inst].results().any(|value| read.contains(&value)) {
continue;
}
func.remove_inst(inst);
again = true;
}
if !again {
return;
}
}
}
fn placeable(func: &Func) -> bool {
for inst in walk(func) {
let opcode = func[inst].opcode;
let known = opcode == Opcode::CapNull || fresh(func, inst).is_some();
if opcode.makes_capability() && !known {
return false;
}
let reads = func[func[inst].args].iter().any(|&value| func[value].ty.is_cap());
if reads && opcode != Opcode::CapStore {
return false;
}
for call in func.successors(inst) {
if func[call.args].iter().any(|&value| func[value].ty.is_cap()) {
return false;
}
}
}
true
}
fn operands(func: &Func, inst: Inst, mut each: impl FnMut(Value)) {
for &value in &func[func[inst].args] {
each(value);
}
for call in func.successors(inst) {
for &value in &func[call.args] {
each(value);
}
}
}
fn substitute(func: &mut Func, moved: &HashMap<Value, Value>) {
let with = |value: Value| moved.get(&value).copied().unwrap_or(value);
for inst in walk(func) {
let args = func[inst].args;
func.rewrite(args, with);
for call in func.successors(inst).collect::<Vec<_>>() {
func.rewrite(call.args, with);
}
}
}
fn nulled(func: &mut Func, word: Type, inst: Inst, moved: &mut HashMap<Value, Value>) {
let Some(result) = func[inst].results().next() else { return };
let Some(address) = reserve(func, inst) else { return };
let span = func.span(inst);
let zero = konst(func, inst, Imm::int(0, word), word);
for step in 0..BYTES / WORD {
let at = offset(func, inst, address, step * WORD, word);
let info = MemInfo {
size: WORD,
align: ALIGN,
order: MemOrder::NotAtomic,
tbaa: None,
owns: 0,
restrict: Restrict::NONE,
};
let extra = Extra::Mem(func.add_mem(info));
let args = func.push_values(&[zero, at]);
let data = InstData { args, extra, ..InstData::new(Opcode::Store) };
let made = func.create_inst(data, &[], span);
func.insert_before(made, inst);
}
moved.insert(result, address);
func.remove_inst(inst);
}
fn fresh(func: &Func, inst: Inst) -> Option<Value> {
if func[inst].opcode != Opcode::CapOf {
return None;
}
let &[base] = &func[func[inst].args] else { return None };
let Def::Result { inst: call, index: 0 } = func[base].def else { return None };
(func[call].opcode == Opcode::Call && func[call].flags.contains(Flags::HEAP)).then_some(base)
}
fn allocated(func: &mut Func, names: &mut Interner, inst: Inst, moved: &mut HashMap<Value, Value>) {
let Some(base) = fresh(func, inst) else { return };
let Some(result) = func[inst].results().next() else { return };
let Some(address) = reserve(func, inst) else { return };
let params = &[Type::PTR; 2];
let args = &[address, base];
let data = crate::lower::calling(func, names, "__rucc_cap_made", params, &[], args);
let made = func.create_inst(data, &[], func.span(inst));
func.insert_before(made, inst);
moved.insert(result, address);
func.remove_inst(inst);
}
fn stored(func: &mut Func, names: &mut Interner, inst: Inst) {
let args: Vec<Value> = func[func[inst].args].to_vec();
crate::lower::call(func, names, inst, "__rucc_cap_store", &[Type::PTR; 4], &[], &args);
}
fn reserve(func: &mut Func, inst: Inst) -> Option<Value> {
let entry = func.entry()?;
let first = func.insts(entry).next()?;
let info = MemInfo {
size: BYTES,
align: ALIGN,
order: MemOrder::NotAtomic,
tbaa: None,
owns: 0,
restrict: Restrict::NONE,
};
let extra = Extra::Mem(func.add_mem(info));
let data = InstData { extra, ..InstData::new(Opcode::Alloca) };
let slot = func.create_inst(data, &[Type::PTR], func.span(inst));
func.insert_before(slot, first);
func[slot].results().next()
}
fn offset(func: &mut Func, inst: Inst, address: Value, bytes: u64, word: Type) -> Value {
if bytes == 0 {
return address;
}
let step = konst(func, inst, Imm::int(i128::from(bytes), word), word);
let args = func.push_values(&[address, step]);
let data = InstData { args, ..InstData::new(Opcode::PtrAdd) };
let made = func.create_inst(data, &[Type::PTR], func.span(inst));
func.insert_before(made, inst);
func[made].results().next().expect("an address created with one result has one")
}
fn konst(func: &mut Func, inst: Inst, imm: Imm, ty: Type) -> Value {
let extra = Extra::Imm(func.add_imm(imm));
let data = InstData { extra, ..InstData::new(Opcode::IConst) };
let made = func.create_inst(data, &[ty], func.span(inst));
func.insert_before(made, inst);
func[made].results().next().expect("a constant created with one result has one")
}
#[cfg(test)]
mod tests {
use rucc_ir::{Builder, CallInfo, Module, Signature, print_func, verify_func};
use rucc_target::{Arch, Env, Os, TargetInfo, Triple};
use super::*;
fn module(names: &mut Interner) -> Module {
let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
Module::new(names.intern("f.c"), &target)
}
fn believed(unit: &Module, func: &Func, names: &Interner) {
if let Err(errors) = verify_func(unit, func, names) {
panic!("that was expected to be believed: {errors:#?}");
}
}
fn built(names: &mut Interner, extra: impl FnOnce(&mut Builder<'_>, Value, Value)) -> Func {
let mut func = Func::new(names.intern("f"), Signature::new().with_params(&[Type::PTR]));
let entry = func.create_block();
let at = func.append_param(entry, Type::PTR);
let mut b = Builder::new(&mut func, entry);
let cap = b.value(InstData::new(Opcode::CapNull), Type::CAP);
extra(&mut b, cap, at);
b.ret(&[]);
func
}
fn called(names: &mut Interner, vouched: bool) -> Func {
let word = Type::int(64);
let mut func =
Func::new(names.intern("f"), Signature::new().with_params(&[word, Type::PTR]));
let entry = func.create_block();
let size = func.append_param(entry, word);
let at = func.append_param(entry, Type::PTR);
let sig = Signature::new().with_params(&[word]).with_returns(&[Type::PTR]);
let sig = func.add_signature(sig);
let callee = names.intern("malloc");
let varargs = func.push_abis(&[]);
let info = func.add_call(CallInfo { callee: Some(callee), signature: sig, varargs });
let args = func.push_values(&[size]);
let flags = if vouched { Flags::HEAP } else { Flags::default() };
let mut b = Builder::new(&mut func, entry);
let data =
InstData { args, extra: Extra::Call(info), flags, ..InstData::new(Opcode::Call) };
let base = b.value(data, Type::PTR);
let args = b.func().push_values(&[base]);
let cap = b.value(InstData { args, ..InstData::new(Opcode::CapOf) }, Type::CAP);
let args = b.func().push_values(&[cap, at, at, cap]);
b.inst(InstData { args, ..InstData::new(Opcode::CapStore) }, &[]);
b.ret(&[]);
func
}
fn count(func: &Func, opcode: Opcode) -> usize {
walk(func).into_iter().filter(|&inst| func[inst].opcode == opcode).count()
}
fn any_capability(func: &Func) -> bool {
walk(func).into_iter().any(|inst| func[inst].results().any(|value| func[value].ty.is_cap()))
}
#[test]
fn a_capability_nothing_reads_is_taken_out() {
let mut names = Interner::new();
let mut func = built(&mut names, |_, _, _| {});
frames(&mut func, &mut names, Type::int(64));
assert_eq!(count(&func, Opcode::CapNull), 0);
assert_eq!(count(&func, Opcode::Alloca), 0);
believed(&module(&mut names), &func, &names);
}
#[test]
fn a_capability_something_reads_becomes_four_zero_words_of_frame() {
let mut names = Interner::new();
let mut func = built(&mut names, |b, cap, at| {
let args = b.func().push_values(&[cap, at, at, cap]);
b.inst(InstData { args, ..InstData::new(Opcode::CapStore) }, &[]);
});
frames(&mut func, &mut names, Type::int(64));
assert_eq!(count(&func, Opcode::Alloca), 1);
assert_eq!(count(&func, Opcode::Store), 4);
assert_eq!(count(&func, Opcode::CapNull), 0);
assert_eq!(count(&func, Opcode::CapStore), 0);
assert!(!any_capability(&func));
let unit = module(&mut names);
let text = print_func(&unit, &func, &names);
assert!(text.contains("__rucc_cap_store"), "{text}");
believed(&unit, &func, &names);
}
#[test]
fn each_capability_gets_a_slot_of_its_own() {
let mut names = Interner::new();
let mut func = built(&mut names, |b, cap, at| {
let other = b.value(InstData::new(Opcode::CapNull), Type::CAP);
let args = b.func().push_values(&[cap, at, at, other]);
b.inst(InstData { args, ..InstData::new(Opcode::CapStore) }, &[]);
});
frames(&mut func, &mut names, Type::int(64));
assert_eq!(count(&func, Opcode::Alloca), 2);
assert_eq!(count(&func, Opcode::Store), 8);
believed(&module(&mut names), &func, &names);
}
#[test]
fn every_slot_is_reserved_in_the_entry_block() {
let mut names = Interner::new();
let mut func = built(&mut names, |b, cap, at| {
let args = b.func().push_values(&[cap, at, at, cap]);
b.inst(InstData { args, ..InstData::new(Opcode::CapStore) }, &[]);
});
frames(&mut func, &mut names, Type::int(64));
let entry = func.entry().expect("the function has a body");
let here = func.insts(entry).filter(|&inst| func[inst].opcode == Opcode::Alloca).count();
assert_eq!(here, count(&func, Opcode::Alloca));
}
#[test]
fn a_capability_for_a_fresh_allocation_is_one_call_and_no_stores() {
let mut names = Interner::new();
let mut func = called(&mut names, true);
frames(&mut func, &mut names, Type::int(64));
assert_eq!(count(&func, Opcode::Alloca), 1);
assert_eq!(count(&func, Opcode::CapOf), 0);
assert_eq!(count(&func, Opcode::Store), 0);
assert!(!any_capability(&func));
let unit = module(&mut names);
let text = print_func(&unit, &func, &names);
assert!(text.contains("__rucc_cap_made"), "{text}");
assert!(text.contains("__rucc_cap_store"), "{text}");
believed(&unit, &func, &names);
}
#[test]
fn a_capability_for_a_pointer_nobody_vouched_for_is_left_where_it_was() {
let mut names = Interner::new();
let mut func = called(&mut names, false);
frames(&mut func, &mut names, Type::int(64));
assert_eq!(count(&func, Opcode::CapOf), 1);
assert_eq!(count(&func, Opcode::CapStore), 1);
assert_eq!(count(&func, Opcode::Alloca), 0);
believed(&module(&mut names), &func, &names);
}
#[test]
fn a_capability_this_pass_cannot_place_leaves_the_others_where_they_were() {
let mut names = Interner::new();
let mut func = built(&mut names, |b, cap, at| {
let args = b.func().push_values(&[at]);
let taken = b.value(InstData { args, ..InstData::new(Opcode::CapOf) }, Type::CAP);
let args = b.func().push_values(&[taken, at, at, cap]);
b.inst(InstData { args, ..InstData::new(Opcode::CapStore) }, &[]);
});
frames(&mut func, &mut names, Type::int(64));
assert_eq!(count(&func, Opcode::CapOf), 1);
assert_eq!(count(&func, Opcode::CapNull), 1);
assert_eq!(count(&func, Opcode::Alloca), 0);
believed(&module(&mut names), &func, &names);
}
}