use std::collections::{HashMap, HashSet};
use rucc_cost::heuristics;
use rucc_ir::{
Block, BlockCall, Builder, Extra, Flags, Func, Inst, InstData, IntPred, Opcode, Type, Value,
};
use crate::canon;
use crate::cfg::Cfg;
use crate::copy;
use crate::discharge::{Question, operand_of, yes};
use crate::dom::Dominators;
use crate::loops::{LoopId, Loops};
use crate::rules::safety;
use crate::scev::{Evolution, Plain, Reading, Scev};
use crate::trip::{Around, counted, covered, inst_of};
use crate::{Analyses, Fuel, Pass, Preserved, Stats};
const SPLIT: &str = "loop split, the iterations in front of the first one that could fail a check \
run without them";
const CLOSED_HERE: &str = "loop put back into closed form, a value it defines is read after it and both halves define one";
const NO_FUEL: &str = "loop left alone, 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 A_LOOP_INSIDE: &str = "loop left alone, it has another loop inside it";
const MANY_LATCHES: &str = "loop left alone, it goes back to its header from more than one place";
const A_CALL_INSIDE: &str = "loop left alone, a call in it might free what the loop is reading";
const ENDS_A_LIFETIME: &str = "loop left alone, something in it ends a lifetime";
const NOT_COPYABLE: &str = "loop left alone, something in it carries a side table this cannot copy";
const ESCAPES: &str = "loop left alone, a value it defines is read outside it";
const TOO_BIG: &str = "loop left alone, the two halves would be more code than the limit allows";
const NOT_A_SWEEP: &str = "check kept in both halves, its address does not walk the loop by a \
constant";
const NOT_FOLLOWED: &str = "check kept in both halves, what its address does round the loop is not \
something the analysis follows";
const MISALIGNED: &str =
"check kept in both halves, its step is not a whole number of its alignment";
const ALREADY_COMPUTED: &str =
"check kept in both halves, how many bytes it covers is a number only the program has";
const NOT_PROVED: &str = "check kept in both halves, no rule in the safety namespace says an offset inside the window is \
an access inside the object";
#[derive(Debug)]
pub struct Split;
impl Pass for Split {
fn name(&self) -> &'static str {
"split"
}
fn describe(&self) -> &'static str {
"a loop becomes a run of iterations with no checks in it and the rest of the loop with them"
}
fn preserves(&self) -> Preserved {
Preserved::NONE
}
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).clone();
let loops = an.loops(func).clone();
if loops.count() == 0 {
return stats;
}
let mut plans = planned(func, &cfg, &loops, &mut stats);
let dom = an.dominators(func).clone();
let repairs = repaired(func, &cfg, &dom, &loops, &plans, fuel);
if repairs.made > 0 {
stats = Stats::new();
plans = planned(func, &cfg, &loops, &mut stats);
for _ in 0..repairs.worked {
stats.optimized(CLOSED_HERE);
}
}
plans.retain(|plan| {
if leaving(func, plan) {
stats.missed(ESCAPES);
return false;
}
true
});
let mut changed = false;
for plan in plans {
if !fuel.take() {
stats.missed(NO_FUEL);
continue;
}
apply(func, &plan);
stats.optimized(SPLIT);
changed = true;
}
if changed {
an.clear();
}
stats
}
}
#[derive(Debug)]
struct Sweep {
check: Inst,
base: Value,
apart: Plain,
step: i128,
reach: i128,
}
#[derive(Debug)]
struct Plan {
id: LoopId,
preheader: Block,
header: Block,
latch: Block,
body: Vec<Block>,
around: Around,
sweeps: Vec<Sweep>,
}
fn sweep(
func: &Func,
cfg: &Cfg,
loops: &Loops,
scev: &mut Scev<'_>,
id: LoopId,
plans: &mut Vec<Plan>,
stats: &mut Stats,
) {
let body = loops.blocks(id).to_vec();
let checks: Vec<Inst> = body
.iter()
.flat_map(|&block| func.insts(block).collect::<Vec<Inst>>())
.filter(|&inst| matches!(func[inst].opcode, Opcode::CheckBounds | Opcode::CheckLive))
.collect();
if checks.is_empty() {
return;
}
let (preheader, latch) = match shaped(func, cfg, loops, id, &body) {
Ok(shape) => shape,
Err(why) => {
stats.missed(why);
return;
}
};
let around =
counted(scev, id).unwrap_or(Around::Number(i128::from(crate::scev::ASSUMED_ITERATIONS)));
let mut sweeps = Vec::new();
for check in checks {
match walked(func, scev, id, check) {
Ok(sweep) => sweeps.push(sweep),
Err(why) => stats.missed(why),
}
}
if sweeps.is_empty() {
return;
}
plans.push(Plan { id, preheader, header: loops.header(id), latch, body, around, sweeps });
}
fn shaped(
func: &Func,
cfg: &Cfg,
loops: &Loops,
id: LoopId,
body: &[Block],
) -> 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(MANY_LATCHES);
};
for &block in body {
if loops.innermost(block) != Some(id) {
return Err(A_LOOP_INSIDE);
}
for inst in func.insts(block) {
match func[inst].opcode {
Opcode::Call | Opcode::CallIndirect | Opcode::TailCall
if !func[inst].flags.contains(Flags::NOFREE) =>
{
return Err(A_CALL_INSIDE);
}
Opcode::InlineAsm | Opcode::MetaEnd | Opcode::MetaTransfer => {
return Err(ENDS_A_LIFETIME);
}
_ => {}
}
if !copy::copyable(func, inst) {
return Err(NOT_COPYABLE);
}
}
}
let size = body.iter().map(|&block| func.insts(block).count()).sum::<usize>();
if size > heuristics::SPLIT_MAX_INSNS as usize {
return Err(TOO_BIG);
}
Ok((preheader, *latch))
}
fn planned(func: &Func, cfg: &Cfg, loops: &Loops, stats: &mut Stats) -> Vec<Plan> {
let mut plans = Vec::new();
let mut scev = Scev::new(func, cfg, loops);
for id in loops.all() {
sweep(func, cfg, loops, &mut scev, id, &mut plans, stats);
}
plans
}
struct Repairs {
made: usize,
worked: usize,
}
fn repaired(
func: &mut Func,
cfg: &Cfg,
dom: &Dominators,
loops: &Loops,
plans: &[Plan],
fuel: &mut Fuel,
) -> Repairs {
let mut repairs = Repairs { made: 0, worked: 0 };
for plan in plans {
if !leaving(func, plan) {
continue;
}
let mut wrote = false;
while let Some(job) = canon::leaked(func, cfg, dom, loops, plan.id) {
if !fuel.take() {
break;
}
canon::close(func, &job);
wrote = true;
}
if !wrote {
continue;
}
repairs.made += 1;
if !leaving(func, plan) {
repairs.worked += 1;
}
}
repairs
}
fn leaving(func: &Func, plan: &Plan) -> bool {
let inside: HashSet<Block> = plan.body.iter().copied().collect();
escapes(func, &plan.body, &inside)
}
fn escapes(func: &Func, body: &[Block], inside: &HashSet<Block>) -> bool {
let mut defined: HashSet<Value> = HashSet::new();
for &block in body {
defined.extend(func[block].params.iter().copied());
for inst in func.insts(block) {
defined.extend(func[inst].results());
}
}
for block in func.blocks() {
if inside.contains(&block) {
continue;
}
for inst in func.insts(block) {
if func[func[inst].args].iter().any(|value| defined.contains(value)) {
return true;
}
for call in func.successors(inst) {
if func[call.args].iter().any(|value| defined.contains(value)) {
return true;
}
}
}
}
false
}
fn walked(
func: &Func,
scev: &mut Scev<'_>,
id: LoopId,
check: Inst,
) -> Result<Sweep, &'static str> {
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);
};
if operand_of(func, capability, Opcode::CapOf, 0) != Some(pointer) {
return Err(NOT_A_SWEEP);
}
let (reach, align) = match func[check].extra {
Extra::Mem(held) => (i128::from(func[held].size), i128::from(func[held].align)),
_ => (1, 1),
};
let (start, step) = match scev.evolution(id, pointer) {
Evolution::Affine(chrec) => {
let Some(step) = chrec.step.as_number() else {
return Err(NOT_A_SWEEP);
};
(chrec.base, step)
}
Evolution::Invariant(base) => (base, 0),
_ => return Err(NOT_FOLLOWED),
};
let (base, apart) = match (start.plain(), start.on()) {
(Some(at @ Plain { value: Some(base), read: None, scale: 1, .. }), _) => {
(base, Plain { value: None, read: None, scale: 0, offset: at.offset })
}
(_, Some((base, apart))) if walks(func, base, apart) => (base, apart),
_ => return Err(NOT_A_SWEEP),
};
if step != 0 && step % align != 0 {
return Err(MISALIGNED);
}
if !windowed(reach, step < 0) {
return Err(NOT_PROVED);
}
Ok(Sweep { check, base, apart, step, reach })
}
fn walks(func: &Func, base: Value, apart: Plain) -> bool {
let word = Type::int(64);
let Some(value) = apart.value else { return false };
if !func[base].ty.is_ptr() {
return false;
}
match apart.read {
None => func[value].ty == word,
Some(read) => read.to == word && func[value].ty.is_int() && func[value].ty.bits() < 64,
}
}
fn apply(func: &mut Func, plan: &Plan) {
let mut renamed: HashMap<Value, Value> = HashMap::new();
let copies = copy::blocks(func, &plan.body, &mut renamed);
let slow = copies[&plan.header];
let Choice { ok, windows } = limited(func, plan);
if windows.is_empty() {
let term = func.terminator(plan.preheader).expect("a preheader ends in a jump");
let args = copy::edge_args(func, term, plan.header);
func.remove_inst(term);
Builder::new(func, plan.preheader).br_if(ok, plan.header, &args, slow, &args);
take(func, plan);
return;
}
let word = Type::int(64);
let types: Vec<Type> = func[plan.header].params.iter().map(|¶m| func[param].ty).collect();
let guard = func.create_block();
let offsets: Vec<Value> = windows.iter().map(|_| func.append_param(guard, word)).collect();
let carried: Vec<Value> = types.iter().map(|&ty| func.append_param(guard, ty)).collect();
let mut build = Builder::new(func, guard);
let mut inside: Option<Value> = None;
for (&offset, window) in offsets.iter().zip(&windows) {
let under = build.icmp(IntPred::Ule, offset, window.bound);
inside = Some(match inside {
None => under,
Some(so_far) => build.binary(Opcode::And, so_far, under, Flags::NONE),
});
}
let inside = inside.expect("a plan with a window has at least one of them");
build.br_if(inside, plan.header, &carried, slow, &carried);
let term = func.terminator(plan.preheader).expect("a preheader ends in a jump to the header");
let args = copy::edge_args(func, term, plan.header);
func.remove_inst(term);
let mut build = Builder::new(func, plan.preheader);
let zero = build.iconst(word, 0);
let mut into: Vec<Value> = offsets.iter().map(|_| zero).collect();
into.extend_from_slice(&args);
build.br_if(ok, guard, &into, slow, &args);
let term = func.terminator(plan.latch).expect("a latch ends in a branch back to the header");
let mut build = Builder::new(func, plan.latch);
let mut made = Vec::new();
let mut next = Vec::new();
for (&offset, window) in offsets.iter().zip(&windows) {
let by = build.iconst(word, window.step);
made.push(by);
let walked = build.binary(Opcode::Add, offset, by, Flags::NUW);
made.push(walked);
next.push(walked);
}
for value in made {
let inst = inst_of(func, value);
func.remove_inst(inst);
func.insert_before(inst, term);
}
route(func, term, plan.header, guard, &next);
take(func, plan);
}
fn take(func: &mut Func, plan: &Plan) {
for sweep in &plan.sweeps {
func.remove_inst(sweep.check);
}
}
fn route(func: &mut Func, term: Inst, from: Block, to: Block, first: &[Value]) {
for at in func.target_list(term).iter() {
let call = func[at];
if call.block != from {
continue;
}
let mut args = first.to_vec();
args.extend_from_slice(&func[call.args]);
let args = func.push_values(&args);
func.set_block_call(at, BlockCall { block: to, args });
}
}
struct Window {
step: i128,
bound: Value,
}
struct Choice {
ok: Value,
windows: Vec<Window>,
}
fn limited(func: &mut Func, plan: &Plan) -> Choice {
let term = func.terminator(plan.preheader).expect("a preheader ends in a jump to the header");
let mut made = Vec::new();
let mut build = Builder::new(func, plan.preheader);
let mut ok: Option<Value> = None;
let mut windows: Vec<Window> = Vec::new();
for sweep in &plan.sweeps {
let around = if sweep.step == 0 { Around::Number(0) } else { plan.around };
let (window, zero) = spare(&mut build, &mut made, sweep, around);
let fits = build.icmp(IntPred::Sge, window, zero);
made.push(fits);
ok = Some(match ok {
None => fits,
Some(so_far) => {
let both = build.binary(Opcode::And, so_far, fits, Flags::NONE);
made.push(both);
both
}
});
if sweep.step == 0 {
continue;
}
let stride = sweep.step.abs();
match windows.iter().position(|held| held.step == stride) {
Some(at) => {
let bound = windows[at].bound;
let smaller = build.icmp(IntPred::Ult, window, bound);
made.push(smaller);
let least = build.select(smaller, window, bound);
made.push(least);
windows[at].bound = least;
}
None => windows.push(Window { step: stride, bound: window }),
}
}
let ok = ok.expect("a plan holds at least one check");
for window in &mut windows {
window.bound = bounded(&mut build, &mut made, window.step, window.bound);
}
for value in made {
let inst = inst_of(func, value);
func.remove_inst(inst);
func.insert_before(inst, term);
}
Choice { ok, windows }
}
fn bounded(build: &mut Builder<'_>, made: &mut Vec<Value>, step: i128, bound: Value) -> Value {
let word = Type::int(64);
let room = build.iconst(word, i128::from(i64::MAX) - step);
made.push(room);
let over = build.icmp(IntPred::Ugt, bound, room);
made.push(over);
let held = build.select(over, room, bound);
made.push(held);
held
}
fn windowed(reach: i128, down: bool) -> 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 head = if down { "swept.down.sym.i64" } else { "swept.sym.i64" };
let term = question.app(head, &[at, span, far, reach, delta]);
match safety::TABLE.find(&question, term) {
Some(found) => yes(&safety::TABLE, found.rule),
None => false,
}
}
fn displacement(build: &mut Builder<'_>, made: &mut Vec<Value>, apart: Plain) -> Option<Value> {
let word = Type::int(64);
let mut sum = match apart.value.filter(|_| apart.scale != 0) {
None => {
return (apart.offset != 0).then(|| {
let by = build.iconst(word, apart.offset);
made.push(by);
by
});
}
Some(value) => value,
};
if let Some(read) = apart.read {
let widen = match read.reading {
Reading::Signed => Opcode::SExt,
Reading::Unsigned => Opcode::ZExt,
};
sum = build.unary(widen, sum, read.to);
made.push(sum);
}
if apart.scale != 1 {
let by = build.iconst(word, apart.scale);
made.push(by);
sum = build.binary(Opcode::Mul, sum, by, Flags::NONE);
made.push(sum);
}
if apart.offset != 0 {
let by = build.iconst(word, apart.offset);
made.push(by);
sum = build.binary(Opcode::Add, sum, by, Flags::NONE);
made.push(sum);
}
Some(sum)
}
fn spare(
build: &mut Builder<'_>,
made: &mut Vec<Value>,
sweep: &Sweep,
around: Around,
) -> (Value, Value) {
let word = Type::int(64);
let first = match displacement(build, made, sweep.apart) {
None => sweep.base,
Some(by) => {
let args = build.func().push_values(&[sweep.base, by]);
let sum = build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR);
made.push(sum);
sum
}
};
let stride = sweep.step.abs();
let want = match around {
Around::Number(times) => {
let far = times.saturating_mul(stride).saturating_add(sweep.reach);
let far = i64::try_from(far).unwrap_or(i64::MAX);
let bytes = build.iconst(word, i128::from(far));
made.push(bytes);
bytes
}
Around::Computed(count, reading) => {
covered(build, made, count, stride, sweep.reach, reading, Flags::NONE)
}
};
let (asked, at) = if sweep.step < 0 {
let by = build.iconst(word, sweep.reach);
made.push(by);
let args = build.func().push_values(&[first, by]);
let end = build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR);
made.push(end);
(Opcode::CapExtentBack, end)
} else {
(Opcode::CapExtent, first)
};
let args = build.func().push_values(&[at]);
let capability = build.value(InstData { args, ..InstData::new(Opcode::CapOf) }, Type::CAP);
made.push(capability);
let args = build.func().push_values(&[capability, at, want]);
let extent = build.value(InstData { args, ..InstData::new(asked) }, word);
made.push(extent);
let reach = build.iconst(word, sweep.reach);
made.push(reach);
let left = build.binary(Opcode::Sub, extent, reach, Flags::NSW);
made.push(left);
let zero = build.iconst(word, 0);
made.push(zero);
(left, zero)
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{
Block, Builder, Extra, Flags, Func, Inst, InstData, IntPred, MemInfo, MemOrder, Module,
Opcode, Restrict, Signature, Type, Value, verify_func,
};
use rucc_target::{TargetInfo, Triple};
use super::{SPLIT, Split};
use crate::canon::Canon;
use crate::stats::Kind;
use crate::{Fuel, Pass, Stats};
const TRIPS: i128 = 16;
const WIDTH: i128 = 4;
fn leaving() -> (Interner, Func, Vec<Block>) {
walking(Some(TRIPS), Flags::NSW)
}
fn counting() -> (Interner, Func, Vec<Block>) {
walking(None, Flags::NSW)
}
fn uncounted() -> (Interner, Func, Vec<Block>) {
walking(Some(TRIPS), Flags::NONE)
}
fn from_an_index() -> (Interner, Func, Vec<Block>) {
let mut names = Interner::new();
let params = [Type::PTR, Type::int(64)];
let mut func = Func::new(names.intern("f"), Signature::new().with_params(¶ms));
let entry = func.create_block();
let head = func.create_block();
let more = func.create_block();
let done = func.create_block();
let array = func.append_param(entry, Type::PTR);
let start = 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 index = build.binary(Opcode::Add, counter, start, Flags::NSW);
let by = build.iconst(Type::int(64), WIDTH);
let scaled = build.binary(Opcode::Mul, index, 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);
let read = build.load(Type::int(32), pointer, mem(), Flags::NONE);
let nothing = build.iconst(Type::int(32), 0);
let stop = build.icmp(IntPred::Eq, read, nothing);
build.br_if(stop, done, &[], more, &[]);
let mut build = Builder::new(&mut func, more);
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, more, done])
}
fn from_a_narrow_index() -> (Interner, Func, Vec<Block>) {
let mut names = Interner::new();
let params = [Type::PTR, Type::int(32)];
let mut func = Func::new(names.intern("f"), Signature::new().with_params(¶ms));
let entry = func.create_block();
let head = func.create_block();
let more = func.create_block();
let done = func.create_block();
let array = func.append_param(entry, Type::PTR);
let start = func.append_param(entry, Type::int(32));
let counter = func.append_param(head, Type::int(32));
let zero = Builder::new(&mut func, entry).iconst(Type::int(32), 0);
Builder::new(&mut func, entry).jump(head, &[zero]);
let mut build = Builder::new(&mut func, head);
let index = build.binary(Opcode::Add, counter, start, Flags::NSW);
let wide = build.unary(Opcode::SExt, index, 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);
let read = build.load(Type::int(32), pointer, mem(), Flags::NONE);
let nothing = build.iconst(Type::int(32), 0);
let stop = build.icmp(IntPred::Eq, read, nothing);
build.br_if(stop, done, &[], more, &[]);
let mut build = Builder::new(&mut func, more);
let one = build.iconst(Type::int(32), 1);
let next = build.binary(Opcode::Add, counter, one, Flags::NSW);
let limit = build.iconst(Type::int(32), 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, more, done])
}
fn downwards() -> (Interner, Func, Vec<Block>) {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new().with_params(&[Type::PTR]));
let entry = func.create_block();
let head = func.create_block();
let more = 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 last = Builder::new(&mut func, entry).iconst(Type::int(64), TRIPS - 1);
Builder::new(&mut func, entry).jump(head, &[last]);
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);
check(&mut build, pointer);
let read = build.load(Type::int(32), pointer, mem(), Flags::NONE);
let nothing = build.iconst(Type::int(32), 0);
let stop = build.icmp(IntPred::Eq, read, nothing);
build.br_if(stop, done, &[], more, &[]);
let mut build = Builder::new(&mut func, more);
let one = build.iconst(Type::int(64), 1);
let next = build.binary(Opcode::Sub, counter, one, Flags::NSW);
let floor = build.iconst(Type::int(64), 0);
let again = build.icmp(IntPred::Sge, next, floor);
build.br_if(again, head, &[next], done, &[]);
Builder::new(&mut func, done).ret(&[]);
(names, func, vec![entry, head, more, done])
}
fn walking(times: Option<i128>, flags: Flags) -> (Interner, Func, Vec<Block>) {
let mut names = Interner::new();
let mut params = vec![Type::PTR];
params.extend(times.is_none().then_some(Type::int(64)));
let mut func = Func::new(names.intern("f"), Signature::new().with_params(¶ms));
let entry = func.create_block();
let head = func.create_block();
let more = func.create_block();
let done = func.create_block();
let array = func.append_param(entry, Type::PTR);
let handed = times.is_none().then(|| 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 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);
check(&mut build, pointer);
let read = build.load(Type::int(32), pointer, mem(), Flags::NONE);
let nothing = build.iconst(Type::int(32), 0);
let stop = build.icmp(IntPred::Eq, read, nothing);
build.br_if(stop, done, &[], more, &[]);
let mut build = Builder::new(&mut func, more);
let one = build.iconst(Type::int(64), 1);
let next = build.binary(Opcode::Add, counter, one, flags);
let limit = match (times, handed) {
(Some(times), _) => build.iconst(Type::int(64), times),
(None, handed) => handed.expect("a loop with no number for a limit was handed one"),
};
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, more, done])
}
fn mem() -> MemInfo {
MemInfo {
size: WIDTH as u64,
align: WIDTH as u32,
order: MemOrder::NotAtomic,
tbaa: None,
restrict: Restrict::NONE,
}
}
fn check(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]);
let extra = Extra::Mem(build.func().add_mem(mem()));
build.inst(InstData { args, extra, ..InstData::new(Opcode::CheckBounds) }, &[]);
}
fn split_up(func: &mut Func) -> Stats {
let mut an = crate::machine::fixtures::analyses();
Canon.run(func, &mut an, &mut Fuel::unlimited());
Split.run(func, &mut an, &mut Fuel::unlimited())
}
#[test]
fn a_loop_whose_result_is_read_after_it_is_put_back_into_closed_form_first() {
let (mut names, mut func, blocks) = leaving();
let mut an = crate::machine::fixtures::analyses();
Canon.run(&mut func, &mut an, &mut Fuel::unlimited());
let (head, done) = (blocks[1], blocks[3]);
let read = func
.insts(head)
.find(|&inst| func[inst].opcode == Opcode::Load)
.and_then(|inst| func[inst].results().next())
.expect("the loop loads what it walks over");
let term = func.terminator(done).expect("the block after the loop returns");
let sum = Builder::new(&mut func, done).binary(Opcode::Add, read, read, Flags::NONE);
let inst = super::inst_of(&func, sum);
func.remove_inst(inst);
func.insert_before(inst, term);
an.clear();
let stats = Split.run(&mut func, &mut an, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, super::CLOSED_HERE), 1);
assert_eq!(stats.count(Kind::Optimized, SPLIT), 1);
assert_eq!(func[done].params.len(), 1, "the block after the loop took the value in");
assert_eq!(all(&func, Opcode::CheckBounds).len(), 1, "the fast half lost its check");
sound(&func, &mut names);
}
fn all(func: &Func, opcode: Opcode) -> Vec<(Block, Inst)> {
func.blocks()
.flat_map(|block| func.insts(block).map(move |inst| (block, inst)).collect::<Vec<_>>())
.filter(|&(_, inst)| func[inst].opcode == opcode)
.collect()
}
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_loop_that_can_stop_early_is_split_even_though_hoisting_will_not_touch_it() {
let (mut names, mut func, _) = leaving();
let mut an = crate::machine::fixtures::analyses();
Canon.run(&mut func, &mut an, &mut Fuel::unlimited());
let refused = crate::hoist::Hoist.run(&mut func, &mut an, &mut Fuel::unlimited());
assert!(!refused.changed(), "hoisting has nothing to say about this loop");
let stats = Split.run(&mut func, &mut an, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, SPLIT), 1);
sound(&func, &mut names);
}
#[test]
fn the_half_the_loop_runs_first_has_no_check_in_it_and_the_other_one_keeps_it() {
let (mut names, mut func, blocks) = leaving();
let head = blocks[1];
split_up(&mut func);
let left = all(&func, Opcode::CheckBounds);
assert_eq!(left.len(), 1, "one check, and it is the one the slow half kept");
assert_ne!(left[0].0, head, "and it is not in the block the loop started in");
sound(&func, &mut names);
}
#[test]
fn how_far_the_runtime_is_asked_to_look_is_settled_in_front_of_the_loop() {
let (mut names, mut func, _) = leaving();
split_up(&mut func);
let asked = all(&func, Opcode::CapExtent);
assert_eq!(asked.len(), 1, "one question for the one check that was sized");
let cfg = crate::Cfg::new(&func);
let doms = crate::Dominators::new(&cfg);
let loops = crate::Loops::new(&cfg, &doms);
assert!(
loops.all().all(|id| !loops.contains(id, asked[0].0)),
"and it is outside the loop"
);
sound(&func, &mut names);
}
#[test]
fn a_walk_that_starts_at_an_index_the_caller_handed_in_is_split() {
let (mut names, mut func, blocks) = from_an_index();
let stats = split_up(&mut func);
assert_eq!(stats.count(Kind::Optimized, SPLIT), 1);
assert_eq!(all(&func, Opcode::CheckBounds).len(), 1, "the fast half lost its check");
let asked = all(&func, Opcode::CapExtent);
assert_eq!(asked.len(), 1, "one question for the one check that was sized");
let at = func[func[asked[0].1].args][1];
let inst = super::inst_of(&func, at);
assert_eq!(func[inst].opcode, Opcode::PtrAdd, "the question is asked about a displacement");
assert_eq!(func[func[inst].args][0], func[blocks[0]].params[0], "off the array");
sound(&func, &mut names);
}
#[test]
fn a_walk_from_an_index_in_int_is_split_and_the_extension_is_emitted_in_front() {
let (mut names, mut func, blocks) = from_a_narrow_index();
let stats = split_up(&mut func);
assert_eq!(stats.count(Kind::Optimized, SPLIT), 1);
assert_eq!(all(&func, Opcode::CheckBounds).len(), 1, "the fast half lost its check");
let asked = all(&func, Opcode::CapExtent);
assert_eq!(asked.len(), 1, "one question for the one check that was sized");
let at = func[func[asked[0].1].args][1];
let sum = super::inst_of(&func, at);
assert_eq!(func[sum].opcode, Opcode::PtrAdd, "the question is asked about a displacement");
assert_eq!(func[func[sum].args][0], func[blocks[0]].params[0], "off the array");
let widened = all(&func, Opcode::SExt);
assert_eq!(widened.len(), 3, "one extension in each half of the loop and one in front");
let start = func[blocks[0]].params[1];
assert_eq!(
widened.iter().filter(|&&(_, inst)| func[func[inst].args][0] == start).count(),
1,
"and the one in front is of the index the caller handed in, which the halves never take",
);
sound(&func, &mut names);
}
#[test]
fn a_walk_from_high_to_low_is_split_and_the_question_goes_the_other_way() {
let (mut names, mut func, blocks) = downwards();
let stats = split_up(&mut func);
assert_eq!(stats.count(Kind::Optimized, SPLIT), 1);
assert_eq!(all(&func, Opcode::CheckBounds).len(), 1, "the fast half lost its check");
assert!(all(&func, Opcode::CapExtent).is_empty(), "nothing asked about the bytes above");
let asked = all(&func, Opcode::CapExtentBack);
assert_eq!(asked.len(), 1, "one question for the one check that was sized");
let at = func[func[asked[0].1].args][1];
let end = super::inst_of(&func, at);
assert_eq!(func[end].opcode, Opcode::PtrAdd, "asked at the end of the first access");
let from = func[func[end].args][0];
let first = super::inst_of(&func, from);
assert_eq!(
func[first].opcode,
Opcode::PtrAdd,
"past a first access that is a displacement"
);
assert_eq!(func[func[first].args][0], func[blocks[0]].params[0], "off the array");
sound(&func, &mut names);
}
#[test]
fn a_loop_with_a_call_in_it_that_might_free_is_left_alone() {
let (_, mut func, _) = calling(Flags::NONE);
let stats = split_up(&mut func);
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, super::A_CALL_INSIDE), 1);
}
#[test]
fn a_loop_with_a_call_in_it_that_cannot_free_is_split() {
let (mut names, mut func, _) = calling(Flags::NOFREE);
let stats = split_up(&mut func);
assert_eq!(stats.count(Kind::Optimized, SPLIT), 1);
assert_eq!(all(&func, Opcode::CheckBounds).len(), 1, "the fast half lost its check");
assert_eq!(all(&func, Opcode::Call).len(), 2, "and both halves kept the call");
sound(&func, &mut names);
}
fn calling(flags: Flags) -> (Interner, Func, Vec<Block>) {
let (mut names, mut func, blocks) = leaving();
let more = blocks[2];
let term = func.terminator(more).expect("the latch branches");
let callee = names.intern("somewhere");
let signature = func.add_signature(Signature::new());
let call = Builder::new(&mut func, more).call(callee, signature, &[]);
func[call].flags |= flags;
func.remove_inst(call);
func.insert_before(call, term);
(names, func, blocks)
}
#[test]
fn a_check_whose_address_does_not_move_is_taken_too() {
let (mut names, mut func, _) = standing(false);
let stats = split_up(&mut func);
assert_eq!(stats.count(Kind::Optimized, SPLIT), 1);
assert_eq!(all(&func, Opcode::CapExtent).len(), 2, "both addresses were sized in front");
assert_eq!(
all(&func, Opcode::CheckBounds).len(),
2,
"the fast half lost both checks and the slow half kept both"
);
sound(&func, &mut names);
}
#[test]
fn the_window_is_worked_out_without_dividing_by_anything() {
let (mut names, mut func, _) = standing(false);
split_up(&mut func);
for opcode in [Opcode::SDiv, Opcode::UDiv] {
assert!(all(&func, opcode).is_empty(), "{opcode:?} is left in the window arithmetic");
}
sound(&func, &mut names);
}
#[test]
fn two_checks_that_walk_by_the_same_amount_share_one_offset() {
let (mut names, mut func, blocks) = twinned();
let stats = split_up(&mut func);
assert_eq!(stats.count(Kind::Optimized, SPLIT), 1);
assert_eq!(all(&func, Opcode::CapExtent).len(), 2, "both addresses were sized in front");
let head = blocks[1];
let cfg = crate::Cfg::new(&func);
let into = cfg.predecessors(head);
assert_eq!(into.len(), 1, "the guard is the only way into the header now");
let guard = into[0];
assert_eq!(
func[guard].params.len(),
func[head].params.len() + 1,
"one offset, not one per check"
);
sound(&func, &mut names);
}
fn twinned() -> (Interner, Func, Vec<Block>) {
let (names, mut func, blocks) = leaving();
let (entry, head) = (blocks[0], blocks[1]);
let array = func[entry].params[0];
let counter = func[head].params[0];
let term = func.terminator(head).expect("the header branches");
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 ahead = build.binary(Opcode::Add, scaled, by, Flags::NSW);
let args = build.func().push_values(&[array, ahead]);
let pointer = build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR);
check(&mut build, pointer);
let made: Vec<Inst> = func.insts(head).skip_while(|&inst| inst != term).skip(1).collect();
for inst in made {
func.remove_inst(inst);
func.insert_before(inst, term);
}
(names, func, blocks)
}
#[test]
fn a_loop_where_nothing_moves_picks_its_half_once_and_counts_nothing() {
let (mut names, mut func, blocks) = standing(true);
let stats = split_up(&mut func);
assert_eq!(stats.count(Kind::Optimized, SPLIT), 1);
assert_eq!(all(&func, Opcode::CapExtent).len(), 1);
assert_eq!(all(&func, Opcode::CheckBounds).len(), 1, "the fast half lost its check");
let (entry, head) = (blocks[0], blocks[1]);
let term = func.terminator(entry).expect("the preheader still ends in something");
assert_eq!(func[term].opcode, Opcode::BrIf, "the way in is the choice");
assert_eq!(func[head].params.len(), 1, "and the header took on no counter");
sound(&func, &mut names);
}
fn standing(alone: bool) -> (Interner, Func, Vec<Block>) {
let (names, mut func, blocks) = leaving();
let (entry, head) = (blocks[0], blocks[1]);
let array = func[entry].params[0];
let walking = all(&func, Opcode::CheckBounds);
let term = func.terminator(head).expect("the header branches");
let mut build = Builder::new(&mut func, head);
check(&mut build, array);
let made: Vec<Inst> = func.insts(head).skip_while(|&inst| inst != term).skip(1).collect();
for inst in made {
func.remove_inst(inst);
func.insert_before(inst, term);
}
if alone {
for (_, inst) in walking {
func.remove_inst(inst);
}
}
(names, func, blocks)
}
#[test]
fn a_loop_whose_count_is_an_expression_is_split_on_what_that_expression_says() {
let (mut names, mut func, _) = counting();
let stats = split_up(&mut func);
assert_eq!(stats.count(Kind::Optimized, SPLIT), 1);
assert_eq!(all(&func, Opcode::CapExtent).len(), 1);
assert_eq!(all(&func, Opcode::CheckBounds).len(), 1, "the fast half lost its check");
sound(&func, &mut names);
}
#[test]
fn a_loop_nobody_counted_is_split_on_a_guess() {
let (mut names, mut func, _) = uncounted();
let mut an = crate::machine::fixtures::analyses();
Canon.run(&mut func, &mut an, &mut Fuel::unlimited());
let refused = crate::hoist::Hoist.run(&mut func, &mut an, &mut Fuel::unlimited());
assert!(!refused.changed(), "hoisting will not size a check from a count nobody settled");
let stats = Split.run(&mut func, &mut an, &mut Fuel::unlimited());
assert_eq!(stats.count(Kind::Optimized, SPLIT), 1);
assert_eq!(all(&func, Opcode::CheckBounds).len(), 1, "the fast half lost its check");
sound(&func, &mut names);
}
#[test]
fn the_pass_stops_when_the_fuel_runs_out() {
let (_, mut func, _) = leaving();
let mut an = crate::machine::fixtures::analyses();
Canon.run(&mut func, &mut an, &mut Fuel::unlimited());
let stats = Split.run(&mut func, &mut an, &mut Fuel::of(0));
assert!(!stats.changed());
assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
}
}