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);
}
let clears = walk(func).into_iter().any(|inst| func[inst].opcode == Opcode::CapClear);
if moved.is_empty() && !clears {
return;
}
substitute(func, &moved);
let mut frame: Option<Value> = None;
let mut given: Option<Value> = None;
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::CapRecover, Some(address)) => recovered(func, names, inst, address),
(Opcode::CapStore, _) => stored(func, names, inst),
(Opcode::CapPublish, _) => {
let at = match frame {
Some(at) => at,
None => {
let Some(at) = crate::frame::reserve(func, inst) else { continue };
frame = Some(at);
at
}
};
crate::frame::hand_over(func, names, word, inst, at);
}
(Opcode::CapClear, _) => crate::frame::cleared(func, names, inst),
(Opcode::CapArg, Some(address)) => {
let at = match given {
Some(at) => at,
None => {
let Some(at) = crate::frame::taken(func, names, inst) else { continue };
given = Some(at);
at
}
};
crate::frame::argument(func, names, word, inst, address, at);
}
(Opcode::CapYield, _) => {
let at = match given {
Some(at) => at,
None => {
let Some(at) = crate::frame::taken(func, names, inst) else { continue };
given = Some(at);
at
}
};
crate::frame::yielded(func, names, inst, at);
}
(Opcode::CapResult, Some(address)) => {
let Some(at) = frame else { continue };
crate::frame::result(func, names, inst, address, at);
}
_ => {}
}
}
}
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::CapOf
| Opcode::CapLoad
| Opcode::CapNarrow
| Opcode::CapRecover
| Opcode::CapArg
| Opcode::CapResult
);
if opcode.makes_capability() && !placed {
return false;
}
let reads = func[func[inst].args].iter().any(|&value| func[value].ty.is_cap());
let consumes = matches!(
opcode,
Opcode::CapStore
| Opcode::CapLoad
| Opcode::CapNarrow
| Opcode::CapPublish
| Opcode::CapYield
);
if reads && !consumes {
return false;
}
if opcode == Opcode::CapPublish && !crate::frame::placeable(func, inst) {
return false;
}
if opcode == Opcode::CapYield && !crate::frame::leaving(func, inst) {
return false;
}
if opcode == Opcode::CapResult && !crate::frame::given(func, inst) {
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 (routine, base) = match fresh(func, inst) {
Some(base) => ("__rucc_cap_made", base),
None => {
let &[at] = &func[func[inst].args] else { return };
("__rucc_cap_recover", at)
}
};
let params = &[Type::PTR; 2];
let args = &[address, base];
let data = crate::lower::calling(func, names, routine, params, &[], args);
let made = func.create_inst(data, &[], func.span(inst));
func.insert_before(made, inst);
func.remove_inst(inst);
}
fn recovered(func: &mut Func, names: &mut Interner, inst: Inst, address: Value) {
let &[addr] = &func[func[inst].args] else { return };
let args = &[address, addr];
let data = crate::lower::calling(func, names, "__rucc_cap_recover", &[Type::PTR; 2], &[], 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()
}
pub(crate) 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")
}
pub(crate) 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_falls_back_to_the_plane_walk() {
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), 0);
assert_eq!(count(&func, Opcode::Alloca), 1);
assert!(!any_capability(&func));
let unit = module(&mut names);
let text = print_func(&unit, &func, &names);
assert!(text.contains("__rucc_cap_recover"), "{text}");
assert!(!text.contains("__rucc_cap_made"), "{text}");
believed(&unit, &func, &names);
}
#[test]
fn the_cheap_answer_is_never_asked_about_a_pointer_into_the_middle_of_something() {
let mut names = Interner::new();
let word = Type::int(64);
let mut func = built(&mut names, |b, _, at| {
let step = number(b, 16, word);
let args = b.func().push_values(&[at, step]);
let inner = b.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR);
let args = b.func().push_values(&[inner]);
let mine = b.value(InstData { args, ..InstData::new(Opcode::CapOf) }, Type::CAP);
let args = b.func().push_values(&[mine, inner, inner, mine]);
b.inst(InstData { args, ..InstData::new(Opcode::CapStore) }, &[]);
});
frames(&mut func, &mut names, word);
let unit = module(&mut names);
let text = print_func(&unit, &func, &names);
assert!(text.contains("__rucc_cap_recover"), "{text}");
assert!(!text.contains("__rucc_cap_made"), "{text}");
believed(&unit, &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]);
b.inst(InstData { args, ..InstData::new(Opcode::CapYield) }, &[]);
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_recovered_capability_is_one_call_with_the_address_it_was_asked_about() {
let mut names = Interner::new();
let mut func = built(&mut names, |b, _, at| {
let args = b.func().push_values(&[at]);
let got = b.value(InstData { args, ..InstData::new(Opcode::CapRecover) }, 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), 1);
assert_eq!(count(&func, Opcode::Store), 0);
assert_eq!(count(&func, Opcode::CapRecover), 0);
assert!(!any_capability(&func));
let call = walk(&func)
.into_iter()
.find(|&inst| func[inst].opcode == Opcode::Call)
.expect("the recovery became a call");
let args: Vec<Value> = func[func[call].args].to_vec();
assert_eq!(args.len(), 2);
assert!(slot(&func, args[0]));
assert!(matches!(func[args[1]].def, Def::Param { .. }));
let unit = module(&mut names);
let text = print_func(&unit, &func, &names);
assert!(text.contains("__rucc_cap_recover"), "{text}");
believed(&unit, &func, &names);
}
#[test]
fn a_member_of_something_recovered_is_two_slots_and_the_narrow_reads_the_first() {
let mut names = Interner::new();
let word = Type::int(64);
let mut func = built(&mut names, |b, _, at| {
let args = b.func().push_values(&[at]);
let whole = b.value(InstData { args, ..InstData::new(Opcode::CapRecover) }, Type::CAP);
let off = number(b, 16, word);
let len = number(b, 8, word);
let args = b.func().push_values(&[whole, 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::CapRecover), 0);
assert_eq!(count(&func, Opcode::CapNarrow), 0);
assert_eq!(count(&func, Opcode::ZExt), 0);
assert!(!any_capability(&func));
let calls: Vec<Inst> =
walk(&func).into_iter().filter(|&inst| func[inst].opcode == Opcode::Call).collect();
assert_eq!(calls.len(), 3);
let recovery: Vec<Value> = func[func[calls[0]].args].to_vec();
let narrowing: Vec<Value> = func[func[calls[1]].args].to_vec();
assert_eq!(narrowing[1], recovery[0]);
assert_ne!(narrowing[0], narrowing[1]);
let unit = module(&mut names);
let text = print_func(&unit, &func, &names);
assert!(text.contains("__rucc_cap_recover"), "{text}");
assert!(text.contains("__rucc_cap_narrow"), "{text}");
believed(&unit, &func, &names);
}
fn passing(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 sig = func.add_signature(Signature::new().with_params(&[Type::PTR]));
let callee = names.intern("g");
let varargs = func.push_abis(&[]);
let info = func.add_call(CallInfo { callee: Some(callee), signature: sig, varargs });
let mut b = Builder::new(&mut func, entry);
let cap = b.value(InstData::new(Opcode::CapNull), Type::CAP);
extra(&mut b, cap, at);
let args = b.func().push_values(&[at]);
let data = InstData { args, extra: Extra::Call(info), ..InstData::new(Opcode::Call) };
b.inst(data, &[]);
b.ret(&[]);
func
}
fn place(func: &Func, inst: Inst) -> usize {
walk(func).into_iter().position(|at| at == inst).expect("the instruction is in the body")
}
#[test]
fn the_capabilities_a_call_hands_over_are_copied_into_one_frame() {
let mut names = Interner::new();
let mut func = passing(&mut names, |b, cap, _| {
let args = b.func().push_values(&[cap]);
b.inst(InstData { args, ..InstData::new(Opcode::CapPublish) }, &[]);
});
frames(&mut func, &mut names, Type::int(64));
assert_eq!(count(&func, Opcode::CapPublish), 0);
assert_eq!(count(&func, Opcode::Alloca), 2);
assert_eq!(count(&func, Opcode::Store), 10);
assert_eq!(count(&func, Opcode::Memcpy), 1);
assert!(!any_capability(&func));
let unit = module(&mut names);
let text = print_func(&unit, &func, &names);
let publish = text.find("__rucc_frame_publish").expect("the frame is published");
let callee = text.find("call @g(").expect("the call is still there");
let restore = text.find("__rucc_frame_restore").expect("the outer frame is put back");
assert!(publish < callee, "{text}");
assert!(callee < restore, "{text}");
believed(&unit, &func, &names);
}
#[test]
fn the_frame_is_put_back_from_the_link_the_publish_wrote() {
let mut names = Interner::new();
let mut func = passing(&mut names, |b, cap, _| {
let args = b.func().push_values(&[cap]);
b.inst(InstData { args, ..InstData::new(Opcode::CapPublish) }, &[]);
});
frames(&mut func, &mut names, Type::int(64));
let reads: Vec<Inst> =
walk(&func).into_iter().filter(|&inst| func[inst].opcode == Opcode::Load).collect();
assert_eq!(reads.len(), 1);
let calls: Vec<Inst> =
walk(&func).into_iter().filter(|&inst| func[inst].opcode == Opcode::Call).collect();
assert_eq!(calls.len(), 3);
assert!(place(&func, calls[1]) < place(&func, reads[0]));
assert!(place(&func, reads[0]) < place(&func, calls[2]));
let read = func[reads[0]].results().next().expect("a read gives one value back");
assert_eq!(func[func[calls[2]].args].to_vec(), vec![read]);
believed(&module(&mut names), &func, &names);
}
#[test]
fn a_call_nobody_can_vouch_for_is_told_there_is_no_frame() {
let mut names = Interner::new();
let mut func = passing(&mut names, |b, _, _| {
b.inst(InstData::new(Opcode::CapClear), &[]);
});
frames(&mut func, &mut names, Type::int(64));
assert_eq!(count(&func, Opcode::CapClear), 0);
assert_eq!(count(&func, Opcode::Alloca), 0);
assert_eq!(count(&func, Opcode::CapNull), 0);
let unit = module(&mut names);
let text = print_func(&unit, &func, &names);
assert!(text.contains("__rucc_frame_clear"), "{text}");
assert!(!text.contains("__rucc_frame_publish"), "{text}");
believed(&unit, &func, &names);
}
#[test]
fn a_publish_that_is_in_front_of_nothing_leaves_the_function_alone() {
let mut names = Interner::new();
let mut func = built(&mut names, |b, cap, _| {
let args = b.func().push_values(&[cap]);
b.inst(InstData { args, ..InstData::new(Opcode::CapPublish) }, &[]);
});
frames(&mut func, &mut names, Type::int(64));
assert_eq!(count(&func, Opcode::CapPublish), 1);
assert_eq!(count(&func, Opcode::CapNull), 1);
assert_eq!(count(&func, Opcode::Alloca), 0);
believed(&module(&mut names), &func, &names);
}
#[test]
fn a_publish_describing_more_arguments_than_the_frame_holds_leaves_the_function_alone() {
let mut names = Interner::new();
let mut func = passing(&mut names, |b, cap, _| {
let caps = vec![cap; crate::frame::ARGS + 1];
let args = b.func().push_values(&caps);
b.inst(InstData { args, ..InstData::new(Opcode::CapPublish) }, &[]);
});
frames(&mut func, &mut names, Type::int(64));
assert_eq!(count(&func, Opcode::CapPublish), 1);
assert_eq!(count(&func, Opcode::Alloca), 0);
believed(&module(&mut names), &func, &names);
}
fn asking(names: &mut Interner, word: Type, count: i128) -> Func {
built(names, |b, _, at| {
for position in 0..count {
let position = b.iconst(word, position);
let args = b.func().push_values(&[at, position]);
let mine = b.value(InstData { args, ..InstData::new(Opcode::CapArg) }, Type::CAP);
let args = b.func().push_values(&[mine, at, at, mine]);
b.inst(InstData { args, ..InstData::new(Opcode::CapStore) }, &[]);
}
})
}
#[test]
fn a_pointer_parameter_gets_the_capability_its_caller_wrote_down() {
let mut names = Interner::new();
let word = Type::int(64);
let mut func = asking(&mut names, word, 1);
frames(&mut func, &mut names, word);
assert_eq!(count(&func, Opcode::CapArg), 0);
assert_eq!(count(&func, Opcode::Alloca), 1);
assert!(!any_capability(&func));
let unit = module(&mut names);
let text = print_func(&unit, &func, &names);
let take = text.find("__rucc_frame_take").expect("the frame is taken");
let ask = text.find("__rucc_frame_arg").expect("the argument is asked about");
assert!(take < ask, "{text}");
believed(&unit, &func, &names);
}
#[test]
fn the_frame_is_taken_once_however_many_arguments_are_asked_about() {
let mut names = Interner::new();
let word = Type::int(64);
let mut func = asking(&mut names, word, 3);
frames(&mut func, &mut names, word);
let unit = module(&mut names);
let text = print_func(&unit, &func, &names);
assert_eq!(text.matches("__rucc_frame_take").count(), 1, "{text}");
assert_eq!(text.matches("__rucc_frame_arg").count(), 3, "{text}");
assert_eq!(count(&func, Opcode::Alloca), 3);
believed(&unit, &func, &names);
}
#[test]
fn a_position_narrower_than_a_word_is_widened_into_one() {
let mut names = Interner::new();
let mut func = asking(&mut names, Type::int(32), 1);
frames(&mut func, &mut names, Type::int(64));
assert_eq!(count(&func, Opcode::ZExt), 1);
believed(&module(&mut names), &func, &names);
}
#[test]
fn the_capability_of_a_returned_pointer_is_left_in_the_frame_the_caller_waits_in() {
let mut names = Interner::new();
let mut func = built(&mut names, |b, cap, _| {
let args = b.func().push_values(&[cap]);
b.inst(InstData { args, ..InstData::new(Opcode::CapYield) }, &[]);
});
frames(&mut func, &mut names, Type::int(64));
assert_eq!(count(&func, Opcode::CapYield), 0);
assert_eq!(count(&func, Opcode::Alloca), 1);
assert!(!any_capability(&func));
let unit = module(&mut names);
let text = print_func(&unit, &func, &names);
let take = text.find("__rucc_frame_take").expect("the frame is taken");
let left = text.find("__rucc_frame_yield").expect("the capability is left behind");
assert!(take < left, "{text}");
believed(&unit, &func, &names);
}
#[test]
fn a_yield_that_is_not_in_front_of_a_return_leaves_the_function_alone() {
let mut names = Interner::new();
let mut func = built(&mut names, |b, cap, at| {
let args = b.func().push_values(&[cap]);
b.inst(InstData { args, ..InstData::new(Opcode::CapYield) }, &[]);
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::CapYield), 1);
assert_eq!(count(&func, Opcode::Alloca), 0);
believed(&module(&mut names), &func, &names);
}
fn returning(names: &mut Interner, vouched: bool) -> 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 sig = Signature::new().with_params(&[Type::PTR]).with_returns(&[Type::PTR]);
let sig = func.add_signature(sig);
let callee = names.intern("g");
let varargs = func.push_abis(&[]);
let info = func.add_call(CallInfo { callee: Some(callee), signature: sig, varargs });
let mut b = Builder::new(&mut func, entry);
if vouched {
let cap = b.value(InstData::new(Opcode::CapNull), Type::CAP);
let args = b.func().push_values(&[cap]);
b.inst(InstData { args, ..InstData::new(Opcode::CapPublish) }, &[]);
} else {
b.inst(InstData::new(Opcode::CapClear), &[]);
}
let args = b.func().push_values(&[at]);
let data = InstData { args, extra: Extra::Call(info), ..InstData::new(Opcode::Call) };
let base = b.value(data, Type::PTR);
let args = b.func().push_values(&[base]);
let mine = b.value(InstData { args, ..InstData::new(Opcode::CapResult) }, Type::CAP);
let args = b.func().push_values(&[mine, base, base, mine]);
b.inst(InstData { args, ..InstData::new(Opcode::CapStore) }, &[]);
b.ret(&[]);
func
}
#[test]
fn the_pointer_a_call_gave_back_is_read_out_of_the_frame_it_was_published_with() {
let mut names = Interner::new();
let mut func = returning(&mut names, true);
frames(&mut func, &mut names, Type::int(64));
assert_eq!(count(&func, Opcode::CapResult), 0);
assert_eq!(count(&func, Opcode::CapPublish), 0);
assert_eq!(count(&func, Opcode::Alloca), 3);
assert!(!any_capability(&func));
let unit = module(&mut names);
let text = print_func(&unit, &func, &names);
let publish = text.find("__rucc_frame_publish").expect("the frame is published");
let call = text.find("call @g(").expect("the call is still there");
let back = text.find("__rucc_frame_returned").expect("the answer is read back");
assert!(publish < call, "{text}");
assert!(call < back, "{text}");
assert!(!text.contains("__rucc_frame_take"), "{text}");
believed(&unit, &func, &names);
}
#[test]
fn a_result_behind_a_call_with_no_frame_leaves_the_function_alone() {
let mut names = Interner::new();
let mut func = returning(&mut names, false);
frames(&mut func, &mut names, Type::int(64));
assert_eq!(count(&func, Opcode::CapResult), 1);
assert_eq!(count(&func, Opcode::CapClear), 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]);
b.inst(InstData { args, ..InstData::new(Opcode::CapPublish) }, &[]);
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::CapOf), 1);
assert_eq!(count(&func, Opcode::CapNull), 1);
assert_eq!(count(&func, Opcode::Alloca), 0);
believed(&module(&mut names), &func, &names);
}
}