use rucc_ir::{Extra, Func, Inst, InstData, Opcode, Type, Value};
use crate::rules::safety;
use crate::{Analyses, Analysis, Fuel, Pass, Preserved, Stats};
const MERGED: &str = "plane writes merged, a run over one range became one write over all of it";
const ALONE: &str = "plane write left alone, nothing next to it writes the same plane";
const NOT_ADJACENT: &str = "plane writes left alone, the rules do not join their two ranges";
const NOT_A_NUMBER: &str = "plane write left alone, its width is not a constant";
const NO_FUEL: &str = "plane writes left alone, the pass ran out of fuel";
const LIMIT: i128 = 4_294_967_296;
#[derive(Debug)]
pub struct Coalesce;
impl Pass for Coalesce {
fn name(&self) -> &'static str {
"coalesce"
}
fn describe(&self) -> &'static str {
"writes to one plane that sit next to each other become one write over the whole range"
}
fn preserves(&self) -> Preserved {
Preserved::ALL.without(Analysis::Liveness)
}
fn run(&self, func: &mut Func, _an: &mut Analyses, fuel: &mut Fuel) -> Stats {
let mut stats = Stats::new();
let blocks: Vec<_> = func.blocks().collect();
for block in blocks {
let insts: Vec<Inst> = func.insts(block).collect();
let mut open: Vec<Run> = Vec::new();
for inst in insts {
let opcode = func[inst].opcode;
let Some(kind) = plane(opcode) else {
flush(func, &mut open, |run| !crossed(opcode, run.kind), &mut stats, fuel);
continue;
};
match read(func, inst, kind) {
Some(write) => extend(func, &mut open, write, &mut stats, fuel),
None => {
stats.missed(NOT_A_NUMBER);
flush(func, &mut open, |run| run.kind == kind, &mut stats, fuel);
}
}
}
flush(func, &mut open, |_| true, &mut stats, fuel);
}
stats
}
}
pub(crate) fn plane(opcode: Opcode) -> Option<Opcode> {
matches!(opcode, Opcode::MetaType | Opcode::MetaInit).then_some(opcode)
}
#[derive(Debug)]
pub(crate) struct Write {
pub(crate) kind: Opcode,
extra: Extra,
pub(crate) base: Value,
pub(crate) at: i128,
pub(crate) size: i128,
pointer: Value,
width: Type,
pub(crate) inst: Inst,
}
#[derive(Debug)]
struct Run {
kind: Opcode,
extra: Extra,
base: Value,
lo: i128,
hi: i128,
low: Value,
width: Type,
parts: Vec<Inst>,
}
impl Run {
fn fits(&self, write: &Write) -> bool {
self.kind == write.kind && self.base == write.base && same(self.extra, write.extra)
}
}
pub(crate) fn read(func: &Func, inst: Inst, kind: Opcode) -> Option<Write> {
let [pointer, length] = func[func[inst].args] else { return None };
let (imm, width) = crate::fold::constant(func, length)?;
let size = imm.signed(width);
if !(1..=LIMIT).contains(&size) {
return None;
}
let (base, at) = crate::discharge::normal(func, pointer);
Some(Write { kind, extra: func[inst].extra, base, at, size, pointer, width, inst })
}
fn extend(func: &mut Func, open: &mut Vec<Run>, write: Write, stats: &mut Stats, fuel: &mut Fuel) {
if let Some(run) = open.iter_mut().find(|run| run.fits(&write)) {
if write.at == run.hi && joins(run.hi - run.lo, write.size) {
run.hi = write.at + write.size;
run.parts.push(write.inst);
return;
}
if write.at + write.size == run.lo && joins(write.size, run.hi - run.lo) {
run.lo = write.at;
run.low = write.pointer;
run.parts.push(write.inst);
return;
}
stats.missed(NOT_ADJACENT);
}
flush(func, open, |run| run.kind == write.kind, stats, fuel);
open.push(Run {
kind: write.kind,
extra: write.extra,
base: write.base,
lo: write.at,
hi: write.at + write.size,
low: write.pointer,
width: write.width,
parts: vec![write.inst],
});
}
fn same(one: Extra, other: Extra) -> bool {
match (one, other) {
(Extra::Node(left), Extra::Node(right)) => left == right,
(Extra::None, Extra::None) => true,
_ => false,
}
}
fn flush(
func: &mut Func,
open: &mut Vec<Run>,
mut pick: impl FnMut(&Run) -> bool,
stats: &mut Stats,
fuel: &mut Fuel,
) {
let mut kept = Vec::with_capacity(open.len());
for run in std::mem::take(open) {
if pick(&run) {
merge(func, run, stats, fuel);
} else {
kept.push(run);
}
}
*open = kept;
}
fn merge(func: &mut Func, run: Run, stats: &mut Stats, fuel: &mut Fuel) {
let Some(&last) = run.parts.last() else { return };
if run.parts.len() < 2 {
stats.note(ALONE);
return;
}
if !fuel.take() {
stats.missed(NO_FUEL);
return;
}
let width = crate::ivopts::number(func, last, run.width, run.hi - run.lo);
let args = func.push_values(&[run.low, width]);
let data = InstData { args, extra: run.extra, ..InstData::new(run.kind) };
let span = func.span(last);
let made = func.create_inst(data, &[], span);
func.insert_before(made, last);
for part in run.parts {
func.remove_inst(part);
}
stats.optimized(MERGED);
}
fn joins(span: i128, reach: i128) -> bool {
let mut question = crate::discharge::Question::default();
let at = question.opaque();
let at = question.app("value.i64", &[at]);
let width = question.number(span);
let width = question.app("iconst.i64", &[width]);
let delta = question.number(span);
let delta = question.app("iconst.i64", &[delta]);
let reach = question.number(reach);
let reach = question.app("iconst.i64", &[reach]);
let byte = question.opaque();
let byte = question.app("value.i64", &[byte]);
let term = question.app("joined.i64", &[at, width, delta, reach, byte]);
match safety::TABLE.find(&question, term) {
Some(found) => crate::discharge::yes(&safety::TABLE, found.rule),
None => false,
}
}
pub(crate) fn crossed(opcode: Opcode, kind: Opcode) -> bool {
let other = if kind == Opcode::MetaType {
matches!(opcode, Opcode::MetaInit | Opcode::MetaInitCopy | Opcode::CheckInit)
} else {
matches!(opcode, Opcode::MetaType | Opcode::MetaTypeCopy | Opcode::CheckType)
};
other
|| !opcode.has_effects()
|| matches!(
opcode,
Opcode::Load
| Opcode::Store
| Opcode::Memcpy
| Opcode::Memmove
| Opcode::Memset
| Opcode::CheckBounds
| Opcode::CheckLive
| Opcode::CheckDeriv
| Opcode::CheckRace
| Opcode::CheckRestrictRead
| Opcode::CheckRestrictWrite
| Opcode::MetaEpoch
)
}
#[cfg(test)]
mod tests {
use rucc_base::{Idx, Interner};
use rucc_ir::{
Block, Builder, Extra, Flags, Func, Inst, InstData, Opcode, Signature, Type, Value,
};
use super::{Coalesce, NO_FUEL, NOT_ADJACENT};
use crate::stats::Kind;
use crate::{Fuel, Pass, Stats};
fn fills(names: &mut Interner, kind: Opcode, at: &[(i64, i64)]) -> Func {
let (mut func, entry, base) = start(names);
let mut build = Builder::new(&mut func, entry);
for &(offset, size) in at {
let address = walk(&mut build, base, offset);
plane(&mut build, kind, address, size, 1);
}
build.ret(&[]);
func
}
fn start(names: &mut Interner) -> (Func, Block, Value) {
let signature = Signature::new().with_params(&[Type::PTR]);
let mut func = Func::new(names.intern("fill"), signature);
let entry = func.create_block();
let base = func.append_param(entry, Type::PTR);
(func, entry, base)
}
fn walk(build: &mut Builder<'_>, base: Value, offset: i64) -> Value {
let step = build.iconst(Type::int(64), i128::from(offset));
build.binary(Opcode::PtrAdd, base, step, Flags::NONE)
}
fn plane(build: &mut Builder<'_>, kind: Opcode, address: Value, size: i64, node: u32) {
let width = build.iconst(Type::int(64), i128::from(size));
let extra = match kind {
Opcode::MetaType => Extra::Node(Idx::new(node)),
_ => Extra::None,
};
let args = build.func().push_values(&[address, width]);
build.inst(InstData { args, extra, ..InstData::new(kind) }, &[]);
}
fn writes(func: &Func, kind: Opcode) -> Vec<(i128, i128)> {
every(func)
.into_iter()
.filter(|&inst| func[inst].opcode == kind)
.map(|inst| {
let [pointer, length] = func[func[inst].args] else { panic!("two operands") };
let (_, at) = crate::discharge::normal(func, pointer);
let (imm, ty) = crate::fold::constant(func, length).expect("a constant width");
(at, imm.signed(ty))
})
.collect()
}
fn every(func: &Func) -> Vec<Inst> {
func.blocks().flat_map(|block| func.insts(block).collect::<Vec<_>>()).collect()
}
fn run(func: &mut Func) -> Stats {
with(func, &mut Fuel::unlimited())
}
fn with(func: &mut Func, fuel: &mut Fuel) -> Stats {
Coalesce.run(func, &mut crate::machine::fixtures::analyses(), fuel)
}
#[test]
fn three_fields_filled_in_order_are_one_write_over_the_whole_structure() {
let mut names = Interner::new();
let mut func = fills(&mut names, Opcode::MetaInit, &[(0, 8), (8, 8), (16, 8)]);
let stats = run(&mut func);
assert!(stats.changed());
assert_eq!(writes(&func, Opcode::MetaInit), [(0, 24)]);
}
#[test]
fn the_one_write_left_is_where_the_last_of_them_was_and_not_where_the_first_was() {
let mut names = Interner::new();
let mut func = fills(&mut names, Opcode::MetaInit, &[(0, 8), (8, 8), (16, 8)]);
run(&mut func);
let order = every(&func);
let merged = order
.iter()
.position(|&inst| func[inst].opcode == Opcode::MetaInit)
.expect("the merged write");
let last = order
.iter()
.rposition(|&inst| func[inst].opcode == Opcode::PtrAdd)
.expect("the last address");
assert!(merged > last, "the merged write rose above an address it is about");
}
#[test]
fn a_structure_filled_in_backwards_merges_the_same_way() {
let mut names = Interner::new();
let mut func = fills(&mut names, Opcode::MetaInit, &[(16, 8), (8, 8), (0, 8)]);
let stats = run(&mut func);
assert!(stats.changed());
assert_eq!(writes(&func, Opcode::MetaInit), [(0, 24)]);
}
#[test]
fn a_gap_between_two_of_them_is_where_the_run_stops() {
let mut names = Interner::new();
let mut func = fills(&mut names, Opcode::MetaInit, &[(0, 8), (8, 8), (24, 8)]);
let stats = run(&mut func);
assert!(stats.changed());
assert_eq!(writes(&func, Opcode::MetaInit), [(0, 16), (24, 8)]);
assert_eq!(stats.count(Kind::Missed, NOT_ADJACENT), 1);
}
#[test]
fn two_writes_over_bytes_that_overlap_stay_in_the_order_the_program_put_them_in() {
let mut names = Interner::new();
let mut func = fills(&mut names, Opcode::MetaInit, &[(0, 8), (4, 8)]);
let stats = run(&mut func);
assert!(!stats.changed());
assert_eq!(writes(&func, Opcode::MetaInit), [(0, 8), (4, 8)]);
}
#[test]
fn a_run_of_one_write_is_left_exactly_as_it_was() {
let mut names = Interner::new();
let mut func = fills(&mut names, Opcode::MetaInit, &[(0, 8)]);
let stats = run(&mut func);
assert!(!stats.changed());
assert_eq!(writes(&func, Opcode::MetaInit), [(0, 8)]);
}
#[test]
fn two_fields_of_one_type_merge_and_a_third_of_another_does_not_join_them() {
let mut names = Interner::new();
let (mut func, entry, base) = start(&mut names);
let mut build = Builder::new(&mut func, entry);
for (offset, node) in [(0, 1), (8, 1), (16, 2)] {
let address = walk(&mut build, base, offset);
plane(&mut build, Opcode::MetaType, address, 8, node);
}
build.ret(&[]);
let stats = run(&mut func);
assert!(stats.changed());
assert_eq!(writes(&func, Opcode::MetaType), [(0, 16), (16, 8)]);
}
#[test]
fn the_two_planes_do_not_stop_each_other_and_each_ends_up_with_one_write() {
let mut names = Interner::new();
let (mut func, entry, base) = start(&mut names);
let mut build = Builder::new(&mut func, entry);
for offset in [0, 8, 16] {
let address = walk(&mut build, base, offset);
plane(&mut build, Opcode::MetaType, address, 8, 1);
plane(&mut build, Opcode::MetaInit, address, 8, 1);
}
build.ret(&[]);
let stats = run(&mut func);
assert!(stats.changed());
assert_eq!(writes(&func, Opcode::MetaType), [(0, 24)]);
assert_eq!(writes(&func, Opcode::MetaInit), [(0, 24)]);
}
#[test]
fn a_call_in_the_middle_of_a_run_is_where_it_stops() {
let mut names = Interner::new();
let (mut func, entry, base) = start(&mut names);
let mut build = Builder::new(&mut func, entry);
for offset in [0, 8] {
let address = walk(&mut build, base, offset);
plane(&mut build, Opcode::MetaInit, address, 8, 1);
}
build.inst(InstData::new(Opcode::Call), &[]);
for offset in [16, 24] {
let address = walk(&mut build, base, offset);
plane(&mut build, Opcode::MetaInit, address, 8, 1);
}
build.ret(&[]);
let stats = run(&mut func);
assert!(stats.changed());
assert_eq!(writes(&func, Opcode::MetaInit), [(0, 16), (16, 16)]);
}
#[test]
fn a_read_of_the_same_plane_stops_the_run_and_a_read_of_another_one_does_not() {
for (between, merged) in
[(Opcode::CheckInit, vec![(0, 8), (8, 8)]), (Opcode::CheckBounds, vec![(0, 16)])]
{
let mut names = Interner::new();
let (mut func, entry, base) = start(&mut names);
let mut build = Builder::new(&mut func, entry);
let first = walk(&mut build, base, 0);
plane(&mut build, Opcode::MetaInit, first, 8, 1);
build.inst(InstData::new(between), &[]);
let second = walk(&mut build, base, 8);
plane(&mut build, Opcode::MetaInit, second, 8, 1);
build.ret(&[]);
run(&mut func);
assert_eq!(writes(&func, Opcode::MetaInit), merged, "across {}", between.name());
}
}
#[test]
fn the_epoch_plane_is_left_alone_however_its_writes_line_up() {
let mut names = Interner::new();
let mut func = fills(&mut names, Opcode::MetaEpoch, &[(0, 8), (8, 8), (16, 8)]);
let stats = run(&mut func);
assert!(!stats.changed());
assert_eq!(writes(&func, Opcode::MetaEpoch), [(0, 8), (8, 8), (16, 8)]);
}
#[test]
fn a_width_the_pass_cannot_read_stops_the_run_rather_than_being_guessed_at() {
let mut names = Interner::new();
let (mut func, entry, base) = start(&mut names);
let mut build = Builder::new(&mut func, entry);
let first = walk(&mut build, base, 0);
plane(&mut build, Opcode::MetaInit, first, 8, 1);
let second = walk(&mut build, base, 8);
let width = build.unary(Opcode::PtrToInt, base, Type::int(64));
let args = build.func().push_values(&[second, width]);
build.inst(InstData { args, ..InstData::new(Opcode::MetaInit) }, &[]);
build.ret(&[]);
let stats = run(&mut func);
assert!(!stats.changed());
}
#[test]
fn no_fuel_leaves_every_write_where_it_is() {
let mut names = Interner::new();
let mut func = fills(&mut names, Opcode::MetaInit, &[(0, 8), (8, 8), (16, 8)]);
let stats = with(&mut func, &mut Fuel::of(0));
assert!(!stats.changed());
assert_eq!(writes(&func, Opcode::MetaInit), [(0, 8), (8, 8), (16, 8)]);
assert_eq!(stats.count(Kind::Missed, NO_FUEL), 1);
}
#[test]
fn a_function_with_no_body_is_left_alone() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let stats = run(&mut func);
assert!(!stats.changed());
}
}