use rucc_ir::{Def, Extra, Flags, Func, Inst, Opcode, Value};
use crate::rules::{Piece, Subject, Table, safety};
use crate::{Analyses, Fuel, Pass, Preserved, Stats};
const REMOVED: &str = "bounds check removed, a dominating check covers the same bytes";
const REMOVED_LIVE: &str = "lifetime check removed, a dominating check covers the same storage";
const NO_FUEL: &str = "bounds check kept, the pass ran out of fuel";
const NO_FUEL_LIVE: &str = "lifetime check kept, the pass ran out of fuel";
const PAST_A_CALL: &str =
"bounds check kept, a call between it and the check that covers it might free";
const PAST_A_CALL_LIVE: &str =
"lifetime check kept, a call between it and the check that covers it might free";
const UNKNOWN_SHAPE: &str = "bounds check left alone, its pointer is not a base and a constant";
const COMPUTED_EXTENT: &str =
"bounds check left alone, how many bytes it covers is a number only the program has";
const UNKNOWN_SHAPE_LIVE: &str =
"lifetime check left alone, its pointer is not a base and a constant";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Discharge;
impl Pass for Discharge {
fn name(&self) -> &'static str {
"discharge"
}
fn describe(&self) -> &'static str {
"a bounds or lifetime check a dominating check already covered is removed"
}
fn preserves(&self) -> Preserved {
Preserved::ALL
}
fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
let mut stats = Stats::new();
let Some(entry) = func.entry() else { return stats };
let dom = an.dominators(func).clone();
let mut going: Vec<(Inst, &'static str)> = Vec::new();
let mut work = vec![(entry, Scope::default())];
while let Some((block, mut scope)) = work.pop() {
for inst in func.insts(block).collect::<Vec<Inst>>() {
if opaque(func, inst) {
scope.forget();
continue;
}
match func[inst].opcode {
Opcode::CheckBounds => {
if func[func[inst].args].len() > 2 {
stats.missed(COMPUTED_EXTENT);
continue;
}
let Some(asked) = about(func, inst) else {
stats.missed(UNKNOWN_SHAPE);
continue;
};
if !scope.bounds.covers(&asked) {
if scope.bounds.covered_before(&asked) {
stats.missed(PAST_A_CALL);
}
scope.bounds.held.push(asked);
continue;
}
if !fuel.take() {
stats.missed(NO_FUEL);
scope.bounds.held.push(asked);
continue;
}
going.push((inst, REMOVED));
}
Opcode::CheckLive => {
let Some(asked) = alive(func, inst) else {
stats.missed(UNKNOWN_SHAPE_LIVE);
continue;
};
if !scope.alive.covers(&asked) {
if scope.alive.covered_before(&asked) {
stats.missed(PAST_A_CALL_LIVE);
}
scope.alive.held.push(widened(&scope.bounds, asked));
continue;
}
if !fuel.take() {
stats.missed(NO_FUEL_LIVE);
scope.alive.held.push(widened(&scope.bounds, asked));
continue;
}
going.push((inst, REMOVED_LIVE));
}
_ => continue,
}
}
for child in dom.children(block) {
work.push((child, scope.clone()));
}
}
for (inst, why) in going {
func.remove_inst(inst);
stats.optimized(why);
}
stats
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Fact {
base: Value,
offset: i128,
size: i128,
}
#[derive(Debug, Clone, Default)]
struct Known {
held: Vec<Fact>,
lost: Vec<Fact>,
}
impl Known {
fn covers(&self, asked: &Fact) -> bool {
self.held.iter().any(|fact| covers(fact, asked))
}
fn covered_before(&self, asked: &Fact) -> bool {
self.lost.iter().any(|fact| covers(fact, asked))
}
fn forget(&mut self) {
self.lost.append(&mut self.held);
}
}
#[derive(Debug, Clone, Default)]
struct Scope {
bounds: Known,
alive: Known,
}
impl Scope {
fn forget(&mut self) {
self.bounds.forget();
self.alive.forget();
}
}
fn opaque(func: &Func, inst: Inst) -> bool {
match func[inst].opcode {
Opcode::Call | Opcode::CallIndirect | Opcode::TailCall => {
!func[inst].flags.contains(Flags::NOFREE)
}
Opcode::InlineAsm | Opcode::MetaEnd | Opcode::MetaTransfer => true,
_ => false,
}
}
fn about(func: &Func, check: Inst) -> Option<Fact> {
let (base, offset) = addressed(func, check)?;
let Extra::Mem(info) = func[check].extra else { return None };
Some(Fact { base, offset, size: i128::from(func[info].size) })
}
fn alive(func: &Func, check: Inst) -> Option<Fact> {
let (base, offset) = addressed(func, check)?;
Some(Fact { base, offset, size: 1 })
}
fn addressed(func: &Func, check: Inst) -> Option<(Value, i128)> {
let args = &func[func[check].args];
let &capability = args.first()?;
let &pointer = args.get(1)?;
if operand_of(func, capability, Opcode::CapOf, 0) != Some(pointer) {
return None;
}
Some(normal(func, pointer))
}
fn widened(bounds: &Known, asked: Fact) -> Fact {
bounds.held.iter().find(|fact| covers(fact, &asked)).copied().unwrap_or(asked)
}
fn normal(func: &Func, value: Value) -> (Value, i128) {
let mut base = value;
let mut offset: i128 = 0;
while let Some((from, step)) = walked(func, base) {
let Some(sum) = offset.checked_add(step) else { break };
base = from;
offset = sum;
}
(base, offset)
}
fn walked(func: &Func, value: Value) -> Option<(Value, i128)> {
let from = operand_of(func, value, Opcode::PtrAdd, 0)?;
let by = operand_of(func, value, Opcode::PtrAdd, 1)?;
Some((from, constant(func, by)?))
}
pub(crate) fn operand_of(func: &Func, value: Value, opcode: Opcode, index: usize) -> Option<Value> {
let Def::Result { inst, .. } = func[value].def else { return None };
if func[inst].opcode != opcode {
return None;
}
func[func[inst].args].get(index).copied()
}
fn constant(func: &Func, value: Value) -> Option<i128> {
let Def::Result { inst, .. } = func[value].def else { return None };
if func[inst].opcode != Opcode::IConst {
return None;
}
let Extra::Imm(imm) = func[inst].extra else { return None };
let ty = func[value].ty;
ty.is_int().then(|| func[imm].signed(ty))
}
fn covers(fact: &Fact, asked: &Fact) -> bool {
if fact.base != asked.base {
return false;
}
let Some(delta) = asked.offset.checked_sub(fact.offset) else { return false };
let mut question = Question::default();
let at = question.opaque();
let at = question.app("value.i64", &[at]);
let span = question.number(fact.size);
let span = question.app("iconst.i64", &[span]);
let far = question.number(delta);
let far = question.app("iconst.i64", &[far]);
let reach = question.number(asked.size);
let reach = question.app("iconst.i64", &[reach]);
let term = question.app("covered.i64", &[at, span, far, reach]);
match safety::TABLE.find(&question, term) {
Some(found) => yes(&safety::TABLE, found.rule),
None => false,
}
}
pub(crate) fn yes(table: &Table, rule: usize) -> bool {
matches!(table.rules[rule].replacement, [Piece::App { .. }, Piece::Int(1)])
}
#[derive(Debug, Default)]
pub(crate) struct Question {
held: Vec<Held>,
}
#[derive(Debug)]
enum Held {
Int(i128),
App(&'static str, Vec<usize>),
Opaque,
}
impl Question {
pub(crate) fn number(&mut self, value: i128) -> usize {
self.held.push(Held::Int(value));
self.held.len() - 1
}
pub(crate) fn app(&mut self, head: &'static str, args: &[usize]) -> usize {
self.held.push(Held::App(head, args.to_vec()));
self.held.len() - 1
}
pub(crate) fn opaque(&mut self) -> usize {
self.held.push(Held::Opaque);
self.held.len() - 1
}
}
impl Subject for Question {
type Node = usize;
fn head(&self, node: usize) -> Option<(&str, usize)> {
match &self.held[node] {
Held::App(head, args) => Some((head, args.len())),
Held::Int(_) | Held::Opaque => None,
}
}
fn arg(&self, node: usize, index: usize) -> usize {
match &self.held[node] {
Held::App(_, args) => args[index],
Held::Int(_) | Held::Opaque => unreachable!("only an application has arguments"),
}
}
fn int(&self, node: usize) -> Option<i128> {
match self.held[node] {
Held::Int(value) => Some(value),
Held::App(..) | Held::Opaque => None,
}
}
fn same(&self, a: usize, b: usize) -> bool {
a == b
}
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{
AsmInfo, Block, BlockCallList, Builder, Extra, Flags, Func, InstData, MemInfo, MemOrder,
Opcode, Restrict, Signature, Type, Value,
};
use super::{Discharge, Fact};
use crate::stats::Kind;
use crate::{Analyses, Fuel, Pass};
fn blank() -> (Interner, Func, Block, Value) {
let mut names = Interner::new();
let name = names.intern("f");
let mut func = Func::new(name, Signature::new().with_params(&[Type::PTR]));
let block = func.create_block();
let pointer = func.append_param(block, Type::PTR);
(names, func, block, pointer)
}
fn check(build: &mut Builder<'_>, pointer: Value, size: u64) {
let args = build.func().push_values(&[pointer]);
let capability = build.value(InstData { args, ..InstData::new(Opcode::CapOf) }, Type::CAP);
let info = MemInfo {
size,
align: 1,
order: MemOrder::NotAtomic,
tbaa: None,
restrict: Restrict::NONE,
};
let args = build.func().push_values(&[capability, pointer]);
let extra = Extra::Mem(build.func().add_mem(info));
build.inst(InstData { args, extra, ..InstData::new(Opcode::CheckBounds) }, &[]);
}
fn live(build: &mut Builder<'_>, pointer: Value) {
let args = build.func().push_values(&[pointer]);
let capability = build.value(InstData { args, ..InstData::new(Opcode::CapOf) }, Type::CAP);
let args = build.func().push_values(&[capability, pointer]);
build.inst(InstData { args, ..InstData::new(Opcode::CheckLive) }, &[]);
}
fn access(build: &mut Builder<'_>, pointer: Value, size: u64) {
check(build, pointer, size);
live(build, pointer);
}
fn past(build: &mut Builder<'_>, pointer: Value, bytes: i128) -> Value {
let offset = build.iconst(Type::int(64), bytes);
let args = build.func().push_values(&[pointer, offset]);
build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR)
}
fn checks(func: &Func) -> usize {
func.blocks()
.flat_map(|block| func.insts(block).collect::<Vec<_>>())
.filter(|&inst| func[inst].opcode == Opcode::CheckBounds)
.count()
}
fn lives(func: &Func) -> usize {
func.blocks()
.flat_map(|block| func.insts(block).collect::<Vec<_>>())
.filter(|&inst| func[inst].opcode == Opcode::CheckLive)
.count()
}
fn run(func: &mut Func) -> crate::Stats {
Discharge.run(func, &mut Analyses::new(), &mut Fuel::unlimited())
}
#[test]
fn a_second_check_of_the_same_bytes_goes() {
let (_, mut func, block, pointer) = blank();
let mut build = Builder::new(&mut func, block);
check(&mut build, pointer, 4);
check(&mut build, pointer, 4);
build.ret(&[]);
let stats = run(&mut func);
assert_eq!(checks(&func), 1);
assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 1);
}
#[test]
fn a_check_over_a_length_the_program_worked_out_is_not_this_pass_to_read() {
let (_, mut func, block, pointer) = blank();
let mut build = Builder::new(&mut func, block);
check(&mut build, pointer, 4);
let args = build.func().push_values(&[pointer]);
let capability = build.value(InstData { args, ..InstData::new(Opcode::CapOf) }, Type::CAP);
let bytes = build.iconst(Type::int(64), 4);
let info = MemInfo {
size: 4,
align: 1,
order: MemOrder::NotAtomic,
tbaa: None,
restrict: Restrict::NONE,
};
let extra = Extra::Mem(build.func().add_mem(info));
let args = build.func().push_values(&[capability, pointer, bytes]);
build.inst(InstData { args, extra, ..InstData::new(Opcode::CheckBounds) }, &[]);
build.ret(&[]);
let stats = run(&mut func);
assert_eq!(checks(&func), 2, "the second one stays");
assert_eq!(stats.count(Kind::Missed, super::COMPUTED_EXTENT), 1);
}
#[test]
fn a_check_of_bytes_inside_a_checked_range_goes() {
let (_, mut func, block, pointer) = blank();
let mut build = Builder::new(&mut func, block);
check(&mut build, pointer, 16);
let field = past(&mut build, pointer, 4);
check(&mut build, field, 4);
build.ret(&[]);
run(&mut func);
assert_eq!(checks(&func), 1);
}
#[test]
fn a_check_of_bytes_past_the_end_of_a_checked_range_stays() {
let (_, mut func, block, pointer) = blank();
let mut build = Builder::new(&mut func, block);
check(&mut build, pointer, 16);
let over = past(&mut build, pointer, 14);
check(&mut build, over, 4);
build.ret(&[]);
assert!(!run(&mut func).changed());
assert_eq!(checks(&func), 2);
}
#[test]
fn a_check_of_bytes_before_a_checked_range_stays() {
let (_, mut func, block, pointer) = blank();
let mut build = Builder::new(&mut func, block);
check(&mut build, pointer, 16);
let under = past(&mut build, pointer, -4);
check(&mut build, under, 4);
build.ret(&[]);
assert!(!run(&mut func).changed());
assert_eq!(checks(&func), 2);
}
#[test]
fn a_check_through_a_pointer_nothing_relates_to_the_first_stays() {
let mut names = Interner::new();
let name = names.intern("two");
let mut func = Func::new(name, Signature::new().with_params(&[Type::PTR, Type::PTR]));
let block = func.create_block();
let one = func.append_param(block, Type::PTR);
let other = func.append_param(block, Type::PTR);
let mut build = Builder::new(&mut func, block);
check(&mut build, one, 16);
check(&mut build, other, 4);
build.ret(&[]);
assert!(!run(&mut func).changed());
assert_eq!(checks(&func), 2);
}
#[test]
fn a_check_a_call_stands_between_stays_and_is_counted() {
let (mut names, mut func, block, pointer) = blank();
let mut build = Builder::new(&mut func, block);
check(&mut build, pointer, 16);
let callee = names.intern("might_free");
let signature = build.func().add_signature(Signature::new());
build.call(callee, signature, &[]);
check(&mut build, pointer, 4);
build.ret(&[]);
let stats = run(&mut func);
assert!(!stats.changed());
assert_eq!(checks(&func), 2);
assert_eq!(stats.count(Kind::Missed, super::PAST_A_CALL), 1);
}
#[test]
fn a_check_a_call_that_cannot_free_stands_between_goes() {
let (mut names, mut func, block, pointer) = blank();
let mut build = Builder::new(&mut func, block);
check(&mut build, pointer, 16);
let callee = names.intern("counts_them");
let signature = build.func().add_signature(Signature::new());
let call = build.call(callee, signature, &[]);
check(&mut build, pointer, 4);
build.ret(&[]);
func[call].flags |= Flags::NOFREE;
let stats = run(&mut func);
assert_eq!(checks(&func), 1);
assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 1);
assert_eq!(stats.count(Kind::Missed, super::PAST_A_CALL), 0);
}
#[test]
fn inline_assembly_throws_the_facts_away_whatever_it_is_flagged() {
let (mut names, mut func, block, pointer) = blank();
let mut build = Builder::new(&mut func, block);
check(&mut build, pointer, 16);
build.inline_asm(
AsmInfo {
template: names.intern("nop"),
constraints: names.intern(""),
clobbers: names.intern(""),
targets: BlockCallList::EMPTY,
},
&[],
&[],
Flags::NONE,
);
check(&mut build, pointer, 4);
build.ret(&[]);
let stats = run(&mut func);
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, super::PAST_A_CALL), 1);
}
#[test]
fn a_check_that_only_one_path_covers_stays() {
let (_, mut func, block, pointer) = blank();
let arm = func.create_block();
let join = func.create_block();
let mut build = Builder::new(&mut func, block);
let condition = build.iconst(Type::int(32), 1);
build.br_if(condition, arm, &[], join, &[]);
let mut build = Builder::new(&mut func, arm);
check(&mut build, pointer, 16);
build.jump(join, &[]);
let mut build = Builder::new(&mut func, join);
check(&mut build, pointer, 4);
build.ret(&[]);
assert!(!run(&mut func).changed());
assert_eq!(checks(&func), 2);
}
#[test]
fn a_check_a_dominating_block_covers_goes() {
let (_, mut func, block, pointer) = blank();
let after = func.create_block();
let mut build = Builder::new(&mut func, block);
check(&mut build, pointer, 16);
build.jump(after, &[]);
let mut build = Builder::new(&mut func, after);
let field = past(&mut build, pointer, 8);
check(&mut build, field, 8);
build.ret(&[]);
run(&mut func);
assert_eq!(checks(&func), 1);
}
#[test]
fn fuel_stops_the_removing_and_not_the_looking() {
let (_, mut func, block, pointer) = blank();
let mut build = Builder::new(&mut func, block);
check(&mut build, pointer, 4);
check(&mut build, pointer, 4);
check(&mut build, pointer, 4);
build.ret(&[]);
let mut fuel = Fuel::of(1);
let stats = Discharge.run(&mut func, &mut Analyses::new(), &mut fuel);
assert_eq!(checks(&func), 2);
assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 1);
assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
}
#[test]
fn a_second_lifetime_check_of_the_same_address_goes() {
let (_, mut func, block, pointer) = blank();
let mut build = Builder::new(&mut func, block);
live(&mut build, pointer);
live(&mut build, pointer);
build.ret(&[]);
let stats = run(&mut func);
assert_eq!(lives(&func), 1);
assert_eq!(stats.count(Kind::Optimized, super::REMOVED_LIVE), 1);
}
#[test]
fn a_lifetime_check_inside_a_checked_range_goes() {
let (_, mut func, block, pointer) = blank();
let mut build = Builder::new(&mut func, block);
access(&mut build, pointer, 16);
let field = past(&mut build, pointer, 4);
access(&mut build, field, 4);
build.ret(&[]);
let stats = run(&mut func);
assert_eq!(checks(&func), 1);
assert_eq!(lives(&func), 1);
assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 1);
assert_eq!(stats.count(Kind::Optimized, super::REMOVED_LIVE), 1);
}
#[test]
fn a_lifetime_check_outside_every_checked_range_stays() {
let (_, mut func, block, pointer) = blank();
let mut build = Builder::new(&mut func, block);
access(&mut build, pointer, 16);
let over = past(&mut build, pointer, 20);
live(&mut build, over);
build.ret(&[]);
assert!(!run(&mut func).changed());
assert_eq!(lives(&func), 2);
}
#[test]
fn a_lifetime_check_with_no_range_around_it_does_not_widen() {
let (_, mut func, block, pointer) = blank();
let mut build = Builder::new(&mut func, block);
live(&mut build, pointer);
let field = past(&mut build, pointer, 4);
live(&mut build, field);
build.ret(&[]);
assert!(!run(&mut func).changed());
assert_eq!(lives(&func), 2);
}
#[test]
fn a_lifetime_check_a_call_stands_between_stays_and_is_counted() {
let (mut names, mut func, block, pointer) = blank();
let mut build = Builder::new(&mut func, block);
access(&mut build, pointer, 16);
let callee = names.intern("might_free");
let signature = build.func().add_signature(Signature::new());
build.call(callee, signature, &[]);
let field = past(&mut build, pointer, 4);
live(&mut build, field);
build.ret(&[]);
let stats = run(&mut func);
assert!(!stats.changed());
assert_eq!(lives(&func), 2);
assert_eq!(stats.count(Kind::Missed, super::PAST_A_CALL_LIVE), 1);
}
#[test]
fn a_lifetime_check_a_call_that_cannot_free_stands_between_goes() {
let (mut names, mut func, block, pointer) = blank();
let mut build = Builder::new(&mut func, block);
access(&mut build, pointer, 16);
let callee = names.intern("counts_them");
let signature = build.func().add_signature(Signature::new());
let call = build.call(callee, signature, &[]);
let field = past(&mut build, pointer, 4);
live(&mut build, field);
build.ret(&[]);
func[call].flags |= Flags::NOFREE;
let stats = run(&mut func);
assert_eq!(lives(&func), 1);
assert_eq!(stats.count(Kind::Optimized, super::REMOVED_LIVE), 1);
}
#[test]
fn ending_a_lifetime_throws_the_facts_away() {
let (_, mut func, block, pointer) = blank();
let mut build = Builder::new(&mut func, block);
access(&mut build, pointer, 16);
let size = build.iconst(Type::int(64), 16);
let args = build.func().push_values(&[pointer, size]);
build.inst(InstData { args, ..InstData::new(Opcode::MetaEnd) }, &[]);
access(&mut build, pointer, 16);
build.ret(&[]);
let stats = run(&mut func);
assert!(!stats.changed());
assert_eq!(checks(&func), 2);
assert_eq!(lives(&func), 2);
assert_eq!(stats.count(Kind::Missed, super::PAST_A_CALL), 1);
assert_eq!(stats.count(Kind::Missed, super::PAST_A_CALL_LIVE), 1);
}
#[test]
fn fuel_runs_out_over_both_kinds_of_check() {
let (_, mut func, block, pointer) = blank();
let mut build = Builder::new(&mut func, block);
access(&mut build, pointer, 16);
access(&mut build, pointer, 4);
build.ret(&[]);
let mut fuel = Fuel::of(1);
let stats = Discharge.run(&mut func, &mut Analyses::new(), &mut fuel);
assert_eq!(checks(&func), 1);
assert_eq!(lives(&func), 2);
assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 1);
assert_eq!(stats.count(Kind::Missed, super::NO_FUEL_LIVE), 1);
}
#[test]
fn a_distance_too_large_to_be_a_real_access_is_not_discharged() {
let huge = i128::from(u64::MAX) * 4;
let fact = Fact { base: Value::new(0), offset: 0, size: huge };
let asked = Fact { base: Value::new(0), offset: huge / 2, size: 4 };
assert!(!super::covers(&fact, &asked));
}
}