use std::collections::HashMap;
use rucc_ir::{Block, Def, Extra, Flags, Func, Imm, Inst, IntPred, Opcode, Type, Value};
use crate::cfg::Cfg;
use crate::loops::{LoopId, Loops};
const STEP_LIMIT: u32 = 16;
const ASSUMED_ITERATIONS: u64 = 10;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Invariant {
pub value: Option<Value>,
pub scale: i128,
pub offset: i128,
}
impl Invariant {
#[must_use]
pub fn number(offset: i128) -> Self {
Self { value: None, scale: 0, offset }
}
#[must_use]
pub fn of(value: Value) -> Self {
Self { value: Some(value), scale: 1, offset: 0 }
}
#[must_use]
pub fn as_number(self) -> Option<i128> {
(self.value.is_none() || self.scale == 0).then_some(self.offset)
}
#[must_use]
pub fn is_zero(self) -> bool {
self.as_number() == Some(0)
}
fn shared(self, other: Self) -> Option<Option<Value>> {
match (self.as_number().is_some(), other.as_number().is_some()) {
(true, _) => Some(other.value),
(_, true) => Some(self.value),
_ => (self.value == other.value).then_some(self.value),
}
}
#[must_use]
pub fn plus(self, other: Self) -> Option<Self> {
let value = self.shared(other)?;
Some(Self {
value,
scale: self.scale.checked_add(other.scale)?,
offset: self.offset.checked_add(other.offset)?,
})
}
#[must_use]
pub fn minus(self, other: Self) -> Option<Self> {
self.plus(other.negated()?)
}
#[must_use]
pub fn negated(self) -> Option<Self> {
Some(Self {
value: self.value,
scale: self.scale.checked_neg()?,
offset: self.offset.checked_neg()?,
})
}
#[must_use]
pub fn times(self, other: Self) -> Option<Self> {
let (symbol, by) = match (self.as_number(), other.as_number()) {
(Some(by), _) => (other, by),
(_, Some(by)) => (self, by),
_ => return None,
};
Some(Self {
value: symbol.value,
scale: symbol.scale.checked_mul(by)?,
offset: symbol.offset.checked_mul(by)?,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Evolution {
Invariant(Invariant),
Affine(Chrec),
Unknown,
}
impl Evolution {
#[must_use]
pub fn chrec(self) -> Option<Chrec> {
match self {
Self::Affine(chrec) => Some(chrec),
_ => None,
}
}
#[must_use]
pub fn invariant(self) -> Option<Invariant> {
match self {
Self::Invariant(inv) => Some(inv),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Chrec {
pub base: Invariant,
pub step: Invariant,
pub ty: Type,
pub flags: Flags,
}
impl Chrec {
#[must_use]
pub fn does_not_wrap(self, signed: bool) -> bool {
self.flags.contains(if signed { Flags::NSW } else { Flags::NUW })
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Assumption {
Approaching,
NoWrap(Chrec),
StrictOverflow,
}
impl Assumption {
#[must_use]
pub fn describe(&self) -> String {
match self {
Self::Approaching => "the counter starts on the near side of its limit".to_string(),
Self::NoWrap(chrec) => {
format!("the induction variable does not wrap in i{}", chrec.ty.bits())
}
Self::StrictOverflow => {
"signed overflow is undefined, so -fwrapv withdraws this count".to_string()
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Count {
Exact(u128),
Symbolic(Invariant),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Bound {
count: Count,
assumptions: Vec<Assumption>,
}
impl Bound {
#[must_use]
pub fn parts(&self) -> (Count, &[Assumption]) {
(self.count, &self.assumptions)
}
#[must_use]
pub fn assumptions(&self) -> &[Assumption] {
&self.assumptions
}
#[must_use]
pub fn proven(&self) -> Option<Count> {
self.assumptions.is_empty().then_some(self.count)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Estimate {
iterations: u64,
guessed: bool,
}
impl Estimate {
#[must_use]
pub fn iterations(self) -> u64 {
self.iterations
}
#[must_use]
pub fn is_guess(self) -> bool {
self.guessed
}
}
#[derive(Debug)]
pub struct Scev<'a> {
func: &'a Func,
cfg: &'a Cfg,
loops: &'a Loops,
known: HashMap<(LoopId, Value), Evolution>,
}
impl<'a> Scev<'a> {
#[must_use]
pub fn new(func: &'a Func, cfg: &'a Cfg, loops: &'a Loops) -> Self {
Self { func, cfg, loops, known: HashMap::new() }
}
pub fn evolution(&mut self, id: LoopId, value: Value) -> Evolution {
if let Some(&known) = self.known.get(&(id, value)) {
return known;
}
self.known.insert((id, value), Evolution::Unknown);
let found = self.compute(id, value);
self.known.insert((id, value), found);
found
}
pub fn bound(&mut self, id: LoopId) -> Option<Bound> {
let exits: Vec<Block> = self.loops.exits(id).iter().map(|exit| exit.from).collect();
exits.into_iter().find_map(|from| self.bound_at(id, from))
}
pub fn estimate(&mut self, id: LoopId) -> Estimate {
match self.bound(id).map(|bound| bound.count) {
Some(Count::Exact(exact)) => {
Estimate { iterations: u64::try_from(exact).unwrap_or(u64::MAX), guessed: false }
}
_ => Estimate { iterations: ASSUMED_ITERATIONS, guessed: true },
}
}
fn compute(&mut self, id: LoopId, value: Value) -> Evolution {
if let Some(invariant) = self.invariant(id, value) {
return Evolution::Invariant(invariant);
}
match self.func[value].def {
Def::Param { block, index } if block == self.loops.header(id) => {
self.at_header(id, value, index as usize)
}
Def::Param { .. } => Evolution::Unknown,
Def::Result { inst, .. } => self.at_inst(id, inst, value),
}
}
fn invariant(&self, id: LoopId, value: Value) -> Option<Invariant> {
if let Some((imm, ty)) = constant(self.func, value) {
return Some(Invariant::number(imm.signed(ty)));
}
self.loops.is_invariant(self.func, id, value).then(|| Invariant::of(value))
}
fn at_header(&mut self, id: LoopId, value: Value, index: usize) -> Evolution {
let (func, cfg, loops) = (self.func, self.cfg, self.loops);
let header = loops.header(id);
let [latch] = loops.latches(id) else { return Evolution::Unknown };
let mut entering = None;
let mut around = None;
for &pred in cfg.predecessors(header) {
let Some(arg) = argument(func, pred, header, index) else { return Evolution::Unknown };
let slot = if pred == *latch { &mut around } else { &mut entering };
if slot.replace(arg).is_some_and(|old| old != arg) {
return Evolution::Unknown;
}
}
let (Some(entering), Some(around)) = (entering, around) else { return Evolution::Unknown };
let Some(base) = self.invariant(id, entering) else { return Evolution::Unknown };
let Some((step, flags)) = self.step(id, around, value, 0) else {
return Evolution::Unknown;
};
affine(base, step, func[value].ty, flags)
}
fn step(&self, id: LoopId, value: Value, of: Value, depth: u32) -> Option<(Invariant, Flags)> {
if value == of {
return Some((Invariant::number(0), Flags::NSW.union(Flags::NUW)));
}
if depth >= STEP_LIMIT {
return None;
}
let Def::Result { inst, .. } = self.func[value].def else { return None };
let data = &self.func[inst];
let args = &self.func[data.args];
let (&lhs, &rhs) = (args.first()?, args.get(1)?);
let combine = |carried: (Invariant, Flags), other: Invariant, subtract: bool| {
let (delta, flags) = carried;
let moved = if subtract { delta.minus(other)? } else { delta.plus(other)? };
Some((moved, flags.intersection(data.flags)))
};
match data.opcode {
Opcode::Add => {
if let Some(carried) = self.step(id, lhs, of, depth + 1) {
return combine(carried, self.invariant(id, rhs)?, false);
}
combine(self.step(id, rhs, of, depth + 1)?, self.invariant(id, lhs)?, false)
}
Opcode::Sub => {
combine(self.step(id, lhs, of, depth + 1)?, self.invariant(id, rhs)?, true)
}
Opcode::PtrAdd => {
combine(self.step(id, lhs, of, depth + 1)?, self.invariant(id, rhs)?, false)
}
_ => None,
}
}
fn at_inst(&mut self, id: LoopId, inst: Inst, value: Value) -> Evolution {
let func = self.func;
let data = &func[inst];
let (opcode, flags) = (data.opcode, data.flags);
let args = &func[data.args];
let ty = func[value].ty;
let Some(&lhs) = args.first() else { return Evolution::Unknown };
match opcode {
Opcode::Add | Opcode::PtrAdd => {
let Some(&rhs) = args.get(1) else { return Evolution::Unknown };
let (left, right) = (self.evolution(id, lhs), self.evolution(id, rhs));
combine(left, right, ty, flags, false)
}
Opcode::Sub => {
let Some(&rhs) = args.get(1) else { return Evolution::Unknown };
let (left, right) = (self.evolution(id, lhs), self.evolution(id, rhs));
combine(left, right, ty, flags, true)
}
Opcode::Mul => {
let Some(&rhs) = args.get(1) else { return Evolution::Unknown };
let (left, right) = (self.evolution(id, lhs), self.evolution(id, rhs));
scale(left, right, ty, flags)
}
Opcode::Shl => {
let Some(&rhs) = args.get(1) else { return Evolution::Unknown };
let Some((count, count_ty)) = constant(func, rhs) else {
return Evolution::Unknown;
};
let count = count.unsigned();
if count >= u128::from(ty.bits()) || !count_ty.is_int() {
return Evolution::Unknown;
}
let by = Evolution::Invariant(Invariant::number(1i128 << count));
scale(self.evolution(id, lhs), by, ty, flags)
}
Opcode::SExt | Opcode::ZExt => self.extend(id, opcode, lhs, ty),
_ => Evolution::Unknown,
}
}
fn extend(&mut self, id: LoopId, opcode: Opcode, from: Value, to: Type) -> Evolution {
let narrow = self.func[from].ty;
let signed = opcode == Opcode::SExt;
match self.evolution(id, from) {
Evolution::Invariant(inv) => match inv.as_number() {
Some(number) if signed || number >= 0 => Evolution::Invariant(inv),
_ => Evolution::Unknown,
},
Evolution::Affine(chrec) if chrec.ty == narrow && chrec.does_not_wrap(signed) => {
let (Some(base), Some(step)) = (chrec.base.as_number(), chrec.step.as_number())
else {
return Evolution::Unknown;
};
Evolution::Affine(Chrec {
base: Invariant::number(base),
step: Invariant::number(step),
ty: to,
flags: chrec.flags,
})
}
_ => Evolution::Unknown,
}
}
fn bound_at(&mut self, id: LoopId, from: Block) -> Option<Bound> {
let func = self.func;
let term = func.terminator(from)?;
if func[term].opcode != Opcode::BrIf {
return None;
}
let args = &func[func[term].args];
let &cond = args.first()?;
let calls = &func[func.target_list(term)];
let (&taken, ¬_taken) = (calls.first()?, calls.get(1)?);
let stays = match (
self.loops.contains(id, taken.block),
self.loops.contains(id, not_taken.block),
) {
(true, false) => true,
(false, true) => false,
_ => return None,
};
let Def::Result { inst, .. } = func[cond].def else { return None };
if func[inst].opcode != Opcode::ICmp {
return None;
}
let Extra::IntPred(pred) = func[inst].extra else { return None };
let pred = if stays { pred } else { invert(pred) };
let operands = &func[func[inst].args];
let (&lhs, &rhs) = (operands.first()?, operands.get(1)?);
let (chrec, limit, pred) = match (self.evolution(id, lhs), self.evolution(id, rhs)) {
(Evolution::Affine(chrec), other) => (chrec, other.invariant()?, pred),
(other, Evolution::Affine(chrec)) => (chrec, other.invariant()?, swap(pred)),
_ => return None,
};
solve(chrec, limit, pred)
}
}
fn combine(left: Evolution, right: Evolution, ty: Type, flags: Flags, subtract: bool) -> Evolution {
let apply = |a: Invariant, b: Invariant| if subtract { a.minus(b) } else { a.plus(b) };
match (left, right) {
(Evolution::Invariant(a), Evolution::Invariant(b)) => {
apply(a, b).map_or(Evolution::Unknown, Evolution::Invariant)
}
(Evolution::Affine(chrec), Evolution::Invariant(b)) => {
let Some(base) = apply(chrec.base, b) else { return Evolution::Unknown };
affine(base, chrec.step, ty, flags.intersection(chrec.flags))
}
(Evolution::Invariant(a), Evolution::Affine(chrec)) => {
let (Some(base), Some(step)) = (
apply(a, chrec.base),
if subtract { chrec.step.negated() } else { Some(chrec.step) },
) else {
return Evolution::Unknown;
};
affine(base, step, ty, flags.intersection(chrec.flags))
}
(Evolution::Affine(a), Evolution::Affine(b)) => {
if a.ty != b.ty {
return Evolution::Unknown;
}
let (Some(base), Some(step)) = (apply(a.base, b.base), apply(a.step, b.step)) else {
return Evolution::Unknown;
};
affine(base, step, ty, flags.intersection(a.flags).intersection(b.flags))
}
_ => Evolution::Unknown,
}
}
fn scale(left: Evolution, right: Evolution, ty: Type, flags: Flags) -> Evolution {
let (chrec, by) = match (left, right) {
(Evolution::Invariant(a), Evolution::Invariant(b)) => {
return a.times(b).map_or(Evolution::Unknown, Evolution::Invariant);
}
(Evolution::Affine(chrec), Evolution::Invariant(by))
| (Evolution::Invariant(by), Evolution::Affine(chrec)) => (chrec, by),
_ => return Evolution::Unknown,
};
let (Some(base), Some(step)) = (chrec.base.times(by), chrec.step.times(by)) else {
return Evolution::Unknown;
};
affine(base, step, ty, flags.intersection(chrec.flags))
}
fn affine(base: Invariant, step: Invariant, ty: Type, flags: Flags) -> Evolution {
if step.is_zero() {
return Evolution::Invariant(base);
}
Evolution::Affine(Chrec { base, step, ty, flags })
}
fn solve(chrec: Chrec, limit: Invariant, pred: IntPred) -> Option<Bound> {
let step = chrec.step.as_number()?;
if step == 0 {
return None;
}
let signed = matches!(pred, IntPred::Slt | IntPred::Sle | IntPred::Sgt | IntPred::Sge);
let mut assumptions = Vec::new();
if !chrec.does_not_wrap(signed) {
assumptions.push(Assumption::NoWrap(chrec));
}
if signed {
assumptions.push(Assumption::StrictOverflow);
}
let (base, limit) = if signed {
(chrec.base, limit)
} else {
(as_unsigned(chrec.base, chrec.ty)?, as_unsigned(limit, chrec.ty)?)
};
let apart = step.unsigned_abs();
match (pred, step > 0) {
(IntPred::Slt | IntPred::Ult, true) => {
ordered(limit.minus(base)?, apart, false, assumptions)
}
(IntPred::Sle | IntPred::Ule, true) => {
ordered(limit.minus(base)?, apart, true, assumptions)
}
(IntPred::Sgt | IntPred::Ugt, false) => {
ordered(base.minus(limit)?, apart, false, assumptions)
}
(IntPred::Sge | IntPred::Uge, false) => {
ordered(base.minus(limit)?, apart, true, assumptions)
}
(IntPred::Ne, _) => {
let distance = if step > 0 { limit.minus(base)? } else { base.minus(limit)? };
landing(distance, apart, assumptions)
}
_ => None,
}
}
fn as_unsigned(inv: Invariant, ty: Type) -> Option<Invariant> {
match inv.as_number() {
Some(number) if number >= 0 => Some(inv),
Some(number) => {
let bits = ty.is_int().then(|| ty.bits()).filter(|&bits| bits < 127)?;
Some(Invariant::number(number & ((1i128 << bits) - 1)))
}
None => (inv.scale == 1 && inv.offset == 0).then_some(inv),
}
}
fn ordered(
distance: Invariant,
step: u128,
inclusive: bool,
mut assumptions: Vec<Assumption>,
) -> Option<Bound> {
match distance.as_number() {
Some(exact) => {
if exact < 0 {
return Some(Bound { count: Count::Exact(0), assumptions: Vec::new() });
}
let count = (exact.unsigned_abs() + u128::from(inclusive)).div_ceil(step);
Some(Bound { count: Count::Exact(count), assumptions })
}
None if step == 1 => {
assumptions.push(Assumption::Approaching);
let count = distance.plus(Invariant::number(i128::from(inclusive)))?;
Some(Bound { count: Count::Symbolic(count), assumptions })
}
None => None,
}
}
fn landing(distance: Invariant, step: u128, mut assumptions: Vec<Assumption>) -> Option<Bound> {
match distance.as_number() {
Some(exact) => {
let travel = u128::try_from(exact).ok()?;
(travel % step == 0).then(|| Bound { count: Count::Exact(travel / step), assumptions })
}
None if step == 1 => {
assumptions.push(Assumption::Approaching);
Some(Bound { count: Count::Symbolic(distance), assumptions })
}
None => None,
}
}
fn invert(pred: IntPred) -> IntPred {
match pred {
IntPred::Eq => IntPred::Ne,
IntPred::Ne => IntPred::Eq,
IntPred::Slt => IntPred::Sge,
IntPred::Sle => IntPred::Sgt,
IntPred::Sgt => IntPred::Sle,
IntPred::Sge => IntPred::Slt,
IntPred::Ult => IntPred::Uge,
IntPred::Ule => IntPred::Ugt,
IntPred::Ugt => IntPred::Ule,
IntPred::Uge => IntPred::Ult,
}
}
fn swap(pred: IntPred) -> IntPred {
match pred {
IntPred::Eq => IntPred::Eq,
IntPred::Ne => IntPred::Ne,
IntPred::Slt => IntPred::Sgt,
IntPred::Sle => IntPred::Sge,
IntPred::Sgt => IntPred::Slt,
IntPred::Sge => IntPred::Sle,
IntPred::Ult => IntPred::Ugt,
IntPred::Ule => IntPred::Uge,
IntPred::Ugt => IntPred::Ult,
IntPred::Uge => IntPred::Ule,
}
}
fn constant(func: &Func, value: Value) -> Option<(Imm, Type)> {
let Def::Result { inst, .. } = func[value].def else { return None };
if func[inst].opcode != Opcode::IConst {
return None;
}
let Extra::Imm(at) = func[inst].extra else { return None };
let ty = func[value].ty;
ty.is_int().then(|| (func[at], ty))
}
fn argument(func: &Func, pred: Block, block: Block, index: usize) -> Option<Value> {
let term = func.terminator(pred)?;
let mut found = None;
for call in func.successors(term) {
if call.block != block {
continue;
}
let arg = *func[call.args].get(index)?;
if found.replace(arg).is_some_and(|old| old != arg) {
return None;
}
}
found
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{Builder, Flags, Func, IntPred, Opcode, Signature, Type, Value};
use crate::cfg::Cfg;
use crate::dom::Dominators;
use crate::loops::{LoopId, Loops};
use crate::scev::{Assumption, Bound, Count, Evolution, Invariant, Scev};
struct Counted {
func: Func,
counter: Value,
next: Value,
}
fn counted(ty: Type, from: i128, to: i128, step: i128, pred: IntPred, flags: Flags) -> Counted {
let (it, ()) = counted_with(ty, from, to, step, pred, flags, |_, _| ());
it
}
fn counted_with<T>(
ty: Type,
from: i128,
to: i128,
step: i128,
pred: IntPred,
flags: Flags,
extra: impl FnOnce(&mut Builder<'_>, Value) -> T,
) -> (Counted, T) {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let header = func.create_block();
let body = func.create_block();
let exit = func.create_block();
let counter = func.append_param(header, ty);
let mut build = Builder::new(&mut func, entry);
let start = build.iconst(ty, from);
build.jump(header, &[start]);
let mut build = Builder::new(&mut func, header);
let limit = build.iconst(ty, to);
let test = build.icmp(pred, counter, limit);
build.br_if(test, body, &[], exit, &[]);
let mut build = Builder::new(&mut func, body);
let derived = extra(&mut build, counter);
let by = build.iconst(ty, step);
let next = build.binary(Opcode::Add, counter, by, flags);
build.jump(header, &[next]);
let mut build = Builder::new(&mut func, exit);
build.ret(&[]);
(Counted { func, counter, next }, derived)
}
fn analyse(func: &Func) -> (Cfg, Loops) {
let cfg = Cfg::new(func);
let doms = Dominators::new(&cfg);
let loops = Loops::new(&cfg, &doms);
(cfg, loops)
}
fn evolution(func: &Func, value: Value) -> Evolution {
let (cfg, loops) = analyse(func);
let id = loops.roots()[0];
Scev::new(func, &cfg, &loops).evolution(id, value)
}
fn bound(func: &Func) -> Option<Bound> {
let (cfg, loops) = analyse(func);
let id: LoopId = loops.roots()[0];
Scev::new(func, &cfg, &loops).bound(id)
}
#[test]
fn a_counter_from_zero_by_one_is_the_chrec_everyone_expects() {
let it = counted(Type::int(32), 0, 100, 1, IntPred::Slt, Flags::NSW);
let chrec = evolution(&it.func, it.counter).chrec().expect("the counter evolves");
assert_eq!(chrec.base, Invariant::number(0));
assert_eq!(chrec.step, Invariant::number(1));
assert_eq!(chrec.ty, Type::int(32));
assert!(chrec.does_not_wrap(true));
}
#[test]
fn the_value_fed_back_is_the_chrec_one_step_along() {
let it = counted(Type::int(32), 5, 100, 3, IntPred::Slt, Flags::NSW);
let chrec = evolution(&it.func, it.next).chrec().expect("the increment evolves");
assert_eq!(chrec.base, Invariant::number(8));
assert_eq!(chrec.step, Invariant::number(3));
}
#[test]
fn a_multiple_of_the_counter_plus_a_number_is_a_chrec_of_its_own() {
let (it, shifted) =
counted_with(Type::int(32), 0, 100, 1, IntPred::Slt, Flags::NSW, |build, counter| {
let two = build.iconst(Type::int(32), 2);
let three = build.iconst(Type::int(32), 3);
let doubled = build.binary(Opcode::Mul, counter, two, Flags::NSW);
build.binary(Opcode::Add, doubled, three, Flags::NSW)
});
let chrec = evolution(&it.func, shifted).chrec().expect("it evolves");
assert_eq!(chrec.base, Invariant::number(3));
assert_eq!(chrec.step, Invariant::number(2));
}
#[test]
fn a_shift_by_a_constant_scales_the_chrec_and_a_shift_past_the_width_does_not() {
let (it, (scaled, poison)) =
counted_with(Type::int(32), 1, 100, 1, IntPred::Slt, Flags::NSW, |build, counter| {
let three = build.iconst(Type::int(32), 3);
let wide = build.iconst(Type::int(32), 32);
(
build.binary(Opcode::Shl, counter, three, Flags::NSW),
build.binary(Opcode::Shl, counter, wide, Flags::NSW),
)
});
let chrec = evolution(&it.func, scaled).chrec().expect("it evolves");
assert_eq!(chrec.base, Invariant::number(8));
assert_eq!(chrec.step, Invariant::number(8));
assert_eq!(evolution(&it.func, poison), Evolution::Unknown);
}
#[test]
fn a_pointer_walked_by_the_element_size_is_a_chrec_in_bytes() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let header = func.create_block();
let body = func.create_block();
let exit = func.create_block();
let start = func.append_param(entry, Type::PTR);
let cursor = func.append_param(header, Type::PTR);
let mut build = Builder::new(&mut func, entry);
build.jump(header, &[start]);
let mut build = Builder::new(&mut func, header);
let done = build.icmp(IntPred::Eq, cursor, start);
build.br_if(done, exit, &[], body, &[]);
let mut build = Builder::new(&mut func, body);
let four = build.iconst(Type::int(64), 4);
let next = build.binary(Opcode::PtrAdd, cursor, four, Flags::NONE);
build.jump(header, &[next]);
let mut build = Builder::new(&mut func, exit);
build.ret(&[]);
let chrec = evolution(&func, cursor).chrec().expect("the cursor evolves");
assert_eq!(chrec.base, Invariant::of(start));
assert_eq!(chrec.step, Invariant::number(4));
assert_eq!(chrec.ty, Type::PTR);
}
#[test]
fn a_counter_in_unsigned_char_wraps_and_does_not_widen_without_a_promise() {
let (it, wide) =
counted_with(Type::int(8), 0, 100, 1, IntPred::Ult, Flags::NONE, |build, counter| {
build.unary(Opcode::ZExt, counter, Type::int(32))
});
let chrec = evolution(&it.func, it.counter).chrec().expect("the counter evolves");
assert_eq!(chrec.ty, Type::int(8));
assert!(!chrec.does_not_wrap(false));
assert_eq!(evolution(&it.func, wide), Evolution::Unknown);
}
#[test]
fn a_counter_in_short_widens_when_the_increment_promised_it_would_not_wrap() {
let (it, (wide, zero_extended)) =
counted_with(Type::int(16), 0, 100, 1, IntPred::Slt, Flags::NSW, |build, counter| {
(
build.unary(Opcode::SExt, counter, Type::int(32)),
build.unary(Opcode::ZExt, counter, Type::int(32)),
)
});
let chrec = evolution(&it.func, wide).chrec().expect("it widens");
assert_eq!(chrec.ty, Type::int(32));
assert_eq!(chrec.base, Invariant::number(0));
assert_eq!(chrec.step, Invariant::number(1));
assert_eq!(evolution(&it.func, zero_extended), Evolution::Unknown);
}
#[test]
fn a_step_of_zero_is_invariant_and_has_no_trip_count() {
let it = counted(Type::int(32), 0, 100, 0, IntPred::Slt, Flags::NSW);
assert!(matches!(evolution(&it.func, it.counter), Evolution::Invariant(_)));
assert_eq!(bound(&it.func), None);
}
#[test]
fn a_counted_loop_has_the_count_anyone_would_work_out_by_hand() {
let it = counted(Type::int(32), 0, 100, 1, IntPred::Slt, Flags::NSW);
let found = bound(&it.func).expect("it is counted");
let (count, assumptions) = found.parts();
assert_eq!(count, Count::Exact(100));
assert_eq!(assumptions, [Assumption::StrictOverflow]);
assert_eq!(found.proven(), None);
}
#[test]
fn a_step_that_overshoots_still_takes_the_iteration_that_overshot() {
let it = counted(Type::int(32), 0, 10, 3, IntPred::Slt, Flags::NSW);
let (count, _) = bound(&it.func).expect("it is counted").parts();
assert_eq!(count, Count::Exact(4));
}
#[test]
fn an_inclusive_test_runs_one_more_time() {
let it = counted(Type::int(32), 0, 10, 1, IntPred::Sle, Flags::NSW);
let (count, _) = bound(&it.func).expect("it is counted").parts();
assert_eq!(count, Count::Exact(11));
}
#[test]
fn a_loop_whose_test_fails_first_time_runs_no_times_and_rests_on_nothing() {
let it = counted(Type::int(32), 10, 0, 1, IntPred::Slt, Flags::NSW);
let found = bound(&it.func).expect("it is counted");
assert_eq!(found.proven(), Some(Count::Exact(0)));
assert!(found.assumptions().is_empty());
}
#[test]
fn counting_down_is_the_same_problem_with_the_ends_swapped() {
let it = counted(Type::int(32), 10, 0, -1, IntPred::Sgt, Flags::NSW);
let (count, _) = bound(&it.func).expect("it is counted").parts();
assert_eq!(count, Count::Exact(10));
}
#[test]
fn an_unsigned_test_does_not_drag_in_the_signed_overflow_assumption() {
let it = counted(Type::int(32), 0, 100, 1, IntPred::Ult, Flags::NUW);
let found = bound(&it.func).expect("it is counted");
assert_eq!(found.proven(), Some(Count::Exact(100)));
}
#[test]
fn a_test_against_something_the_loop_does_not_change_gives_a_symbolic_count() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let header = func.create_block();
let body = func.create_block();
let exit = func.create_block();
let limit = func.append_param(entry, Type::int(32));
let counter = func.append_param(header, Type::int(32));
let mut build = Builder::new(&mut func, entry);
let zero = build.iconst(Type::int(32), 0);
build.jump(header, &[zero]);
let mut build = Builder::new(&mut func, header);
let test = build.icmp(IntPred::Slt, counter, limit);
build.br_if(test, body, &[], exit, &[]);
let mut build = Builder::new(&mut func, body);
let one = build.iconst(Type::int(32), 1);
let next = build.binary(Opcode::Add, counter, one, Flags::NSW);
build.jump(header, &[next]);
let mut build = Builder::new(&mut func, exit);
build.ret(&[]);
let found = bound(&func).expect("it is counted");
let (count, assumptions) = found.parts();
assert_eq!(count, Count::Symbolic(Invariant::of(limit)));
assert!(assumptions.contains(&Assumption::Approaching), "{assumptions:?}");
assert!(assumptions.contains(&Assumption::StrictOverflow), "{assumptions:?}");
assert_eq!(found.proven(), None);
}
#[test]
fn a_counter_without_a_no_wrap_promise_carries_the_assumption_instead() {
let it = counted(Type::int(32), 0, 100, 1, IntPred::Ult, Flags::NONE);
let found = bound(&it.func).expect("it is counted");
let (_, assumptions) = found.parts();
assert!(assumptions.iter().any(|a| matches!(a, Assumption::NoWrap(_))), "{assumptions:?}");
}
#[test]
fn a_test_that_ends_the_loop_when_it_succeeds_is_read_the_other_way_round() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let entry = func.create_block();
let header = func.create_block();
let body = func.create_block();
let exit = func.create_block();
let counter = func.append_param(header, Type::int(32));
let mut build = Builder::new(&mut func, entry);
let zero = build.iconst(Type::int(32), 0);
build.jump(header, &[zero]);
let mut build = Builder::new(&mut func, header);
let limit = build.iconst(Type::int(32), 100);
let done = build.icmp(IntPred::Sge, counter, limit);
build.br_if(done, exit, &[], body, &[]);
let mut build = Builder::new(&mut func, body);
let one = build.iconst(Type::int(32), 1);
let next = build.binary(Opcode::Add, counter, one, Flags::NSW);
build.jump(header, &[next]);
let mut build = Builder::new(&mut func, exit);
build.ret(&[]);
let (count, _) = bound(&func).expect("it is counted").parts();
assert_eq!(count, Count::Exact(100));
}
#[test]
fn an_unsigned_limit_past_the_middle_of_its_type_is_not_a_negative_one() {
let it = counted(Type::int(8), 0, 200, 1, IntPred::Ult, Flags::NUW);
let found = bound(&it.func).expect("it is counted");
assert_eq!(found.proven(), Some(Count::Exact(200)));
}
#[test]
fn a_walk_that_lands_on_a_not_equal_limit_exactly_is_counted() {
let it = counted(Type::int(32), 0, 10, 1, IntPred::Ne, Flags::NSW.union(Flags::NUW));
let found = bound(&it.func).expect("it lands on its limit");
assert_eq!(found.proven(), Some(Count::Exact(10)));
}
#[test]
fn a_counter_stepping_away_from_a_not_equal_limit_is_not_a_loop_that_runs_no_times() {
let it = counted(Type::int(32), 48, 15, 1, IntPred::Ne, Flags::NSW);
assert_eq!(bound(&it.func), None);
}
#[test]
fn a_counter_stepping_over_a_not_equal_limit_never_arrives_either() {
let it = counted(Type::int(32), 0, 10, 3, IntPred::Ne, Flags::NSW);
assert_eq!(bound(&it.func), None);
}
#[test]
fn an_estimate_is_the_count_when_there_is_one_and_a_guess_when_there_is_not() {
let counted_loop = counted(Type::int(32), 0, 7, 1, IntPred::Slt, Flags::NSW);
let (cfg, loops) = analyse(&counted_loop.func);
let id = loops.roots()[0];
let estimate = Scev::new(&counted_loop.func, &cfg, &loops).estimate(id);
assert_eq!(estimate.iterations(), 7);
assert!(!estimate.is_guess());
let uncounted = counted(Type::int(32), 0, 100, 0, IntPred::Slt, Flags::NSW);
let (cfg, loops) = analyse(&uncounted.func);
let id = loops.roots()[0];
let estimate = Scev::new(&uncounted.func, &cfg, &loops).estimate(id);
assert!(estimate.is_guess());
assert_eq!(estimate.iterations(), super::ASSUMED_ITERATIONS);
}
#[test]
fn a_value_the_loop_does_not_touch_is_invariant_rather_than_unknown() {
let it = counted(Type::int(32), 0, 100, 1, IntPred::Slt, Flags::NSW);
let (cfg, loops) = analyse(&it.func);
let id = loops.roots()[0];
let mut scev = Scev::new(&it.func, &cfg, &loops);
assert_eq!(
scev.evolution(id, it.counter).chrec().expect("it evolves").base,
Invariant::number(0)
);
}
#[test]
fn every_assumption_says_what_it_is_in_a_line() {
let it = counted(Type::int(8), 0, 100, 1, IntPred::Ult, Flags::NONE);
let found = bound(&it.func).expect("it is counted");
for assumption in found.assumptions() {
let line = assumption.describe();
assert!(!line.is_empty());
assert!(!line.contains('\n'), "an assumption is one line: {line}");
}
}
}