use std::collections::{BTreeMap, HashMap, HashSet};
use rucc_ir::{Block, Def, Extra, Func, Inst, IntPred, Opcode, Value};
use super::ops::{self, Truth, Undo};
use super::{PAIRS, Range};
use crate::cfg::Cfg;
use crate::dom::Dominators;
const RELATIONS: usize = 16;
const EXCLUSIONS: usize = PAIRS + 1;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Options {
pub logical_depth: u32,
pub recompute_depth: u32,
pub refinements: usize,
}
impl Default for Options {
fn default() -> Self {
Self { logical_depth: 6, recompute_depth: 5, refinements: 8 }
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Counts {
queries: u64,
hits: u64,
fallbacks: u64,
full: u64,
assumed: u64,
lost: BTreeMap<Opcode, u64>,
}
impl Counts {
#[must_use]
pub const fn queries(&self) -> u64 {
self.queries
}
#[must_use]
pub const fn hits(&self) -> u64 {
self.hits
}
#[must_use]
pub const fn fallbacks(&self) -> u64 {
self.fallbacks
}
#[must_use]
pub const fn full(&self) -> u64 {
self.full
}
#[must_use]
pub const fn assumed(&self) -> u64 {
self.assumed
}
#[must_use]
pub fn losses(&self) -> Vec<(Opcode, u64)> {
let mut losses: Vec<(Opcode, u64)> = self.lost.iter().map(|(&op, &n)| (op, n)).collect();
losses.sort_by_key(|&(opcode, count)| (std::cmp::Reverse(count), opcode));
losses
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Relation {
left: Value,
pred: IntPred,
right: Value,
}
#[derive(Clone, Debug, Default)]
struct Entry {
at_def: Option<Range>,
refined: HashMap<Block, Range>,
}
#[derive(Debug)]
pub struct Ranges<'a> {
func: &'a Func,
cfg: &'a Cfg,
dom: &'a Dominators,
options: Options,
cache: HashMap<Value, Entry>,
relations: HashMap<Block, Vec<Relation>>,
counts: Counts,
active: HashSet<Value>,
cycles: u64,
}
impl<'a> Ranges<'a> {
#[must_use]
pub fn new(func: &'a Func, cfg: &'a Cfg, dom: &'a Dominators) -> Self {
Self::with(func, cfg, dom, Options::default())
}
#[must_use]
pub fn with(func: &'a Func, cfg: &'a Cfg, dom: &'a Dominators, options: Options) -> Self {
Self {
func,
cfg,
dom,
options,
cache: HashMap::new(),
relations: HashMap::new(),
counts: Counts::default(),
active: HashSet::new(),
cycles: 0,
}
}
#[must_use]
pub const fn counts(&self) -> &Counts {
&self.counts
}
pub fn of(&mut self, value: Value) -> Range {
self.counts.queries += 1;
self.at_def(value)
}
pub fn at(&mut self, value: Value, block: Block) -> Range {
self.counts.queries += 1;
self.refined(value, block)
}
pub fn at_inst(&mut self, value: Value, inst: Inst) -> Range {
match self.func.block_of(inst) {
Some(block) => self.at(value, block),
None => self.of(value),
}
}
pub fn compare(&mut self, pred: IntPred, a: Value, b: Value, block: Block) -> Truth {
let (left, right) = (self.at(a, block), self.at(b, block));
if left.width() != right.width() {
return Truth::Either;
}
match ops::compare(pred, left, right) {
Truth::Either => (),
settled => return settled,
}
match self.relation(a, b, block) {
Some(known) if implies(known, pred) => Truth::Always,
Some(known) if excludes(known, pred) => Truth::Never,
_ => Truth::Either,
}
}
pub fn relation(&mut self, a: Value, b: Value, block: Block) -> Option<IntPred> {
let facts = self.facts(block).clone();
if let Some(direct) = read(&facts, a, b) {
return Some(direct);
}
for step in &facts {
for middle in [step.left, step.right] {
if middle == a || middle == b {
continue;
}
let composed = read(&facts, a, middle)
.zip(read(&facts, middle, b))
.and_then(|(first, second)| compose(first, second));
if composed.is_some() {
return composed;
}
}
}
None
}
fn at_def(&mut self, value: Value) -> Range {
let ty = self.func[value].ty;
if !ty.is_int() || !ty.is_scalar() {
return Range::of(ty);
}
if let Some(cached) = self.cache.get(&value).and_then(|entry| entry.at_def) {
self.counts.hits += 1;
return cached;
}
if !self.active.insert(value) {
self.cycles += 1;
return Range::of(ty);
}
let before = self.cycles;
let range = self.compute(value);
self.active.remove(&value);
if self.cycles == before {
self.cache.entry(value).or_default().at_def = Some(range);
}
range
}
fn compute(&mut self, value: Value) -> Range {
let ty = self.func[value].ty;
match self.func[value].def {
Def::Param { block, index } => self.of_param(value, block, index),
Def::Result { inst, .. } => {
let range = self.of_inst(value, inst);
if range.is_full() {
self.counts.full += 1;
*self.counts.lost.entry(self.func[inst].opcode).or_default() += 1;
}
debug_assert_eq!(range.width(), ty.bits(), "a range of the wrong width");
range
}
}
}
fn of_param(&mut self, value: Value, block: Block, index: u32) -> Range {
let ty = self.func[value].ty;
if self.cfg.entry() == Some(block) {
return Range::of(ty);
}
let preds: Vec<Block> = self.cfg.predecessors(block).to_vec();
if preds.is_empty() {
return Range::of(ty);
}
let mut range = Range::empty(ty.bits());
for pred in preds {
let Some(arg) = argument(self.func, pred, block, index as usize) else {
return Range::of(ty);
};
let incoming = self.refined(arg, pred);
let edge = self.edge_fact(pred, block, arg).unwrap_or_else(|| Range::of(ty));
range = range.union(incoming.intersect(edge));
if range.is_full() {
return range;
}
}
range
}
fn of_inst(&mut self, value: Value, inst: Inst) -> Range {
let ty = self.func[value].ty;
let width = ty.bits();
let data = self.func[inst];
let block = self.func.block_of(inst);
let args: Vec<Value> = self.func[data.args].to_vec();
let flags = data.flags;
let operand = |this: &mut Self, index: usize| match (args.get(index), block) {
(Some(&arg), Some(block)) => this.refined(arg, block),
(Some(&arg), None) => this.at_def(arg),
(None, _) => Range::of(ty),
};
match data.opcode {
Opcode::IConst => {
let Extra::Imm(at) = data.extra else { return Range::of(ty) };
Range::exactly(self.func[at].unsigned(), width)
}
Opcode::Add | Opcode::Sub | Opcode::Mul => {
let (a, b) = (operand(self, 0), operand(self, 1));
if a.width() != b.width() {
return Range::of(ty);
}
let apply = |flags| match data.opcode {
Opcode::Add => ops::add(a, b, flags),
Opcode::Sub => ops::sub(a, b, flags),
_ => ops::mul(a, b, flags),
};
self.assuming(apply, flags)
}
Opcode::And | Opcode::Or | Opcode::Xor => {
let (a, b) = (operand(self, 0), operand(self, 1));
if a.width() != b.width() {
return Range::of(ty);
}
match data.opcode {
Opcode::And => ops::and(a, b),
Opcode::Or => ops::or(a, b),
_ => ops::xor(a, b),
}
}
Opcode::Shl | Opcode::LShr | Opcode::AShr => {
let (a, count) = (operand(self, 0), operand(self, 1));
if a.width() != count.width() {
return Range::of(ty);
}
let apply = |flags| match data.opcode {
Opcode::Shl => ops::shl(a, count, flags),
Opcode::LShr => ops::lshr(a, count, flags),
_ => ops::ashr(a, count, flags),
};
self.assuming(apply, flags)
}
Opcode::Trunc => ops::trunc(operand(self, 0), width),
Opcode::ZExt => ops::zext(operand(self, 0), width),
Opcode::SExt => ops::sext(operand(self, 0), width),
Opcode::ICmp => {
let Extra::IntPred(pred) = data.extra else { return Range::of(ty) };
let (a, b) = (operand(self, 0), operand(self, 1));
if a.width() != b.width() {
return Range::of(ty);
}
match ops::compare(pred, a, b) {
Truth::Always => Range::exactly(1, width),
Truth::Never => Range::exactly(0, width),
Truth::Either => Range::of(ty),
}
}
Opcode::Ctlz | Opcode::Cttz | Opcode::Ctpop => {
let counted = args.first().map_or(width, |&arg| self.func[arg].ty.bits());
Range::between(0, u128::from(counted), width)
}
_ => Range::of(ty),
}
}
fn assuming(
&mut self,
apply: impl Fn(rucc_ir::Flags) -> Range,
flags: rucc_ir::Flags,
) -> Range {
let range = apply(flags);
if !flags.is_empty() && range != apply(rucc_ir::Flags::NONE) {
self.counts.assumed += 1;
}
range
}
fn refined(&mut self, value: Value, block: Block) -> Range {
let ty = self.func[value].ty;
if !ty.is_int() || !ty.is_scalar() {
return Range::of(ty);
}
if let Some(&cached) = self.cache.get(&value).and_then(|e| e.refined.get(&block)) {
self.counts.hits += 1;
return cached;
}
let full = self
.cache
.get(&value)
.is_some_and(|entry| entry.refined.len() >= self.options.refinements);
if full {
self.counts.fallbacks += 1;
return self.at_def(value);
}
let before = self.cycles;
let range = self.walk(value, block);
if self.cycles == before {
let entry = self.cache.entry(value).or_default();
if entry.refined.len() < self.options.refinements {
entry.refined.insert(block, range);
}
}
range
}
fn walk(&mut self, value: Value, block: Block) -> Range {
let mut range = self.at_def(value);
let stop = defining_block(self.func, value);
let mut cursor = block;
let mut steps = 0;
while steps < self.options.recompute_depth && Some(cursor) != stop {
let Some(parent) = self.dom.immediate_dominator(cursor) else { break };
if self.cfg.predecessors(cursor) == [parent] {
if let Some(fact) = self.edge_fact(parent, cursor, value) {
range = range.intersect(fact);
}
}
cursor = parent;
steps += 1;
}
range
}
fn edge_fact(&mut self, from: Block, to: Block, value: Value) -> Option<Range> {
let term = self.func.terminator(from)?;
let depth = self.options.logical_depth;
match self.func[term].opcode {
Opcode::BrIf => {
let calls: Vec<_> = self.func.successors(term).collect();
let (then, other) = (calls.first()?, calls.get(1)?);
if then.block == other.block {
return None;
}
let taken = then.block == to;
let cond = *self.func[self.func[term].args].first()?;
self.condition_fact(cond, taken, value, from, depth)
}
Opcode::Switch => self.switch_fact(term, to, value, from, depth),
_ => None,
}
}
fn switch_fact(
&mut self,
term: Inst,
to: Block,
value: Value,
block: Block,
depth: u32,
) -> Option<Range> {
if depth == 0 {
return None;
}
let Extra::Switch(info) = self.func[term].extra else { return None };
let info = self.func[info];
let calls: Vec<_> = self.func[info.targets].to_vec();
let cases: Vec<_> = self.func[info.cases].to_vec();
let subject = *self.func[self.func[term].args].first()?;
let width = self.func[subject].ty.bits();
let default = calls.first()?.block;
let hits: Vec<usize> = (1..calls.len()).filter(|&index| calls[index].block == to).collect();
let known = if default == to {
if !hits.is_empty() {
return None;
}
let mut range = Range::full(width);
for &case in cases.iter().take(EXCLUSIONS) {
range = range.intersect(Range::other_than(case.unsigned(), width));
}
range
} else {
let pairs: Vec<(u128, u128)> = hits
.iter()
.filter_map(|&index| cases.get(index - 1))
.map(|case| (case.unsigned(), case.unsigned()))
.collect();
if pairs.is_empty() {
return None;
}
Range::from_pairs(&pairs, width)
};
self.carry_back(subject, known, value, block, depth - 1)
}
fn condition_fact(
&mut self,
cond: Value,
taken: bool,
value: Value,
block: Block,
depth: u32,
) -> Option<Range> {
if depth == 0 {
return None;
}
if cond == value {
let width = self.func[value].ty.bits();
return Some(Range::exactly(u128::from(taken), width));
}
let Def::Result { inst, .. } = self.func[cond].def else { return None };
let data = self.func[inst];
let args: Vec<Value> = self.func[data.args].to_vec();
match data.opcode {
Opcode::ICmp => {
let Extra::IntPred(pred) = data.extra else { return None };
let pred = if taken { pred } else { pred.inverse() };
let (&left, &right) = (args.first()?, args.get(1)?);
let (a, b) = (self.refined(left, block), self.refined(right, block));
if a.width() != b.width() {
return None;
}
let want = ops::narrow_for(pred, a, b);
if let Some(found) = self.carry_back(left, want, value, block, depth - 1) {
return Some(found);
}
let want = ops::narrow_for(pred.swapped(), b, a);
self.carry_back(right, want, value, block, depth - 1)
}
Opcode::And | Opcode::Or => {
let holds = data.opcode == Opcode::And;
if taken != holds {
return None;
}
let (&left, &right) = (args.first()?, args.get(1)?);
let a = self.condition_fact(left, taken, value, block, depth - 1);
let b = self.condition_fact(right, taken, value, block, depth - 1);
match (a, b) {
(Some(a), Some(b)) => Some(a.intersect(b)),
(found, None) | (None, found) => found,
}
}
Opcode::Xor => {
let (&left, &right) = (args.first()?, args.get(1)?);
let (cond, other) = match self.constant(right) {
Some(_) => (left, right),
None => (right, left),
};
let one = self.constant(other)? == 1 && self.func[other].ty.bits() == 1;
if !one {
return None;
}
self.condition_fact(cond, !taken, value, block, depth - 1)
}
_ => None,
}
}
fn carry_back(
&mut self,
subject: Value,
known: Range,
value: Value,
block: Block,
depth: u32,
) -> Option<Range> {
if subject == value {
return Some(known);
}
if depth == 0 || known.is_full() {
return None;
}
let Def::Result { inst, .. } = self.func[subject].def else { return None };
let data = self.func[inst];
let args: Vec<Value> = self.func[data.args].to_vec();
let (&left, right) = (args.first()?, args.get(1).copied());
let steps: Vec<(Value, Undo, Option<Value>)> = match data.opcode {
Opcode::Add => vec![(left, Undo::AddLeft, right), (right?, Undo::AddLeft, Some(left))],
Opcode::Sub => vec![(left, Undo::SubLeft, right), (right?, Undo::SubRight, Some(left))],
Opcode::Xor => vec![(left, Undo::Xor, right), (right?, Undo::Xor, Some(left))],
Opcode::ZExt => vec![(left, Undo::Zext(self.func[left].ty.bits()), None)],
Opcode::SExt => vec![(left, Undo::Sext(self.func[left].ty.bits()), None)],
_ => return None,
};
for (operand, undo, other) in steps {
let other = match other {
Some(other) => self.refined(other, block),
None => Range::full(known.width()),
};
if other.width() != known.width() {
continue;
}
let back = ops::backward(undo, known, other);
if let Some(found) = self.carry_back(operand, back, value, block, depth - 1) {
return Some(found);
}
}
None
}
fn facts(&mut self, block: Block) -> &Vec<Relation> {
if !self.relations.contains_key(&block) {
let mut facts = match self.dom.immediate_dominator(block) {
Some(parent) => self.facts(parent).clone(),
None => Vec::new(),
};
if let Some(own) = self.own_relation(block) {
facts.push(own);
if facts.len() > RELATIONS {
facts.remove(0);
}
}
self.relations.insert(block, facts);
}
&self.relations[&block]
}
fn own_relation(&mut self, block: Block) -> Option<Relation> {
let [from] = *self.cfg.predecessors(block) else { return None };
let term = self.func.terminator(from)?;
if self.func[term].opcode != Opcode::BrIf {
return None;
}
let calls: Vec<_> = self.func.successors(term).collect();
let (then, other) = (calls.first()?, calls.get(1)?);
if then.block == other.block {
return None;
}
let taken = then.block == block;
let cond = *self.func[self.func[term].args].first()?;
let Def::Result { inst, .. } = self.func[cond].def else { return None };
if self.func[inst].opcode != Opcode::ICmp {
return None;
}
let Extra::IntPred(pred) = self.func[inst].extra else { return None };
let args = &self.func[self.func[inst].args];
let (&left, &right) = (args.first()?, args.get(1)?);
let pred = if taken { pred } else { pred.inverse() };
Some(Relation { left, pred, right })
}
fn constant(&self, value: Value) -> Option<u128> {
let Def::Result { inst, .. } = self.func[value].def else { return None };
if self.func[inst].opcode != Opcode::IConst {
return None;
}
let Extra::Imm(at) = self.func[inst].extra else { return None };
Some(self.func[at].unsigned())
}
}
fn defining_block(func: &Func, value: Value) -> Option<Block> {
match func[value].def {
Def::Param { block, .. } => Some(block),
Def::Result { inst, .. } => func.block_of(inst),
}
}
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
}
fn read(facts: &[Relation], a: Value, b: Value) -> Option<IntPred> {
facts.iter().rev().find_map(|fact| {
if fact.left == a && fact.right == b {
Some(fact.pred)
} else if fact.left == b && fact.right == a {
Some(fact.pred.swapped())
} else {
None
}
})
}
const fn outcomes(pred: IntPred) -> u8 {
match pred {
IntPred::Eq => 0b010,
IntPred::Ne => 0b101,
IntPred::Slt | IntPred::Ult => 0b001,
IntPred::Sle | IntPred::Ule => 0b011,
IntPred::Sgt | IntPred::Ugt => 0b100,
IntPred::Sge | IntPred::Uge => 0b110,
}
}
const fn comparable(a: IntPred, b: IntPred) -> bool {
ordering_free(a) || ordering_free(b) || a.is_signed() == b.is_signed()
}
const fn ordering_free(pred: IntPred) -> bool {
matches!(pred, IntPred::Eq | IntPred::Ne)
}
fn implies(known: IntPred, pred: IntPred) -> bool {
comparable(known, pred) && outcomes(known) & !outcomes(pred) == 0
}
fn excludes(known: IntPred, pred: IntPred) -> bool {
comparable(known, pred) && outcomes(known) & outcomes(pred) == 0
}
fn compose(first: IntPred, second: IntPred) -> Option<IntPred> {
if !comparable(first, second) {
return None;
}
let strict = |pred| matches!(pred, IntPred::Slt | IntPred::Ult | IntPred::Sgt | IntPred::Ugt);
let direction = |pred| outcomes(pred) & 0b101;
match (first, second) {
(IntPred::Eq, other) | (other, IntPred::Eq) => Some(other),
(IntPred::Ne, _) | (_, IntPred::Ne) => None,
_ if direction(first) != direction(second) => None,
_ if strict(first) => Some(first),
_ => Some(second),
}
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{Block, Builder, Flags, Func, IntPred, Opcode, Signature, Type, Value};
use super::{Options, Ranges};
use crate::cfg::Cfg;
use crate::dom::Dominators;
use crate::range::Range;
use crate::range::ops::{self, Truth};
const I32: Type = Type::int(32);
fn shape(params: usize, blocks: usize) -> (Func, Vec<Value>, Vec<Block>) {
let mut names = Interner::new();
let types = vec![I32; params];
let mut func = Func::new(names.intern("f"), Signature::new().with_params(&types));
let blocks: Vec<Block> = (0..blocks).map(|_| func.create_block()).collect();
let args = types.iter().map(|&ty| func.append_param(blocks[0], ty)).collect();
(func, args, blocks)
}
struct Asked {
cfg: Cfg,
dom: Dominators,
func: Func,
}
impl Asked {
fn new(func: Func) -> Self {
let cfg = Cfg::new(&func);
let dom = Dominators::new(&cfg);
Asked { cfg, dom, func }
}
fn ranges(&self) -> Ranges<'_> {
Ranges::new(&self.func, &self.cfg, &self.dom)
}
fn with(&self, options: Options) -> Ranges<'_> {
Ranges::with(&self.func, &self.cfg, &self.dom, options)
}
}
fn bounds(range: Range) -> Option<(i128, i128)> {
range.signed_bounds()
}
#[test]
fn a_constant_is_itself() {
let (mut func, _, blocks) = shape(0, 1);
let mut build = Builder::new(&mut func, blocks[0]);
let seven = build.iconst(I32, 7);
build.ret(&[]);
let asked = Asked::new(func);
assert_eq!(asked.ranges().of(seven).singleton(), Some(7));
}
#[test]
fn arithmetic_on_constants_is_the_arithmetic() {
let (mut func, _, blocks) = shape(0, 1);
let mut build = Builder::new(&mut func, blocks[0]);
let a = build.iconst(I32, 7);
let b = build.iconst(I32, 5);
let sum = build.binary(Opcode::Add, a, b, Flags::NONE);
build.ret(&[]);
let asked = Asked::new(func);
assert_eq!(asked.ranges().of(sum).singleton(), Some(12));
}
#[test]
fn a_value_nothing_is_known_about_is_the_whole_of_its_type_and_says_which_opcode_lost_it() {
let (mut func, args, blocks) = shape(1, 1);
let mut build = Builder::new(&mut func, blocks[0]);
let counted = build.unary(Opcode::Ctlz, args[0], I32);
let squared = build.binary(Opcode::Mul, args[0], args[0], Flags::NONE);
build.ret(&[]);
let asked = Asked::new(func);
let mut ranges = asked.ranges();
assert!(ranges.of(args[0]).is_full(), "a parameter is anything");
assert_eq!(bounds(ranges.of(counted)), Some((0, 32)));
assert!(ranges.of(squared).is_full());
assert_eq!(ranges.counts().losses(), vec![(Opcode::Mul, 1)]);
}
fn guarded(pred: IntPred, bound: i128) -> (Func, Value, Block, Block) {
let (mut func, args, blocks) = shape(1, 3);
let mut build = Builder::new(&mut func, blocks[0]);
let limit = build.iconst(I32, bound);
let test = build.icmp(pred, args[0], limit);
build.br_if(test, blocks[1], &[], blocks[2], &[]);
Builder::new(&mut func, blocks[1]).ret(&[]);
Builder::new(&mut func, blocks[2]).ret(&[]);
(func, args[0], blocks[1], blocks[2])
}
#[test]
fn a_branch_narrows_the_value_it_tested_on_both_of_its_edges() {
let (func, x, then, otherwise) = guarded(IntPred::Slt, 10);
let asked = Asked::new(func);
let mut ranges = asked.ranges();
assert_eq!(bounds(ranges.at(x, then)), Some((i128::from(i32::MIN), 9)));
assert_eq!(bounds(ranges.at(x, otherwise)), Some((10, i128::from(i32::MAX))));
}
#[test]
fn the_range_at_the_definition_is_not_the_range_at_the_use() {
let (func, x, then, _) = guarded(IntPred::Ult, 64);
let asked = Asked::new(func);
let mut ranges = asked.ranges();
assert!(ranges.of(x).is_full(), "nothing is known where it is defined");
assert_eq!(ranges.at(x, then).unsigned_bounds(), Some((0, 63)));
}
#[test]
fn a_null_check_is_the_fact_a_single_interval_cannot_hold() {
let (func, x, _, otherwise) = guarded(IntPred::Eq, 0);
let asked = Asked::new(func);
let mut ranges = asked.ranges();
let range = ranges.at(x, otherwise);
assert!(range.nonzero(), "the else edge of an equality with zero proves it");
assert_eq!(range.pairs().len(), 1);
}
fn through_arithmetic(offset: i128, bound: i128) -> (Func, Value, Block) {
let (mut func, args, blocks) = shape(1, 3);
let mut build = Builder::new(&mut func, blocks[0]);
let by = build.iconst(I32, offset);
let shifted = build.binary(Opcode::Add, args[0], by, Flags::NSW);
let limit = build.iconst(I32, bound);
let test = build.icmp(IntPred::Slt, shifted, limit);
build.br_if(test, blocks[1], &[], blocks[2], &[]);
Builder::new(&mut func, blocks[1]).ret(&[]);
Builder::new(&mut func, blocks[2]).ret(&[]);
(func, args[0], blocks[1])
}
#[test]
fn the_condition_is_inverted_back_to_the_value_it_was_computed_from() {
let (func, x, then) = through_arithmetic(3, 10);
let asked = Asked::new(func);
let mut ranges = asked.ranges();
let (_, high) = bounds(ranges.at(x, then)).expect("not empty");
assert!(high <= 6, "x + 3 < 10 makes x at most six, and this said {high}");
}
#[test]
fn the_inversion_stops_where_it_is_told_to() {
let (func, x, then) = through_arithmetic(3, 10);
let asked = Asked::new(func);
let options = Options { logical_depth: 1, ..Options::default() };
let mut ranges = asked.with(options);
assert!(ranges.at(x, then).is_full(), "one step cannot reach past the comparison");
}
#[test]
fn a_value_carried_round_a_loop_is_not_pinned_down_and_the_branch_still_says_something() {
let (mut func, _, blocks) = shape(0, 4);
let counter = func.append_param(blocks[1], I32);
let mut build = Builder::new(&mut func, blocks[0]);
let start = build.iconst(I32, 0);
build.jump(blocks[1], &[start]);
let mut build = Builder::new(&mut func, blocks[1]);
let limit = build.iconst(I32, 100);
let test = build.icmp(IntPred::Slt, counter, limit);
build.br_if(test, blocks[2], &[], blocks[3], &[]);
let mut build = Builder::new(&mut func, blocks[2]);
let one = build.iconst(I32, 1);
let next = build.binary(Opcode::Add, counter, one, Flags::NSW);
build.jump(blocks[1], &[next]);
Builder::new(&mut func, blocks[3]).ret(&[]);
let asked = Asked::new(func);
let mut ranges = asked.ranges();
let at_def = ranges.of(counter);
assert!(at_def.contains(0) && at_def.contains(50) && at_def.contains(100));
assert_eq!(bounds(at_def), Some((i128::from(i32::MIN) + 1, 100)));
let (_, inside) = bounds(ranges.at(counter, blocks[2])).expect("not empty");
assert_eq!(inside, 99);
let (after, _) = bounds(ranges.at(counter, blocks[3])).expect("not empty");
assert_eq!(after, 100);
}
#[test]
fn a_block_parameter_is_everything_its_predecessors_pass_to_it() {
let (mut func, args, blocks) = shape(1, 4);
let merged = func.append_param(blocks[3], I32);
let mut build = Builder::new(&mut func, blocks[0]);
let zero = build.iconst(I32, 0);
let cond = build.icmp(IntPred::Slt, args[0], zero);
build.br_if(cond, blocks[1], &[], blocks[2], &[]);
let mut build = Builder::new(&mut func, blocks[1]);
let five = build.iconst(I32, 5);
build.jump(blocks[3], &[five]);
let mut build = Builder::new(&mut func, blocks[2]);
let nine = build.iconst(I32, 9);
build.jump(blocks[3], &[nine]);
Builder::new(&mut func, blocks[3]).ret(&[]);
let asked = Asked::new(func);
let mut ranges = asked.ranges();
let range = ranges.of(merged);
assert!(range.contains(5) && range.contains(9), "both arms are in it");
assert!(!range.contains(7), "and nothing between them is");
}
#[test]
fn a_switch_edge_pins_its_cases_and_the_default_excludes_them() {
let (mut func, args, blocks) = shape(1, 3);
let mut build = Builder::new(&mut func, blocks[0]);
build.switch(args[0], blocks[2], &[(4, blocks[1]), (7, blocks[1])]);
Builder::new(&mut func, blocks[1]).ret(&[]);
Builder::new(&mut func, blocks[2]).ret(&[]);
let asked = Asked::new(func);
let mut ranges = asked.ranges();
assert_eq!(ranges.at(args[0], blocks[1]).list(4), Some(vec![4, 7]), "the two cases");
let fell_through = ranges.at(args[0], blocks[2]);
assert!(!fell_through.contains(4) && !fell_through.contains(7));
assert!(fell_through.contains(5), "and everything else is still possible");
}
#[test]
fn both_arms_of_an_and_hold_where_it_is_true() {
let (mut func, args, blocks) = shape(1, 3);
let mut build = Builder::new(&mut func, blocks[0]);
let low = build.iconst(I32, 10);
let high = build.iconst(I32, 20);
let above = build.icmp(IntPred::Sgt, args[0], low);
let below = build.icmp(IntPred::Slt, args[0], high);
let both = build.binary(Opcode::And, above, below, Flags::NONE);
build.br_if(both, blocks[1], &[], blocks[2], &[]);
Builder::new(&mut func, blocks[1]).ret(&[]);
Builder::new(&mut func, blocks[2]).ret(&[]);
let asked = Asked::new(func);
let mut ranges = asked.ranges();
assert_eq!(bounds(ranges.at(args[0], blocks[1])), Some((11, 19)));
assert!(ranges.at(args[0], blocks[2]).is_full(), "the false edge says nothing");
}
#[test]
fn a_comparison_the_ranges_settle_is_settled() {
let (func, x, then, _) = guarded(IntPred::Slt, 10);
let mut asked = Asked::new(func);
let ten = {
let mut build = Builder::new(&mut asked.func, then);
build.iconst(I32, 10)
};
let asked = Asked::new(asked.func);
let mut ranges = asked.ranges();
assert_eq!(ranges.compare(IntPred::Slt, x, ten, then), Truth::Always);
assert_eq!(ranges.compare(IntPred::Sgt, x, ten, then), Truth::Never);
}
fn related() -> (Func, Value, Value, Vec<Block>) {
let (mut func, args, blocks) = shape(2, 4);
let mut build = Builder::new(&mut func, blocks[0]);
let test = build.icmp(IntPred::Slt, args[0], args[1]);
build.br_if(test, blocks[1], &[], blocks[2], &[]);
Builder::new(&mut func, blocks[1]).jump(blocks[3], &[]);
Builder::new(&mut func, blocks[2]).jump(blocks[3], &[]);
Builder::new(&mut func, blocks[3]).ret(&[]);
(func, args[0], args[1], blocks)
}
#[test]
fn a_relation_the_intervals_cannot_see_is_still_known() {
let (func, a, b, blocks) = related();
let asked = Asked::new(func);
let mut ranges = asked.ranges();
let (left, right) = (ranges.at(a, blocks[1]), ranges.at(b, blocks[1]));
assert_eq!(ops::compare(IntPred::Slt, left, right), Truth::Either);
assert_eq!(ranges.relation(a, b, blocks[1]), Some(IntPred::Slt));
assert_eq!(ranges.compare(IntPred::Slt, a, b, blocks[1]), Truth::Always);
assert_eq!(ranges.compare(IntPred::Sge, a, b, blocks[1]), Truth::Never);
assert_eq!(ranges.compare(IntPred::Ne, a, b, blocks[1]), Truth::Always);
assert_eq!(ranges.compare(IntPred::Ult, a, b, blocks[1]), Truth::Either);
}
#[test]
fn a_relation_belongs_to_the_block_the_edge_led_to() {
let (func, a, b, blocks) = related();
let asked = Asked::new(func);
let mut ranges = asked.ranges();
assert_eq!(ranges.relation(a, b, blocks[1]), Some(IntPred::Slt));
assert_eq!(ranges.relation(a, b, blocks[2]), Some(IntPred::Sge), "the other edge");
assert_eq!(ranges.relation(a, b, blocks[3]), None, "where they meet, neither holds");
assert_eq!(ranges.compare(IntPred::Slt, a, b, blocks[3]), Truth::Either);
}
#[test]
fn one_step_of_composition_is_taken() {
let (mut func, args, blocks) = shape(3, 4);
let [a, b, c] = [args[0], args[1], args[2]];
let mut build = Builder::new(&mut func, blocks[0]);
let first = build.icmp(IntPred::Slt, a, b);
build.br_if(first, blocks[1], &[], blocks[3], &[]);
let mut build = Builder::new(&mut func, blocks[1]);
let second = build.icmp(IntPred::Sle, b, c);
build.br_if(second, blocks[2], &[], blocks[3], &[]);
Builder::new(&mut func, blocks[2]).ret(&[]);
Builder::new(&mut func, blocks[3]).ret(&[]);
let asked = Asked::new(func);
let mut ranges = asked.ranges();
assert_eq!(ranges.relation(a, c, blocks[2]), Some(IntPred::Slt), "a < b and b <= c");
assert_eq!(ranges.compare(IntPred::Slt, a, c, blocks[2]), Truth::Always);
}
#[test]
fn the_cache_gives_up_rather_than_growing_without_a_bound() {
let (func, x, then, otherwise) = guarded(IntPred::Slt, 10);
let asked = Asked::new(func);
let options = Options { refinements: 1, ..Options::default() };
let mut ranges = asked.with(options);
assert_eq!(bounds(ranges.at(x, then)), Some((i128::from(i32::MIN), 9)));
assert!(ranges.at(x, otherwise).is_full(), "past the bound it is the definition range");
assert_eq!(ranges.counts().fallbacks(), 1);
}
#[test]
fn asking_twice_asks_the_cache_the_second_time() {
let (func, x, then, _) = guarded(IntPred::Slt, 10);
let asked = Asked::new(func);
let mut ranges = asked.ranges();
let first = ranges.at(x, then);
let hits = ranges.counts().hits();
let second = ranges.at(x, then);
assert_eq!(first, second);
assert!(ranges.counts().hits() > hits, "the second query hit the cache");
assert_eq!(ranges.counts().queries(), 2);
}
#[test]
fn a_range_that_is_only_true_because_overflow_is_undefined_is_counted() {
let (mut func, args, blocks) = shape(1, 1);
let mut build = Builder::new(&mut func, blocks[0]);
let big = build.iconst(I32, i128::from(i32::MAX) - 4);
let counted = build.unary(Opcode::Ctlz, args[0], I32);
let sum = build.binary(Opcode::Add, counted, big, Flags::NSW);
build.ret(&[]);
let asked = Asked::new(func);
let mut ranges = asked.ranges();
assert!(!ranges.of(sum).is_full(), "the promise not to overflow bounds the sum");
assert_eq!(ranges.counts().assumed(), 1);
}
#[test]
fn a_query_about_something_that_is_not_an_integer_answers_without_pretending() {
let (mut func, _, blocks) = shape(0, 1);
let mut build = Builder::new(&mut func, blocks[0]);
let mem = build.mem_entry();
build.ret(&[]);
let asked = Asked::new(func);
let mut ranges = asked.ranges();
assert!(ranges.of(mem).is_full());
assert_eq!(ranges.counts().full(), 0, "a memory value is not a lost integer");
}
}