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) {
if !func[inst].opcode.makes_capability() {
continue;
}
let Some(result) = func[inst].results().next() else { continue };
let Some(address) = reserve(func, inst) else { continue };
moved.insert(result, address);
}
if moved.is_empty() {
return;
}
substitute(func, &moved);
for inst in walk(func) {
let slot = func[inst].results().next().and_then(|value| moved.get(&value).copied());
match (func[inst].opcode, slot) {
(Opcode::CapNull, Some(address)) => nulled(func, word, inst, address),
(Opcode::CapOf, Some(address)) => allocated(func, names, inst, address),
(Opcode::CapLoad, Some(address)) => read(func, names, inst, address),
(Opcode::CapNarrow, Some(address)) => narrowed(func, names, word, inst, address),
(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 placed = matches!(opcode, Opcode::CapNull | Opcode::CapLoad | Opcode::CapNarrow);
if opcode.makes_capability() && !placed && fresh(func, inst).is_none() {
return false;
}
let reads = func[func[inst].args].iter().any(|&value| func[value].ty.is_cap());
if reads && !matches!(opcode, Opcode::CapStore | Opcode::CapLoad | Opcode::CapNarrow) {
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, address: Value) {
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);
}
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, address: Value) {
let Some(base) = fresh(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);
func.remove_inst(inst);
}
fn read(func: &mut Func, names: &mut Interner, inst: Inst, address: Value) {
let mut args = vec![address];
args.extend_from_slice(&func[func[inst].args]);
let data = crate::lower::calling(func, names, "__rucc_cap_load", &[Type::PTR; 4], &[], &args);
let made = func.create_inst(data, &[], func.span(inst));
func.insert_before(made, inst);
func.remove_inst(inst);
}
fn narrowed(func: &mut Func, names: &mut Interner, word: Type, inst: Inst, address: Value) {
let [base, off, len] = func[func[inst].args] else { return };
let off = crate::lower::fitted(func, inst, off, word);
let len = crate::lower::fitted(func, inst, len, word);
let params = &[Type::PTR, Type::PTR, word, word];
let args = &[address, base, off, len];
let data = crate::lower::calling(func, names, "__rucc_cap_narrow", params, &[], args);
let made = func.create_inst(data, &[], func.span(inst));
func.insert_before(made, inst);
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);
}
fn number(b: &mut Builder<'_>, value: i128, ty: Type) -> Value {
let imm = b.func().add_imm(Imm::int(value, ty));
b.value(InstData { extra: Extra::Imm(imm), ..InstData::new(Opcode::IConst) }, ty)
}
fn slot(func: &Func, value: Value) -> bool {
matches!(func[value].def, Def::Result { inst, .. } if func[inst].opcode == Opcode::Alloca)
}
#[test]
fn a_capability_read_out_of_memory_is_one_call_with_both_slots_in_hand() {
let mut names = Interner::new();
let mut func = built(&mut names, |b, cap, at| {
let args = b.func().push_values(&[cap, at, at]);
let got = b.value(InstData { args, ..InstData::new(Opcode::CapLoad) }, Type::CAP);
let args = b.func().push_values(&[got, at, at, got]);
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), 4);
assert_eq!(count(&func, Opcode::CapLoad), 0);
assert!(!any_capability(&func));
let call = walk(&func)
.into_iter()
.find(|&inst| func[inst].opcode == Opcode::Call)
.expect("the read became a call");
let args: Vec<Value> = func[func[call].args].to_vec();
assert_eq!(args.len(), 4);
assert_ne!(args[0], args[1]);
assert!(slot(&func, args[0]));
assert!(slot(&func, args[1]));
let unit = module(&mut names);
let text = print_func(&unit, &func, &names);
assert!(text.contains("__rucc_cap_load"), "{text}");
assert!(text.contains("__rucc_cap_store"), "{text}");
believed(&unit, &func, &names);
}
#[test]
fn a_capability_read_beside_one_this_pass_cannot_place_is_left_where_it_was() {
let mut names = Interner::new();
let mut func = built(&mut names, |b, _, 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]);
let got = b.value(InstData { args, ..InstData::new(Opcode::CapLoad) }, Type::CAP);
let args = b.func().push_values(&[got, at, at, got]);
b.inst(InstData { args, ..InstData::new(Opcode::CapStore) }, &[]);
});
frames(&mut func, &mut names, Type::int(64));
assert_eq!(count(&func, Opcode::CapLoad), 1);
assert_eq!(count(&func, Opcode::CapOf), 1);
assert_eq!(count(&func, Opcode::Alloca), 0);
believed(&module(&mut names), &func, &names);
}
#[test]
fn a_capability_read_nobody_looks_at_is_taken_out_with_the_one_it_read_from() {
let mut names = Interner::new();
let mut func = built(&mut names, |b, cap, at| {
let args = b.func().push_values(&[cap, at, at]);
b.value(InstData { args, ..InstData::new(Opcode::CapLoad) }, Type::CAP);
});
frames(&mut func, &mut names, Type::int(64));
assert_eq!(count(&func, Opcode::CapLoad), 0);
assert_eq!(count(&func, Opcode::CapNull), 0);
assert_eq!(count(&func, Opcode::Alloca), 0);
believed(&module(&mut names), &func, &names);
}
#[test]
fn a_narrowed_capability_is_one_call_with_the_two_numbers_in_the_targets_width() {
let mut names = Interner::new();
let word = Type::int(64);
let narrow = Type::int(32);
let mut func = built(&mut names, |b, cap, at| {
let off = number(b, 16, narrow);
let len = number(b, 8, narrow);
let args = b.func().push_values(&[cap, off, len]);
let member = b.value(InstData { args, ..InstData::new(Opcode::CapNarrow) }, Type::CAP);
let args = b.func().push_values(&[member, at, at, member]);
b.inst(InstData { args, ..InstData::new(Opcode::CapStore) }, &[]);
});
frames(&mut func, &mut names, word);
assert_eq!(count(&func, Opcode::Alloca), 2);
assert_eq!(count(&func, Opcode::CapNarrow), 0);
assert_eq!(count(&func, Opcode::ZExt), 2);
assert!(!any_capability(&func));
let call = walk(&func)
.into_iter()
.find(|&inst| func[inst].opcode == Opcode::Call)
.expect("the narrowing became a call");
let args: Vec<Value> = func[func[call].args].to_vec();
assert_eq!(args.len(), 4);
assert!(slot(&func, args[0]));
assert!(slot(&func, args[1]));
assert_eq!(func[args[2]].ty, word);
assert_eq!(func[args[3]].ty, word);
let unit = module(&mut names);
let text = print_func(&unit, &func, &names);
assert!(text.contains("__rucc_cap_narrow"), "{text}");
believed(&unit, &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);
}
}