use rucc_base::Symbol;
use rucc_ir::{
Block, Builder, Def, Extra, Flags, Func, Inst, InstData, MemInfo, Opcode, Type, Value,
};
use crate::alias::{Origin, origin};
use crate::cfg::Cfg;
use crate::discharge::{Question, named_by, yes};
use crate::dom::Dominators;
use crate::loops::{LoopId, Loops};
use crate::range::query::Ranges;
use crate::rules::safety;
use crate::scev::{Anchor, Evolution, Invariant, Plain, Reading, Scev};
use crate::trip::{Around, counted, covered, inst_of};
use crate::{Analyses, Analysis, Fuel, Pass, Preserved, Stats};
const HOISTED: &str = "bounds check taken out of a loop, one check in front of it covers every \
iteration";
const HOISTED_TYPE: &str = "type check taken out of a loop, one check in front of it covers every \
iteration";
const HOISTED_INIT: &str = "init check taken out of a loop, one check in front of it covers every \
iteration";
const NO_FUEL: &str = "check kept, the pass ran out of fuel";
const NO_PREHEADER: &str = "loop left alone, it has no block in front of it to put a check in";
const ANOTHER_WAY_OUT: &str =
"loop left alone, it can be left somewhere other than its bottom test";
const A_LOOP_INSIDE: &str = "loop left alone, it has another loop inside it";
const A_CALL_INSIDE: &str = "loop left alone, a call in it might not come back";
const COUNT_TOO_WIDE: &str =
"check kept, how many bytes the loop covers might not fit in sixty four bits";
const NOT_A_SWEEP: &str = "check kept, its address does not walk the loop by a constant";
const START_NOT_A_WORD: &str =
"check kept, where its walk starts is not worked out in sixty four bit arithmetic";
const NOT_FOLLOWED: &str =
"check kept, what its address does round the loop is not something the analysis follows";
const ALREADY_COMPUTED: &str =
"check kept, how many bytes it covers is a number only the program has";
const BACKWARDS: &str = "check kept, its address walks the loop from high to low";
const NOT_EVERY_TIME: &str = "check kept, an iteration can finish without reaching it";
const MISALIGNED: &str = "check kept, its step is not a whole number of its alignment";
const TOO_WIDE: &str = "check kept, the range the loop sweeps is too wide for the rule";
const NOT_ITS_CAPABILITY: &str =
"check kept, its capability is about a pointer the walk does not start on";
const WRITES_THE_INIT_PLANE: &str = "init check kept, the loop writes the init plane and a later \
iteration may read what an earlier one wrote";
const WRITES_THE_TYPE_PLANE: &str = "type check kept, the loop writes the type plane and a later \
iteration may read what an earlier one wrote";
#[derive(Debug)]
pub struct Hoist;
impl Pass for Hoist {
fn name(&self) -> &'static str {
"hoist"
}
fn describe(&self) -> &'static str {
"a check in a counted loop becomes one check in front of the loop"
}
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();
if func.entry().is_none() {
return stats;
}
let cfg = an.cfg(func);
let doms = an.dominators(func);
let loops = an.loops(func);
if loops.count() == 0 {
return stats;
}
let mut plans = Vec::new();
{
let mut scev = Scev::new(func, cfg, loops);
let mut ranges = Ranges::new(func, cfg, doms);
for id in loops.all() {
sweep(func, cfg, doms, loops, &mut scev, &mut ranges, id, &mut plans, &mut stats);
}
}
let mut written = Vec::new();
for plan in plans {
if !fuel.take() {
stats.missed(NO_FUEL);
continue;
}
let done = match plan.opcode {
Opcode::CheckType => HOISTED_TYPE,
Opcode::CheckInit => HOISTED_INIT,
_ => HOISTED,
};
apply(func, &plan, &mut written);
stats.optimized(done);
}
stats
}
}
#[derive(Debug)]
struct Plan {
preheader: Block,
base: Anchor,
start: Plain,
span: Extent,
stride: Option<i128>,
info: MemInfo,
opcode: Opcode,
check: Inst,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Extent {
Bytes(u64),
Computed { count: Plain, step: i128, reach: i128, reading: Reading },
}
#[expect(clippy::too_many_arguments, reason = "four analyses, a plan list and a report to fill")]
fn sweep(
func: &Func,
cfg: &Cfg,
doms: &Dominators,
loops: &Loops,
scev: &mut Scev<'_>,
ranges: &mut Ranges<'_>,
id: LoopId,
plans: &mut Vec<Plan>,
stats: &mut Stats,
) {
let checks: Vec<Inst> = loops
.blocks(id)
.iter()
.filter(|&&block| loops.innermost(block) == Some(id))
.flat_map(|&block| func.insts(block).collect::<Vec<Inst>>())
.filter(|&inst| {
matches!(func[inst].opcode, Opcode::CheckBounds | Opcode::CheckInit | Opcode::CheckType)
})
.collect();
if checks.is_empty() {
return;
}
let (preheader, guard) = match shaped(func, cfg, doms, loops, id) {
Ok(shape) => shape,
Err(why) => {
stats.missed(why);
return;
}
};
let around = match counted(scev, id) {
Ok(around) => around,
Err(why) => {
stats.missed(why);
return;
}
};
let written = writes(func, loops, id);
for check in checks {
if let Some(why) = written.refusing(func, check) {
stats.missed(why);
continue;
}
match planned(func, doms, scev, ranges, id, preheader, guard, around, check) {
Ok(plan) => plans.push(plan),
Err(why) => stats.missed(why),
}
}
}
#[derive(Clone, Debug)]
struct Written {
init: Vec<Value>,
ty: Vec<Value>,
}
impl Written {
fn refusing(&self, func: &Func, check: Inst) -> Option<&'static str> {
let (writes, why) = match func[check].opcode {
Opcode::CheckInit => (&self.init, WRITES_THE_INIT_PLANE),
Opcode::CheckType => (&self.ty, WRITES_THE_TYPE_PLANE),
_ => return None,
};
let &pointer = func[func[check].args].get(1)?;
let read = Object::of(func, pointer);
let apart = |&at: &Value| read.zip(Object::of(func, at)).is_some_and(|(a, b)| a.apart(b));
(!writes.iter().all(apart)).then_some(why)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Object {
Local(Inst),
Global(Symbol),
Fresh(Inst),
}
impl Object {
fn of(func: &Func, pointer: Value) -> Option<Self> {
match origin(func, pointer).0 {
Origin::Local(inst) => Some(Self::Local(inst)),
Origin::Global(name) => Some(Self::Global(name)),
Origin::Unknown(value) => {
let Def::Result { inst, index: 0 } = func[value].def else { return None };
let data = &func[inst];
let fresh = data.opcode == Opcode::Call
&& data.flags.contains(Flags::HEAP)
&& func[data.args].iter().all(|&arg| func[arg].ty != Type::PTR);
fresh.then_some(Self::Fresh(inst))
}
}
}
fn apart(self, other: Self) -> bool {
match (self, other) {
(Self::Global(_), Self::Global(_)) => false,
(Self::Local(one), Self::Local(two)) | (Self::Fresh(one), Self::Fresh(two)) => {
one != two
}
_ => true,
}
}
}
fn writes(func: &Func, loops: &Loops, id: LoopId) -> Written {
let mut written = Written { init: Vec::new(), ty: Vec::new() };
for &block in loops.blocks(id) {
for inst in func.insts(block) {
let Some(&at) = func[func[inst].args].first() else { continue };
match func[inst].opcode {
Opcode::MetaInit | Opcode::MetaInitCopy => written.init.push(at),
Opcode::MetaType | Opcode::MetaTypeCopy => written.ty.push(at),
Opcode::MetaBegin => {
written.init.push(at);
written.ty.push(at);
}
_ => {}
}
}
}
written
}
pub(crate) fn shaped(
func: &Func,
cfg: &Cfg,
doms: &Dominators,
loops: &Loops,
id: LoopId,
) -> Result<(Block, Block), &'static str> {
let Some(preheader) = loops.preheader(cfg, id) else {
return Err(NO_PREHEADER);
};
let [latch] = loops.latches(id) else {
return Err(ANOTHER_WAY_OUT);
};
let [exit] = loops.exits(id) else {
return Err(ANOTHER_WAY_OUT);
};
if !doms.dominates(exit.from, *latch) {
return Err(ANOTHER_WAY_OUT);
}
for &block in loops.blocks(id) {
if loops.innermost(block) != Some(id) {
return Err(A_LOOP_INSIDE);
}
if cfg.successors(block).is_empty() {
return Err(ANOTHER_WAY_OUT);
}
for inst in func.insts(block) {
if matches!(
func[inst].opcode,
Opcode::Call
| Opcode::CallIndirect
| Opcode::TailCall
| Opcode::InlineAsm
| Opcode::MetaEnd
| Opcode::MetaTransfer
) {
return Err(A_CALL_INSIDE);
}
}
}
Ok((preheader, exit.from))
}
#[expect(clippy::too_many_arguments, reason = "each one is a separate thing the answer rests on")]
fn planned(
func: &Func,
doms: &Dominators,
scev: &mut Scev<'_>,
ranges: &mut Ranges<'_>,
id: LoopId,
preheader: Block,
guard: Block,
around: Around,
check: Inst,
) -> Result<Plan, &'static str> {
let block = func.block_of(check).ok_or(NOT_EVERY_TIME)?;
if !doms.dominates(block, guard) {
return Err(NOT_EVERY_TIME);
}
let args = &func[func[check].args];
if args.len() > 2 {
return Err(ALREADY_COMPUTED);
}
let (Some(&capability), Some(&pointer)) = (args.first(), args.get(1)) else {
return Err(NOT_A_SWEEP);
};
let named = named_by(func, capability);
let Extra::Mem(held) = func[check].extra else { return Err(NOT_A_SWEEP) };
let info = func[held];
let reach = i128::from(info.size);
let mut stride = None;
let (base, start, span) = match scev.evolution(id, pointer) {
Evolution::Invariant(at) => {
let Some((base, start)) = anchored(at) else {
return Err(NOT_A_SWEEP);
};
plain_enough(func, start)?;
if !swept(reach, 0, reach) {
return Err(TOO_WIDE);
}
(base, start, Extent::Bytes(u64::try_from(reach).map_err(|_| TOO_WIDE)?))
}
Evolution::Affine(chrec) => {
let Some(step) = chrec.step.as_number() else {
return Err(NOT_A_SWEEP);
};
if step <= 0 {
return Err(BACKWARDS);
}
let Some((base, start)) = anchored(chrec.base) else {
return Err(NOT_A_SWEEP);
};
plain_enough(func, start)?;
if step % i128::from(info.align) != 0 {
return Err(MISALIGNED);
}
if matches!(func[check].opcode, Opcode::CheckInit | Opcode::CheckType) && step > reach {
stride = Some(step);
}
let span = match around {
Around::Number(around) => {
let far = around.checked_mul(step).ok_or(TOO_WIDE)?;
let span = far.checked_add(reach).ok_or(TOO_WIDE)?;
if !swept(span, far, reach) {
return Err(TOO_WIDE);
}
Extent::Bytes(u64::try_from(span).map_err(|_| TOO_WIDE)?)
}
Around::Computed(count, reading) => {
fits(func, ranges, preheader, count, step, reach, reading)?;
if !swept_sym(reach) {
return Err(TOO_WIDE);
}
Extent::Computed { count, step, reach, reading }
}
};
(base, start, span)
}
Evolution::Unknown => return Err(NOT_FOLLOWED),
};
let opcode = func[check].opcode;
if opcode == Opcode::CheckBounds {
let Some(named) = named else { return Err(NOT_A_SWEEP) };
let none_past = (start.value.is_none() || start.scale == 0) && start.offset == 0;
let at_it = base == Anchor::Value(named) && none_past;
if named != pointer && !at_it {
return Err(NOT_ITS_CAPABILITY);
}
}
Ok(Plan { preheader, base, start, span, stride, info, opcode, check })
}
pub(crate) fn anchored(inv: Invariant) -> Option<(Anchor, Plain)> {
if let Some(at @ Plain { value: Some(base), scale: 1, .. }) = inv.plain() {
let past = Plain { value: None, read: None, scale: 0, offset: at.offset };
return Some((Anchor::Value(base), past));
}
inv.on()
}
pub(crate) fn plain_enough(func: &Func, start: Plain) -> Result<(), &'static str> {
let Some(value) = start.value.filter(|_| start.scale != 0) else { return Ok(()) };
let ty = match start.read {
Some(read) => read.to,
None => func[value].ty,
};
if ty.is_int() && ty.bits() == 64 { Ok(()) } else { Err(START_NOT_A_WORD) }
}
pub(crate) fn fits(
func: &Func,
ranges: &mut Ranges<'_>,
preheader: Block,
count: Plain,
step: i128,
reach: i128,
reading: Reading,
) -> Result<(), &'static str> {
let value = count.value.ok_or(COUNT_TOO_WIDE)?;
let ty = func[value].ty;
if !ty.is_int() {
return Err(COUNT_TOO_WIDE);
}
if ty.bits() > 64 {
return Err(COUNT_TOO_WIDE);
}
let most = if ty.bits() == 64 {
widest(ranges, preheader, value, reading).ok_or(COUNT_TOO_WIDE)?
} else {
match reading {
Reading::Signed => 1i128 << (ty.bits() - 1),
Reading::Unsigned => (1i128 << ty.bits()) - 1,
}
};
let reached = count
.scale
.checked_abs()
.and_then(|scale| scale.checked_mul(most))
.and_then(|far| far.checked_add(count.offset.checked_abs()?))
.ok_or(COUNT_TOO_WIDE)?;
let span =
reached.checked_mul(step).and_then(|far| far.checked_add(reach)).ok_or(COUNT_TOO_WIDE)?;
if span > i128::from(i64::MAX) {
return Err(COUNT_TOO_WIDE);
}
Ok(())
}
fn widest(
ranges: &mut Ranges<'_>,
preheader: Block,
value: Value,
reading: Reading,
) -> Option<i128> {
let range = ranges.at(value, preheader);
match reading {
Reading::Signed => {
let (low, high) = range.signed_bounds()?;
Some(low.checked_abs()?.max(high.checked_abs()?))
}
Reading::Unsigned => i128::try_from(range.unsigned_bounds()?.1).ok(),
}
}
fn swept(span: i128, far: i128, reach: i128) -> bool {
let mut question = Question::default();
let at = question.opaque();
let at = question.app("value.i64", &[at]);
let span = question.number(span);
let span = question.app("iconst.i64", &[span]);
let far = question.number(far);
let far = question.app("iconst.i64", &[far]);
let reach = question.number(reach);
let reach = question.app("iconst.i64", &[reach]);
let delta = question.opaque();
let delta = question.app("value.i64", &[delta]);
let term = question.app("swept.i64", &[at, span, far, reach, delta]);
match safety::TABLE.find(&question, term) {
Some(found) => yes(&safety::TABLE, found.rule),
None => false,
}
}
fn swept_sym(reach: i128) -> bool {
let mut question = Question::default();
let at = question.opaque();
let at = question.app("value.i64", &[at]);
let span = question.opaque();
let span = question.app("value.i64", &[span]);
let far = question.opaque();
let far = question.app("value.i64", &[far]);
let reach = question.number(reach);
let reach = question.app("iconst.i64", &[reach]);
let delta = question.opaque();
let delta = question.app("value.i64", &[delta]);
let term = question.app("swept.sym.i64", &[at, span, far, reach, delta]);
match safety::TABLE.find(&question, term) {
Some(found) => yes(&safety::TABLE, found.rule),
None => false,
}
}
pub(crate) fn starting(
build: &mut Builder<'_>,
made: &mut Vec<Value>,
base: Value,
start: Plain,
) -> Value {
let word = Type::int(64);
let past = match start.value.filter(|_| start.scale != 0) {
None => {
if start.offset == 0 {
return base;
}
let by = build.iconst(word, start.offset);
made.push(by);
by
}
Some(value) => {
let mut at = value;
if let Some(read) = start.read {
let widen = match read.reading {
Reading::Signed => Opcode::SExt,
Reading::Unsigned => Opcode::ZExt,
};
at = build.unary(widen, value, word);
made.push(at);
}
if start.scale != 1 {
let scale = build.iconst(word, start.scale);
made.push(scale);
at = build.binary(Opcode::Mul, at, scale, Flags::NONE);
made.push(at);
}
if start.offset != 0 {
let offset = build.iconst(word, start.offset);
made.push(offset);
at = build.binary(Opcode::Add, at, offset, Flags::NONE);
made.push(at);
}
at
}
};
let args = build.func().push_values(&[base, past]);
let sum = build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR);
made.push(sum);
sum
}
struct Operands {
preheader: Block,
base: Anchor,
start: Plain,
span: Extent,
stride: Option<i128>,
size: u64,
operands: Vec<Value>,
}
fn apply(func: &mut Func, plan: &Plan, written: &mut Vec<Operands>) {
let term = func.terminator(plan.preheader).expect("a preheader ends in a jump to the header");
let same = |w: &&Operands| {
(w.preheader, w.base, w.start, w.span, w.stride)
== (plan.preheader, plan.base, plan.start, plan.span, plan.stride)
};
let (size, operands) = match written.iter().find(same) {
Some(w) => (w.size, w.operands.clone()),
None => {
let (size, operands) = operands(func, plan, term);
written.push(Operands {
preheader: plan.preheader,
base: plan.base,
start: plan.start,
span: plan.span,
stride: plan.stride,
size,
operands: operands.clone(),
});
(size, operands)
}
};
let info = MemInfo { size, ..plan.info };
let extra = Extra::Mem(func.add_mem(info));
let args = func.push_values(&operands);
let data = InstData { args, extra, ..InstData::new(plan.opcode) };
let check = Builder::new(func, plan.preheader).inst(data, &[]);
func.remove_inst(check);
func.insert_before(check, term);
func.remove_inst(plan.check);
}
fn operands(func: &mut Func, plan: &Plan, term: Inst) -> (u64, Vec<Value>) {
let mut made = Vec::new();
let mut build = Builder::new(func, plan.preheader);
let base = match plan.base {
Anchor::Value(value) => value,
Anchor::Address(symbol) => {
let extra = Extra::Symbol(symbol);
let at =
build.value(InstData { extra, ..InstData::new(Opcode::GlobalAddr) }, Type::PTR);
made.push(at);
at
}
};
let first = starting(&mut build, &mut made, base, plan.start);
let args = build.func().push_values(&[first]);
let capability = build.value(InstData { args, ..InstData::new(Opcode::CapOf) }, Type::CAP);
made.push(capability);
let (size, extent) = match plan.span {
Extent::Bytes(bytes) => (bytes, None),
Extent::Computed { count, step, reach, reading } => (
plan.info.size,
Some(covered(&mut build, &mut made, count, step, reach, reading, Flags::NSW)),
),
};
let (size, operands) = match (plan.stride, extent) {
(Some(step), extent) => {
let word = Type::int(64);
let span = match extent {
Some(bytes) => bytes,
None => {
let bytes = build.iconst(word, i128::from(size));
made.push(bytes);
bytes
}
};
let step = build.iconst(word, step);
made.push(step);
(plan.info.size, vec![capability, first, span, step])
}
(None, Some(bytes)) => (size, vec![capability, first, bytes]),
(None, None) => (size, vec![capability, first]),
};
for value in made {
let inst = inst_of(func, value);
func.remove_inst(inst);
func.insert_before(inst, term);
}
(size, operands)
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{Flags, IntPred, MemInfo, MemOrder, Module, Restrict, Signature, verify_func};
use rucc_target::{TargetInfo, Triple};
use super::{HOISTED, HOISTED_INIT, HOISTED_TYPE, Hoist};
use crate::canon::Canon;
use crate::stats::Kind;
use crate::{Fuel, Pass, Stats};
use rucc_ir::{Block, Builder, Extra, Func, Inst, InstData, Opcode, Type, Value};
const WIDTH: i128 = 4;
fn walking(trips: i128, step: i128, size: u64, align: u32) -> (Interner, Func, Vec<Block>) {
promising(trips, step, size, align, Flags::NSW)
}
fn promising(
trips: i128,
step: i128,
size: u64,
align: u32,
flags: Flags,
) -> (Interner, Func, Vec<Block>) {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::PTR]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let head = func.create_block();
let done = func.create_block();
let array = func.append_param(entry, Type::PTR);
let counter = func.append_param(head, Type::int(64));
let zero = Builder::new(&mut func, entry).iconst(Type::int(64), 0);
Builder::new(&mut func, entry).jump(head, &[zero]);
let mut build = Builder::new(&mut func, head);
let by = build.iconst(Type::int(64), step);
let scaled = build.binary(Opcode::Mul, counter, by, Flags::NSW);
let args = build.func().push_values(&[array, scaled]);
let pointer = build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR);
check(&mut build, pointer, size, align);
let one = build.iconst(Type::int(64), 1);
let next = build.binary(Opcode::Add, counter, one, flags);
let limit = build.iconst(Type::int(64), trips);
let again = build.icmp(IntPred::Slt, next, limit);
build.br_if(again, head, &[next], done, &[]);
Builder::new(&mut func, done).ret(&[]);
(names, func, vec![entry, head, done])
}
fn check(build: &mut Builder<'_>, pointer: Value, size: u64, align: u32) {
let args = build.func().push_values(&[pointer]);
let capability = build.value(InstData { args, ..InstData::new(Opcode::CapOf) }, Type::CAP);
let info = MemInfo {
size,
align,
order: MemOrder::NotAtomic,
tbaa: None,
owns: 0,
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 hoisted(func: &mut Func) -> Stats {
let mut an = crate::machine::fixtures::analyses();
Canon.run(func, &mut an, &mut Fuel::unlimited());
Hoist.run(func, &mut an, &mut Fuel::unlimited())
}
fn checks(func: &Func) -> Vec<(Block, Inst)> {
func.blocks()
.flat_map(|block| func.insts(block).map(move |inst| (block, inst)).collect::<Vec<_>>())
.filter(|&(_, inst)| func[inst].opcode == Opcode::CheckBounds)
.collect()
}
fn extent(func: &Func, check: Inst) -> u64 {
let Extra::Mem(info) = func[check].extra else { panic!("a check carries a payload") };
func[info].size
}
fn sound(func: &Func, names: &mut Interner) {
let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
let module = Module::new(names.intern("t.c"), &target);
if let Err(errors) = verify_func(&module, func, names) {
panic!("{errors:#?}");
}
}
#[test]
fn a_check_that_walks_a_counted_loop_becomes_one_check_in_front_of_it() {
let (mut names, mut func, _) = walking(16, WIDTH, 4, 4);
let stats = hoisted(&mut func);
assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
let left = checks(&func);
assert_eq!(left.len(), 1, "one check, and it is the one that was put in front");
assert_eq!(extent(&func, left[0].1), 64, "fifteen steps of four, plus the last read");
sound(&func, &mut names);
}
fn over_a_global(trips: i128, step: i128) -> (Interner, Func, Vec<Block>) {
let mut names = Interner::new();
let tab = names.intern("tab");
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let head = func.create_block();
let done = func.create_block();
let counter = func.append_param(head, Type::int(64));
let zero = Builder::new(&mut func, entry).iconst(Type::int(64), 0);
Builder::new(&mut func, entry).jump(head, &[zero]);
let mut build = Builder::new(&mut func, head);
let by = build.iconst(Type::int(64), step);
let scaled = build.binary(Opcode::Mul, counter, by, Flags::NSW);
let extra = Extra::Symbol(tab);
let array = build.value(InstData { extra, ..InstData::new(Opcode::GlobalAddr) }, Type::PTR);
let args = build.func().push_values(&[array, scaled]);
let pointer = build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR);
check(&mut build, pointer, 4, 4);
let one = build.iconst(Type::int(64), 1);
let next = build.binary(Opcode::Add, counter, one, Flags::NSW);
let limit = build.iconst(Type::int(64), trips);
let again = build.icmp(IntPred::Slt, next, limit);
build.br_if(again, head, &[next], done, &[]);
Builder::new(&mut func, done).ret(&[]);
(names, func, vec![entry, head, done])
}
#[test]
fn a_walk_over_a_file_scope_array_is_hoisted_like_any_other() {
let (mut names, mut func, _) = over_a_global(16, WIDTH);
let stats = hoisted(&mut func);
assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
let left = checks(&func);
assert_eq!(left.len(), 1, "one check, and it is the one that was put in front");
assert_eq!(extent(&func, left[0].1), 64, "fifteen steps of four, plus the last read");
let (cfg, _, loops) = forest(&func);
let id = loops.all().next().expect("there is a loop");
assert_eq!(loops.preheader(&cfg, id), Some(left[0].0), "it is in the preheader");
sound(&func, &mut names);
}
#[test]
fn a_loop_whose_counter_promises_nothing_keeps_its_check() {
let (_, mut func, _) = promising(16, WIDTH, 4, 4, Flags::NONE);
let stats = hoisted(&mut func);
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, crate::trip::NOT_COUNTED), 1);
assert_eq!(checks(&func).len(), 1, "and it is still in the body");
}
#[test]
fn the_check_that_is_left_is_outside_the_loop() {
let (_, mut func, _) = walking(16, WIDTH, 4, 4);
hoisted(&mut func);
let (block, _) = checks(&func)[0];
let (cfg, doms, loops) = forest(&func);
let _ = doms;
let id = loops.all().next().expect("there is a loop");
assert!(!loops.contains(id, block), "the check is not in the loop any more");
assert_eq!(loops.preheader(&cfg, id), Some(block), "it is in the preheader");
}
fn forest(func: &Func) -> (crate::Cfg, crate::Dominators, crate::Loops) {
let cfg = crate::Cfg::new(func);
let doms = crate::Dominators::new(&cfg);
let loops = crate::Loops::new(&cfg, &doms);
(cfg, doms, loops)
}
#[test]
fn a_walk_whose_step_is_wider_than_its_access_covers_the_gaps_too() {
let (mut names, mut func, _) = walking(8, 16, 4, 4);
assert_eq!(hoisted(&mut func).count(Kind::Optimized, HOISTED), 1);
assert_eq!(extent(&func, checks(&func)[0].1), 116, "seven steps of sixteen, plus four");
sound(&func, &mut names);
}
fn unknown(
ty: Type,
pred: IntPred,
flags: Flags,
widening: Opcode,
) -> (Interner, Func, Vec<Block>) {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::PTR, ty]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let head = func.create_block();
let done = func.create_block();
let array = func.append_param(entry, Type::PTR);
let limit = func.append_param(entry, ty);
let counter = func.append_param(head, ty);
let zero = Builder::new(&mut func, entry).iconst(ty, 0);
Builder::new(&mut func, entry).jump(head, &[zero]);
let mut build = Builder::new(&mut func, head);
let wide = if ty == Type::int(64) {
counter
} else {
build.unary(widening, counter, Type::int(64))
};
let by = build.iconst(Type::int(64), WIDTH);
let scaled = build.binary(Opcode::Mul, wide, by, Flags::NSW);
let args = build.func().push_values(&[array, scaled]);
let pointer = build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR);
check(&mut build, pointer, 4, 4);
let one = build.iconst(ty, 1);
let next = build.binary(Opcode::Add, counter, one, flags);
let again = build.icmp(pred, next, limit);
build.br_if(again, head, &[next], done, &[]);
Builder::new(&mut func, done).ret(&[]);
(names, func, vec![entry, head, done])
}
fn operands(func: &Func, check: Inst) -> Vec<Value> {
func[func[check].args].to_vec()
}
#[test]
fn a_loop_that_runs_a_number_of_times_nobody_knows_gets_a_check_that_works_it_out() {
let (mut names, mut func, _) =
unknown(Type::int(32), IntPred::Slt, Flags::NSW, Opcode::SExt);
let stats = hoisted(&mut func);
assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
let left = checks(&func);
assert_eq!(left.len(), 1, "one check, and it is the one that was put in front");
assert_eq!(operands(&func, left[0].1).len(), 3, "its extent is an operand");
assert_eq!(extent(&func, left[0].1), 4, "and its payload is one element of the walk");
sound(&func, &mut names);
}
#[test]
fn the_extent_a_loop_of_unknown_length_gets_is_the_one_the_arithmetic_says() {
let (_, mut func, blocks) = unknown(Type::int(32), IntPred::Slt, Flags::NSW, Opcode::SExt);
hoisted(&mut func);
let (block, check) = checks(&func)[0];
assert_ne!(block, blocks[1], "the check is out of the body");
let bytes = operands(&func, check)[2];
let steps: Vec<Opcode> = func
.insts(block)
.map(|inst| func[inst].opcode)
.filter(|&opcode| {
matches!(opcode, Opcode::SExt | Opcode::Add | Opcode::ICmp | Opcode::Select)
})
.collect();
assert_eq!(
steps,
[Opcode::SExt, Opcode::Add, Opcode::ICmp, Opcode::Select, Opcode::Add],
"sign extend, take one off, clamp at zero, and add the last read back on"
);
assert_eq!(func[bytes].ty, Type::int(64), "the extent is a word wide");
}
#[test]
fn a_loop_counted_as_wide_as_the_arithmetic_keeps_its_check_when_nothing_bounds_the_count() {
let (_, mut func, _) = unknown(Type::int(64), IntPred::Slt, Flags::NSW, Opcode::SExt);
let stats = hoisted(&mut func);
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, super::COUNT_TOO_WIDE), 1);
assert_eq!(checks(&func).len(), 1, "and it is still in the body");
}
fn widened() -> (Interner, Func, Vec<Block>) {
let ty = Type::int(64);
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::PTR, Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let head = func.create_block();
let done = func.create_block();
let array = func.append_param(entry, Type::PTR);
let narrow = func.append_param(entry, Type::int(32));
let counter = func.append_param(head, ty);
let limit = Builder::new(&mut func, entry).unary(Opcode::ZExt, narrow, ty);
let zero = Builder::new(&mut func, entry).iconst(ty, 0);
Builder::new(&mut func, entry).jump(head, &[zero]);
let mut build = Builder::new(&mut func, head);
let by = build.iconst(ty, WIDTH);
let scaled = build.binary(Opcode::Mul, counter, by, Flags::NSW);
let args = build.func().push_values(&[array, scaled]);
let pointer = build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR);
check(&mut build, pointer, 4, 4);
let one = build.iconst(ty, 1);
let next = build.binary(Opcode::Add, counter, one, Flags::NSW);
let again = build.icmp(IntPred::Slt, next, limit);
build.br_if(again, head, &[next], done, &[]);
Builder::new(&mut func, done).ret(&[]);
(names, func, vec![entry, head, done])
}
#[test]
fn a_wide_count_the_ranges_can_bound_gets_its_check_taken_out() {
let (mut names, mut func, blocks) = widened();
let stats = hoisted(&mut func);
assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
let left = checks(&func);
assert_eq!(left.len(), 1, "one check, and it is the one that was put in front");
assert_ne!(left[0].0, blocks[1], "the check is out of the body");
assert_eq!(operands(&func, left[0].1).len(), 3, "its extent is an operand");
sound(&func, &mut names);
}
#[test]
fn a_loop_whose_exit_test_is_unsigned_gets_its_count_widened_the_same_way() {
let (mut names, mut func, blocks) =
unknown(Type::int(32), IntPred::Ult, Flags::NSW, Opcode::SExt);
let stats = hoisted(&mut func);
assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
let (block, check) = checks(&func)[0];
assert_ne!(block, blocks[1], "the check is out of the body");
let steps: Vec<Opcode> = func
.insts(block)
.map(|inst| func[inst].opcode)
.filter(|&opcode| {
matches!(
opcode,
Opcode::SExt | Opcode::ZExt | Opcode::Add | Opcode::ICmp | Opcode::Select
)
})
.collect();
assert_eq!(
steps,
[Opcode::ZExt, Opcode::Add, Opcode::ICmp, Opcode::Select, Opcode::Add],
"zero extend, take one off, clamp at zero, and add the last read back on"
);
assert_eq!(func[operands(&func, check)[2]].ty, Type::int(64), "the extent is a word wide");
sound(&func, &mut names);
}
#[test]
fn an_unsigned_counter_under_an_inclusive_test_keeps_its_check() {
let (_, mut func, _) = unknown(Type::int(32), IntPred::Ule, Flags::NSW, Opcode::SExt);
let stats = hoisted(&mut func);
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, crate::trip::RESTS_ON_NO_WRAP), 1);
}
#[test]
fn a_subscript_on_an_unsigned_counter_widens_on_the_strength_of_the_test() {
let (mut names, mut func, blocks) =
unknown(Type::int(32), IntPred::Ult, Flags::NONE, Opcode::ZExt);
let stats = hoisted(&mut func);
assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
let left = checks(&func);
assert_eq!(left.len(), 1, "one check, and it is the one that was put in front");
assert_ne!(left[0].0, blocks[1], "the check is out of the body");
sound(&func, &mut names);
}
#[test]
fn a_subscript_on_a_counter_under_an_inclusive_test_keeps_its_check() {
let (_, mut func, _) = unknown(Type::int(32), IntPred::Ule, Flags::NONE, Opcode::ZExt);
let stats = hoisted(&mut func);
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, crate::trip::RESTS_ON_NO_WRAP), 1);
assert_eq!(checks(&func).len(), 1, "and it is still in the body");
}
#[test]
fn a_loop_whose_counter_of_unknown_length_promises_nothing_keeps_its_check() {
let (_, mut func, _) = unknown(Type::int(32), IntPred::Slt, Flags::NONE, Opcode::SExt);
let stats = hoisted(&mut func);
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, crate::trip::RESTS_ON_NO_WRAP), 1);
assert_eq!(
stats.count(Kind::Missed, crate::trip::NOT_COUNTED),
0,
"and not as the other one"
);
}
#[test]
fn a_loop_that_ends_on_a_test_its_counter_may_step_over_keeps_its_check() {
let (_, mut func, _) = unknown(Type::int(32), IntPred::Ne, Flags::NUW, Opcode::ZExt);
let stats = hoisted(&mut func);
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, crate::trip::RESTS_ON_APPROACHING), 1);
let other = stats.count(Kind::Missed, crate::trip::NOT_COUNTED);
assert_eq!(other, 0, "and not as a count nobody worked out");
}
#[test]
fn a_check_on_an_address_the_analysis_cannot_follow_says_so() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::PTR]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let head = func.create_block();
let done = func.create_block();
let array = func.append_param(entry, Type::PTR);
let counter = func.append_param(head, Type::int(64));
let zero = Builder::new(&mut func, entry).iconst(Type::int(64), 0);
Builder::new(&mut func, entry).jump(head, &[zero]);
let mut build = Builder::new(&mut func, head);
let by = build.iconst(Type::int(64), WIDTH);
let scaled = build.binary(Opcode::Mul, counter, by, Flags::NSW);
let args = build.func().push_values(&[array, scaled]);
let slot = build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR);
let info = MemInfo {
size: 8,
align: 8,
order: MemOrder::NotAtomic,
tbaa: None,
owns: 0,
restrict: Restrict::NONE,
};
let loaded = build.load(Type::PTR, slot, info, Flags::NONE);
check(&mut build, loaded, 4, 4);
let one = build.iconst(Type::int(64), 1);
let next = build.binary(Opcode::Add, counter, one, Flags::NSW);
let limit = build.iconst(Type::int(64), 16);
let again = build.icmp(IntPred::Slt, next, limit);
build.br_if(again, head, &[next], done, &[]);
Builder::new(&mut func, done).ret(&[]);
let stats = hoisted(&mut func);
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, super::NOT_FOLLOWED), 1);
assert_eq!(stats.count(Kind::Missed, super::NOT_A_SWEEP), 0, "and not as the other one");
assert_eq!(checks(&func).len(), 1, "the check is still in the body");
}
#[test]
fn a_loop_with_a_call_in_it_keeps_its_check() {
let (mut names, mut func, blocks) = walking(16, WIDTH, 4, 4);
let head = blocks[1];
let term = func.terminator(head).expect("the header branches");
let callee = names.intern("might_not_return");
let signature = func.add_signature(Signature::new());
let call = Builder::new(&mut func, head).call(callee, signature, &[]);
func.remove_inst(call);
func.insert_before(call, term);
let stats = hoisted(&mut func);
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, super::A_CALL_INSIDE), 1);
}
#[test]
fn a_loop_that_can_be_left_early_keeps_its_check() {
let (mut names, mut func, blocks) = walking(16, WIDTH, 4, 4);
let (head, done) = (blocks[1], blocks[2]);
let split = func.create_block();
let term = func.terminator(head).expect("the header branches");
let mut build = Builder::new(&mut func, head);
let counter = build.func()[head].params[0];
let seven = build.iconst(Type::int(64), 7);
let bail = build.icmp(IntPred::Eq, counter, seven);
let leave = build.br_if(bail, done, &[], split, &[]);
for inst in [inst_of(&func, seven), inst_of(&func, bail), leave] {
func.remove_inst(inst);
func.insert_before(inst, term);
}
let rest: Vec<Inst> = func.insts(head).skip_while(|&inst| inst != term).collect();
for inst in rest {
func.remove_inst(inst);
Builder::new(&mut func, split).func();
func.append_inst(split, inst);
}
let stats = hoisted(&mut func);
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, super::ANOTHER_WAY_OUT), 1);
sound(&func, &mut names);
}
#[test]
fn a_check_an_iteration_can_finish_without_reaching_stays() {
let (mut names, mut func, _) = guarded();
let stats = hoisted(&mut func);
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, super::NOT_EVERY_TIME), 1);
sound(&func, &mut names);
}
fn guarded() -> (Interner, Func, Vec<Block>) {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::PTR, Type::int(64)]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let head = func.create_block();
let read = func.create_block();
let tail = func.create_block();
let done = func.create_block();
let array = func.append_param(entry, Type::PTR);
let choice = func.append_param(entry, Type::int(64));
let counter = func.append_param(head, Type::int(64));
let zero = Builder::new(&mut func, entry).iconst(Type::int(64), 0);
Builder::new(&mut func, entry).jump(head, &[zero]);
let mut build = Builder::new(&mut func, head);
let take = build.icmp(IntPred::Ne, choice, zero);
build.br_if(take, read, &[], tail, &[]);
let mut build = Builder::new(&mut func, read);
let by = build.iconst(Type::int(64), 4);
let scaled = build.binary(Opcode::Mul, counter, by, Flags::NSW);
let args = build.func().push_values(&[array, scaled]);
let pointer = build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR);
check(&mut build, pointer, 4, 4);
build.jump(tail, &[]);
let mut build = Builder::new(&mut func, tail);
let one = build.iconst(Type::int(64), 1);
let next = build.binary(Opcode::Add, counter, one, Flags::NSW);
let limit = build.iconst(Type::int(64), 16);
let again = build.icmp(IntPred::Slt, next, limit);
build.br_if(again, head, &[next], done, &[]);
Builder::new(&mut func, done).ret(&[]);
(names, func, vec![entry, head, read, tail, done])
}
#[test]
fn a_check_whose_step_does_not_keep_its_alignment_stays() {
let (_, mut func, _) = walking(8, 3, 4, 4);
let stats = hoisted(&mut func);
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, super::MISALIGNED), 1);
}
#[test]
fn a_check_whose_address_walks_backwards_stays() {
let (_, mut func, _) = walking(8, -4, 4, 4);
let stats = hoisted(&mut func);
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, super::BACKWARDS), 1);
}
#[test]
fn a_check_through_a_pointer_the_loop_does_not_move_comes_out_as_it_stands() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::PTR]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let head = func.create_block();
let done = func.create_block();
let array = func.append_param(entry, Type::PTR);
let counter = func.append_param(head, Type::int(64));
let zero = Builder::new(&mut func, entry).iconst(Type::int(64), 0);
Builder::new(&mut func, entry).jump(head, &[zero]);
let mut build = Builder::new(&mut func, head);
check(&mut build, array, 4, 4);
let one = build.iconst(Type::int(64), 1);
let next = build.binary(Opcode::Add, counter, one, Flags::NSW);
let limit = build.iconst(Type::int(64), 16);
let again = build.icmp(IntPred::Slt, next, limit);
build.br_if(again, head, &[next], done, &[]);
Builder::new(&mut func, done).ret(&[]);
let stats = hoisted(&mut func);
assert!(stats.changed());
assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
let left = checks(&func);
assert_eq!(left.len(), 1, "one check, and it is the one that was put in front");
assert_eq!(extent(&func, left[0].1), 4, "one access, since the address never moved");
sound(&func, &mut names);
}
fn taken_at(func: &mut Func, block: Block, from: Value) {
let check = func
.insts(block)
.find(|&inst| func[inst].opcode == Opcode::CheckBounds)
.expect("the loop checks the address it works out");
let held = func[func[check].args][0];
let made = inst_of(func, held);
func[made].args = func.push_values(&[from]);
}
fn shifted(func: &mut Func, block: Block) {
let add = func
.insts(block)
.find(|&inst| func[inst].opcode == Opcode::PtrAdd)
.expect("the loop works out an address");
let (array, scaled) = (func[func[add].args][0], func[func[add].args][1]);
let mut build = Builder::new(func, block);
let by = build.iconst(Type::int(64), WIDTH);
let along = build.binary(Opcode::Add, scaled, by, Flags::NSW);
for value in [by, along] {
let inst = inst_of(func, value);
func.remove_inst(inst);
func.insert_before(inst, add);
}
func[add].args = func.push_values(&[array, along]);
}
#[test]
fn a_check_whose_capability_was_taken_where_the_walk_starts_is_hoisted_like_any_other() {
let (mut names, mut func, blocks) = walking(16, WIDTH, 4, 4);
let array = func[blocks[0]].params[0];
taken_at(&mut func, blocks[1], array);
let stats = hoisted(&mut func);
assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
assert_eq!(checks(&func).len(), 1, "one check, and it is the one that was put in front");
sound(&func, &mut names);
}
#[test]
fn a_check_whose_capability_was_taken_behind_where_the_walk_starts_stays() {
let (_, mut func, blocks) = walking(16, WIDTH, 4, 4);
let array = func[blocks[0]].params[0];
shifted(&mut func, blocks[1]);
taken_at(&mut func, blocks[1], array);
let stats = hoisted(&mut func);
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, super::NOT_ITS_CAPABILITY), 1);
}
#[test]
fn a_check_that_already_covers_a_computed_range_is_left_where_it_is() {
let (mut names, mut func, blocks) = walking(16, WIDTH, 4, 4);
let head = blocks[1];
let check = func
.insts(head)
.find(|&inst| func[inst].opcode == Opcode::CheckBounds)
.expect("the body has a check");
let [capability, pointer] = func[func[check].args] else { panic!("two operands") };
let bytes = Builder::new(&mut func, head).iconst(Type::int(64), 64);
let moved = inst_of(&func, bytes);
func.remove_inst(moved);
func.insert_before(moved, check);
func[check].args = func.push_values(&[capability, pointer, bytes]);
let stats = hoisted(&mut func);
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, super::ALREADY_COMPUTED), 1);
sound(&func, &mut names);
}
#[test]
fn fuel_stops_the_hoist_where_it_stands() {
let (mut names, mut func, _) = walking(16, WIDTH, 4, 4);
let mut an = crate::machine::fixtures::analyses();
Canon.run(&mut func, &mut an, &mut Fuel::unlimited());
let stats = Hoist.run(&mut func, &mut an, &mut Fuel::of(0));
assert_eq!(stats.count(Kind::Optimized, HOISTED), 0);
assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
assert_eq!(checks(&func).len(), 1, "and the check is where it was");
sound(&func, &mut names);
}
#[test]
fn a_loop_that_sweeps_further_than_the_rule_goes_keeps_its_check() {
let (_, mut func, _) = walking(2, 1 << 33, 4, 1);
let stats = hoisted(&mut func);
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, super::TOO_WIDE), 1);
}
fn planed(kind: Opcode, writing: Option<Opcode>) -> (Interner, Func) {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::PTR]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let head = func.create_block();
let done = func.create_block();
let array = func.append_param(entry, Type::PTR);
let counter = func.append_param(head, Type::int(64));
let zero = Builder::new(&mut func, entry).iconst(Type::int(64), 0);
Builder::new(&mut func, entry).jump(head, &[zero]);
let mut build = Builder::new(&mut func, head);
let by = build.iconst(Type::int(64), WIDTH);
let scaled = build.binary(Opcode::Mul, counter, by, Flags::NSW);
let args = build.func().push_values(&[array, scaled]);
let pointer = build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR);
let info = MemInfo {
size: 4,
align: 4,
order: MemOrder::NotAtomic,
tbaa: None,
owns: 0,
restrict: Restrict::NONE,
};
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]);
let extra = Extra::Mem(build.func().add_mem(info));
build.inst(InstData { args, extra, ..InstData::new(kind) }, &[]);
if let Some(writing) = writing {
let bytes = build.iconst(Type::int(64), 4);
let args = match writing {
Opcode::MetaTypeCopy | Opcode::MetaInitCopy => {
build.func().push_values(&[pointer, pointer, bytes])
}
_ => build.func().push_values(&[pointer, bytes]),
};
build.inst(InstData { args, ..InstData::new(writing) }, &[]);
}
let one = build.iconst(Type::int(64), 1);
let next = build.binary(Opcode::Add, counter, one, Flags::NSW);
let limit = build.iconst(Type::int(64), 16);
let again = build.icmp(IntPred::Slt, next, limit);
build.br_if(again, head, &[next], done, &[]);
Builder::new(&mut func, done).ret(&[]);
(names, func)
}
fn kinds(func: &Func, kind: Opcode) -> Vec<(Block, Inst)> {
func.blocks()
.flat_map(|block| func.insts(block).map(move |inst| (block, inst)).collect::<Vec<_>>())
.filter(|&(_, inst)| func[inst].opcode == kind)
.collect()
}
#[test]
fn a_type_check_that_walks_a_counted_loop_comes_out_of_it() {
let (mut names, mut func) = planed(Opcode::CheckType, None);
let stats = hoisted(&mut func);
assert_eq!(stats.count(Kind::Optimized, HOISTED_TYPE), 1);
let left = kinds(&func, Opcode::CheckType);
assert_eq!(left.len(), 1);
assert_eq!(extent(&func, left[0].1), 64, "fifteen steps of four, plus the last read");
sound(&func, &mut names);
}
#[test]
fn an_init_check_that_walks_a_counted_loop_comes_out_of_it() {
let (mut names, mut func) = planed(Opcode::CheckInit, None);
let stats = hoisted(&mut func);
assert_eq!(stats.count(Kind::Optimized, HOISTED_INIT), 1);
let left = kinds(&func, Opcode::CheckInit);
assert_eq!(left.len(), 1);
assert_eq!(extent(&func, left[0].1), 64);
sound(&func, &mut names);
}
#[test]
fn a_loop_that_writes_the_init_plane_keeps_its_init_check() {
let (mut names, mut func) = planed(Opcode::CheckInit, Some(Opcode::MetaInit));
let stats = hoisted(&mut func);
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, super::WRITES_THE_INIT_PLANE), 1);
assert_eq!(kinds(&func, Opcode::CheckInit).len(), 1, "and it is where it was");
sound(&func, &mut names);
}
#[test]
fn a_loop_that_writes_the_type_plane_keeps_its_type_check() {
let (mut names, mut func) = planed(Opcode::CheckType, Some(Opcode::MetaTypeCopy));
let stats = hoisted(&mut func);
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, super::WRITES_THE_TYPE_PLANE), 1);
assert_eq!(kinds(&func, Opcode::CheckType).len(), 1);
sound(&func, &mut names);
}
#[test]
fn a_loop_that_writes_a_plane_still_gives_up_its_bounds_check() {
let (mut names, mut func) = planed(Opcode::CheckBounds, Some(Opcode::MetaInit));
let stats = hoisted(&mut func);
assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
assert_eq!(checks(&func).len(), 1);
assert_eq!(extent(&func, checks(&func)[0].1), 64);
sound(&func, &mut names);
}
#[test]
fn a_lifetime_starting_in_the_loop_keeps_both_planes_checks() {
for kind in [Opcode::CheckInit, Opcode::CheckType] {
let (_, mut func) = planed(kind, Some(Opcode::MetaBegin));
let stats = hoisted(&mut func);
assert!(!stats.changed(), "{kind:?}");
}
}
#[derive(Clone, Copy, Debug)]
enum Into {
Allocated,
Parameter,
Reallocated,
}
fn copying(kind: Opcode, writing: Opcode, into: Into) -> (Interner, Func) {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::PTR]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let head = func.create_block();
let done = func.create_block();
let given = func.append_param(entry, Type::PTR);
let counter = func.append_param(head, Type::int(64));
let word = Type::int(64);
let mut build = Builder::new(&mut func, entry);
let sized = Signature::new().with_params(&[word]).with_returns(&[Type::PTR]);
let sized = build.func().add_signature(sized);
let resized = Signature::new().with_params(&[Type::PTR, word]).with_returns(&[Type::PTR]);
let resized = build.func().add_signature(resized);
let bytes = build.iconst(word, 64);
let mut allocate = |build: &mut Builder<'_>, name: &str, sig, args: &[Value]| {
let call = build.call(names.intern(name), sig, args);
let at = build.func();
at[call].flags |= Flags::HEAP;
at[call].results().next().expect("a call that gives back a pointer")
};
let from = allocate(&mut build, "malloc", sized, &[bytes]);
let to = match into {
Into::Allocated => allocate(&mut build, "malloc", sized, &[bytes]),
Into::Parameter => given,
Into::Reallocated => allocate(&mut build, "realloc", resized, &[from, bytes]),
};
let zero = build.iconst(word, 0);
build.jump(head, &[zero]);
let mut build = Builder::new(&mut func, head);
let by = build.iconst(word, WIDTH);
let scaled = build.binary(Opcode::Mul, counter, by, Flags::NSW);
let args = build.func().push_values(&[from, scaled]);
let read = build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR);
let args = build.func().push_values(&[to, scaled]);
let wrote = build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR);
let info = MemInfo {
size: 4,
align: 4,
order: MemOrder::NotAtomic,
tbaa: None,
owns: 0,
restrict: Restrict::NONE,
};
let args = build.func().push_values(&[read]);
let capability = build.value(InstData { args, ..InstData::new(Opcode::CapOf) }, Type::CAP);
let args = build.func().push_values(&[capability, read]);
let extra = Extra::Mem(build.func().add_mem(info));
build.inst(InstData { args, extra, ..InstData::new(kind) }, &[]);
let length = build.iconst(word, 4);
let args = match writing {
Opcode::MetaTypeCopy | Opcode::MetaInitCopy => {
build.func().push_values(&[wrote, read, length])
}
_ => build.func().push_values(&[wrote, length]),
};
build.inst(InstData { args, ..InstData::new(writing) }, &[]);
let one = build.iconst(word, 1);
let next = build.binary(Opcode::Add, counter, one, Flags::NSW);
let limit = build.iconst(word, 16);
let again = build.icmp(IntPred::Slt, next, limit);
build.br_if(again, head, &[next], done, &[]);
Builder::new(&mut func, done).ret(&[]);
(names, func)
}
#[test]
fn a_copy_between_two_allocations_takes_the_plane_checks_on_its_source_out() {
let cases = [
(Opcode::CheckInit, Opcode::MetaInit, HOISTED_INIT),
(Opcode::CheckInit, Opcode::MetaInitCopy, HOISTED_INIT),
(Opcode::CheckType, Opcode::MetaTypeCopy, HOISTED_TYPE),
(Opcode::CheckInit, Opcode::MetaBegin, HOISTED_INIT),
];
for (kind, writing, done) in cases {
let (_, mut func) = copying(kind, writing, Into::Allocated);
let stats = hoisted(&mut func);
assert_eq!(stats.count(Kind::Optimized, done), 1, "{kind:?} under {writing:?}");
let left = kinds(&func, kind);
assert_eq!(left.len(), 1);
assert_eq!(extent(&func, left[0].1), 64);
}
}
#[test]
fn a_copy_into_somewhere_that_may_be_its_source_keeps_its_plane_checks() {
for into in [Into::Parameter, Into::Reallocated] {
for (kind, writing, why) in [
(Opcode::CheckInit, Opcode::MetaInit, super::WRITES_THE_INIT_PLANE),
(Opcode::CheckType, Opcode::MetaTypeCopy, super::WRITES_THE_TYPE_PLANE),
] {
let (_, mut func) = copying(kind, writing, into);
let stats = hoisted(&mut func);
assert!(!stats.changed(), "{kind:?} into {into:?}");
assert_eq!(stats.count(Kind::Missed, why), 1);
}
}
}
fn rowed(narrow: bool) -> (Interner, Func, Vec<Block>) {
let mut names = Interner::new();
let row_ty = if narrow { Type::int(32) } else { Type::int(64) };
let signature = Signature::new().with_params(&[Type::PTR, row_ty]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let head = func.create_block();
let done = func.create_block();
let array = func.append_param(entry, Type::PTR);
let row = func.append_param(entry, row_ty);
let counter = func.append_param(head, Type::int(64));
let zero = Builder::new(&mut func, entry).iconst(Type::int(64), 0);
Builder::new(&mut func, entry).jump(head, &[zero]);
let mut build = Builder::new(&mut func, head);
let wide = if narrow { build.unary(Opcode::SExt, row, Type::int(64)) } else { row };
let stride = build.iconst(Type::int(64), 64);
let down = build.binary(Opcode::Mul, wide, stride, Flags::NSW);
let by = build.iconst(Type::int(64), WIDTH);
let along = build.binary(Opcode::Mul, counter, by, Flags::NSW);
let sum = build.binary(Opcode::Add, down, along, Flags::NSW);
let args = build.func().push_values(&[array, sum]);
let pointer = build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR);
check(&mut build, pointer, 4, 4);
let one = build.iconst(Type::int(64), 1);
let next = build.binary(Opcode::Add, counter, one, Flags::NSW);
let limit = build.iconst(Type::int(64), 16);
let again = build.icmp(IntPred::Slt, next, limit);
build.br_if(again, head, &[next], done, &[]);
Builder::new(&mut func, done).ret(&[]);
(names, func, vec![entry, head, done])
}
#[test]
fn a_walk_along_a_row_starts_where_the_preheader_works_it_out() {
let (mut names, mut func, _) = rowed(false);
let stats = hoisted(&mut func);
assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
let left = checks(&func);
assert_eq!(left.len(), 1, "one check, and it is the one that was put in front");
assert_eq!(extent(&func, left[0].1), 64, "fifteen steps of four, plus the last read");
sound(&func, &mut names);
}
#[test]
fn a_row_offset_the_program_reads_wide_is_read_wide_in_front_of_the_loop_too() {
let (mut names, mut func, _) = rowed(true);
let stats = hoisted(&mut func);
assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
let left = checks(&func);
assert_eq!(left.len(), 1);
assert_eq!(extent(&func, left[0].1), 64);
let (block, _) = left[0];
let widened = func.insts(block).any(|inst| func[inst].opcode == Opcode::SExt);
assert!(widened, "the preheader works the row offset out at the width the address wants");
sound(&func, &mut names);
}
#[test]
fn a_walk_along_a_row_keeps_a_bounds_check_whose_capability_is_about_the_whole_array() {
let (_, mut func, blocks) = rowed(false);
let array = func[blocks[0]].params[0];
taken_at(&mut func, blocks[1], array);
let stats = hoisted(&mut func);
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, super::NOT_ITS_CAPABILITY), 1);
}
#[test]
fn a_plane_check_over_a_walk_that_leaves_gaps_is_handed_the_step() {
let (mut names, mut func, _) = walking(8, 16, 4, 4);
assert_eq!(hoisted(&mut func).count(Kind::Optimized, HOISTED), 1);
let left = checks(&func);
assert_eq!(func[func[left[0].1].args].len(), 2);
assert_eq!(extent(&func, left[0].1), 116);
sound(&func, &mut names);
for (kind, done) in [(Opcode::CheckInit, HOISTED_INIT), (Opcode::CheckType, HOISTED_TYPE)] {
let (mut names, mut func, blocks) = walking(8, 16, 4, 4);
let check = func
.insts(blocks[1])
.find(|&inst| func[inst].opcode == Opcode::CheckBounds)
.expect("the loop checks the address it works out");
func[check].opcode = kind;
assert_eq!(hoisted(&mut func).count(Kind::Optimized, done), 1, "{kind:?}");
let left: Vec<Inst> = func
.blocks()
.flat_map(|block| func.insts(block).collect::<Vec<_>>())
.filter(|&inst| func[inst].opcode == kind)
.collect();
assert_eq!(left.len(), 1, "{kind:?}");
assert_ne!(func.block_of(left[0]), Some(blocks[1]), "{kind:?}");
let &[_, _, span, step] = &func[func[left[0]].args] else {
panic!("{kind:?} carries the span and the step");
};
assert_eq!(crate::discharge::constant(&func, span), Some(116), "{kind:?}");
assert_eq!(crate::discharge::constant(&func, step), Some(16), "{kind:?}");
assert_eq!(extent(&func, left[0]), 4, "{kind:?}");
sound(&func, &mut names);
}
}
#[test]
fn the_type_check_and_the_init_check_of_one_read_come_out_with_the_same_operands() {
for (trips, step) in [(8, 16), (8, 4)] {
let (mut names, mut func, blocks) = walking(trips, step, 4, 4);
let check = func
.insts(blocks[1])
.find(|&inst| func[inst].opcode == Opcode::CheckBounds)
.expect("the loop checks the address it works out");
func[check].opcode = Opcode::CheckType;
let data = func[check];
let args = func[data.args].to_vec();
let args = func.push_values(&args);
let init = InstData { args, opcode: Opcode::CheckInit, ..data };
let init = func.create_inst(init, &[], func.span(check));
func.insert_after(init, check);
let stats = hoisted(&mut func);
assert_eq!(stats.count(Kind::Optimized, HOISTED_TYPE), 1);
assert_eq!(stats.count(Kind::Optimized, HOISTED_INIT), 1);
let out: Vec<Inst> = func
.blocks()
.flat_map(|block| func.insts(block).collect::<Vec<_>>())
.filter(|&inst| matches!(func[inst].opcode, Opcode::CheckType | Opcode::CheckInit))
.collect();
assert_eq!(out.len(), 2);
assert_eq!(func[func[out[0]].args], func[func[out[1]].args], "step {step}");
sound(&func, &mut names);
}
}
#[test]
fn a_start_worked_out_narrower_than_the_address_arithmetic_is_refused() {
let mut names = Interner::new();
let signature = Signature::new().with_params(&[Type::int(32)]);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let short = func.append_param(entry, Type::int(32));
Builder::new(&mut func, entry).ret(&[]);
let narrow = super::Plain { value: Some(short), read: None, scale: 1, offset: 0 };
assert_eq!(super::plain_enough(&func, narrow), Err(super::START_NOT_A_WORD));
let none = super::Plain { value: Some(short), read: None, scale: 0, offset: 8 };
assert_eq!(super::plain_enough(&func, none), Ok(()));
}
fn inst_of(func: &Func, value: Value) -> Inst {
super::inst_of(func, value)
}
}