use std::{
collections::{HashMap, HashSet, VecDeque},
marker::PhantomData,
};
use crate::{
analysis::{
cfg::SsaCfg,
range::{IntervalRange, ValueRange},
},
bitset::BitSet,
events::{EventKind, EventListener},
graph::{NodeId, RootedGraph, Successors, algorithms::DominatorTree},
ir::{
block::SsaBlock,
function::{SsaEditOptions, SsaFunction},
instruction::SsaInstruction,
ops::SsaOp,
phi::PhiNode,
value::ConstValue,
variable::SsaVarId,
},
target::Target,
};
#[derive(Debug, Clone, Default)]
pub struct ValueRanges {
ranges: HashMap<SsaVarId, ValueRange>,
}
impl ValueRanges {
#[must_use]
pub fn get(&self, var: SsaVarId) -> Option<&ValueRange> {
self.ranges.get(&var)
}
#[must_use]
pub fn len(&self) -> usize {
self.ranges.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.ranges.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (SsaVarId, &ValueRange)> {
self.ranges.iter().map(|(var, range)| (*var, range))
}
#[must_use]
pub fn range_at<T: Target>(
&self,
ssa: &SsaFunction<T>,
dominators: &DominatorTree,
var: SsaVarId,
block: usize,
) -> ValueRange {
let mut range = self.get(var).cloned().unwrap_or_default();
let mut current = NodeId::new(block);
for _ in 0..ssa.block_count() {
let Some(parent) = dominators.immediate_dominator(current) else {
break;
};
if let Some(refined) = self.refine_through_edge(ssa, dominators, var, parent, current) {
range = range.meet(&refined);
}
current = parent;
}
range
}
fn refine_through_edge<T: Target>(
&self,
ssa: &SsaFunction<T>,
dominators: &DominatorTree,
var: SsaVarId,
parent: NodeId,
child: NodeId,
) -> Option<ValueRange> {
let block = ssa.block(parent.index())?;
let SsaOp::Branch {
condition,
true_target,
false_target,
} = block.terminator_op()?
else {
return None;
};
let via_true =
dominators.dominates(NodeId::new(*true_target), child) || child.index() == *true_target;
let via_false = dominators.dominates(NodeId::new(*false_target), child)
|| child.index() == *false_target;
let taken = match (via_true, via_false) {
(true, false) => true,
(false, true) => false,
_ => return None,
};
let comparison = ssa.get_definition(*condition)?;
self.refine_from_comparison(comparison, var, taken)
}
fn refine_from_comparison<T: Target>(
&self,
comparison: &SsaOp<T>,
var: SsaVarId,
taken: bool,
) -> Option<ValueRange> {
match comparison {
SsaOp::Clt { left, right, .. } => {
if *left == var {
let limit = self.get(*right)?;
if taken {
limit
.max()
.and_then(|m| m.checked_sub(1))
.map(|upper| ValueRange::Interval(IntervalRange::at_most(upper)))
} else {
limit
.min()
.map(|lower| ValueRange::Interval(IntervalRange::at_least(lower)))
}
} else if *right == var {
let limit = self.get(*left)?;
if taken {
limit
.min()
.and_then(|m| m.checked_add(1))
.map(|lower| ValueRange::Interval(IntervalRange::at_least(lower)))
} else {
limit
.max()
.map(|upper| ValueRange::Interval(IntervalRange::at_most(upper)))
}
} else {
None
}
}
SsaOp::Cgt { left, right, .. } => {
if *left == var {
let limit = self.get(*right)?;
if taken {
limit
.min()
.and_then(|m| m.checked_add(1))
.map(|lower| ValueRange::Interval(IntervalRange::at_least(lower)))
} else {
limit
.max()
.map(|upper| ValueRange::Interval(IntervalRange::at_most(upper)))
}
} else if *right == var {
let limit = self.get(*left)?;
if taken {
limit
.max()
.and_then(|m| m.checked_sub(1))
.map(|upper| ValueRange::Interval(IntervalRange::at_most(upper)))
} else {
limit
.min()
.map(|lower| ValueRange::Interval(IntervalRange::at_least(lower)))
}
} else {
None
}
}
SsaOp::Ceq { left, right, .. } if taken => {
let other = if *left == var {
*right
} else if *right == var {
*left
} else {
return None;
};
self.get(other).cloned()
}
_ => None,
}
}
}
#[must_use]
pub fn analyze<T: Target>(ssa: &SsaFunction<T>, max_iterations: usize) -> Option<ValueRanges> {
let mut analysis: RangeAnalysis<T> = RangeAnalysis::new(max_iterations);
let result = analysis.analyze(ssa);
result.converged.then_some(ValueRanges {
ranges: result.ranges,
})
}
pub fn run<T, L>(
ssa: &mut SsaFunction<T>,
method: &T::MethodRef,
events: &L,
max_iterations: usize,
) -> bool
where
T: Target,
L: EventListener<T> + ?Sized,
{
let mut analysis: RangeAnalysis<T> = RangeAnalysis::new(max_iterations);
let result = analysis.analyze(ssa);
let mut branch_simplifications: Vec<(usize, usize, bool)> = Vec::new();
let mut comparison_replacements: Vec<(usize, usize, SsaVarId, bool)> = Vec::new();
for (block_idx, block) in ssa.iter_blocks() {
if let Some(SsaOp::Branch {
condition,
true_target,
false_target,
}) = block.terminator_op()
&& let Some(range) = result.get_range(*condition)
{
if let Some(is_true) = range.always_equal_to(0)
&& is_true
{
branch_simplifications.push((block_idx, *false_target, false));
}
if let Some(val) = range.as_constant()
&& val != 0
{
branch_simplifications.push((block_idx, *true_target, true));
}
}
for (instr_idx, instr) in block.instructions().iter().enumerate() {
if let Some((dest, value)) = try_simplify_comparison(instr.op(), &result) {
comparison_replacements.push((block_idx, instr_idx, dest, value));
}
}
}
let mut changed = false;
let result = ssa.edit(SsaEditOptions::new(), |editor| {
for (block_idx, target, is_true) in branch_simplifications {
let Some(_) = editor.function().block(block_idx) else {
continue;
};
editor.fold_terminator_pruning_phis(block_idx, SsaOp::Jump { target })?;
let event = crate::events::Event {
kind: EventKind::OpaquePredicateRemoved,
method: Some(method.clone()),
location: Some(block_idx),
message: format!(
"range analysis: condition always {}",
if is_true { "true" } else { "false" }
),
pass: None,
};
events.push(event);
let event = crate::events::Event {
kind: EventKind::BranchSimplified,
method: Some(method.clone()),
location: Some(block_idx),
message: format!("simplified to unconditional jump to {target}"),
pass: None,
};
events.push(event);
changed = true;
}
for (block_idx, instr_idx, dest, value) in comparison_replacements {
if editor
.function()
.block(block_idx)
.and_then(|block| block.instruction(instr_idx))
.is_none()
{
continue;
}
let const_value = if value {
ConstValue::True
} else {
ConstValue::False
};
editor.replace_instruction_op(
block_idx,
instr_idx,
SsaOp::Const {
dest,
value: const_value,
},
)?;
let event = crate::events::Event {
kind: EventKind::ConstantFolded,
method: Some(method.clone()),
location: Some(instr_idx),
message: format!("range analysis: comparison → {value}"),
pass: None,
};
events.push(event);
changed = true;
}
Ok(())
});
if result.is_err() {
return false;
}
changed
}
fn try_simplify_comparison<T: Target>(
op: &SsaOp<T>,
result: &RangeResult,
) -> Option<(SsaVarId, bool)> {
match op {
SsaOp::Clt {
dest, left, right, ..
} => {
let left_range = result.get_range(*left)?;
let right_range = result.get_range(*right)?;
if let (Some(l_max), Some(r_min)) = (left_range.max(), right_range.min())
&& l_max < r_min
{
return Some((*dest, true));
}
if let (Some(l_min), Some(r_max)) = (left_range.min(), right_range.max())
&& l_min >= r_max
{
return Some((*dest, false));
}
None
}
SsaOp::Cgt {
dest, left, right, ..
} => {
let left_range = result.get_range(*left)?;
let right_range = result.get_range(*right)?;
if let (Some(l_min), Some(r_max)) = (left_range.min(), right_range.max())
&& l_min > r_max
{
return Some((*dest, true));
}
if let (Some(l_max), Some(r_min)) = (left_range.max(), right_range.min())
&& l_max <= r_min
{
return Some((*dest, false));
}
None
}
SsaOp::Ceq { dest, left, right } => {
let left_range = result.get_range(*left)?;
let right_range = result.get_range(*right)?;
if let (Some(l), Some(r)) = (left_range.as_constant(), right_range.as_constant()) {
return Some((*dest, l == r));
}
if !ranges_overlap(left_range, right_range) {
return Some((*dest, false));
}
None
}
_ => None,
}
}
fn ranges_overlap(a: &ValueRange, b: &ValueRange) -> bool {
if a.is_top() || b.is_top() {
return true;
}
if a.is_bottom() || b.is_bottom() {
return false;
}
match (a.max(), a.min(), b.max(), b.min()) {
(Some(a_max), Some(a_min), Some(b_max), Some(b_min)) => a_max >= b_min && a_min <= b_max,
_ => true,
}
}
struct RangeAnalysis<T: Target> {
ranges: HashMap<SsaVarId, ValueRange>,
executable_edges: HashSet<(usize, usize)>,
executable_blocks: BitSet,
ssa_worklist: VecDeque<SsaVarId>,
cfg_worklist: VecDeque<(usize, usize)>,
update_counts: HashMap<SsaVarId, u32>,
max_iterations: usize,
_phantom: PhantomData<T>,
}
const WIDEN_AFTER_REVISIONS: u32 = 3;
impl<T: Target> RangeAnalysis<T> {
fn new(max_iterations: usize) -> Self {
Self {
ranges: HashMap::new(),
executable_edges: HashSet::new(),
executable_blocks: BitSet::new(0),
ssa_worklist: VecDeque::new(),
cfg_worklist: VecDeque::new(),
update_counts: HashMap::new(),
max_iterations,
_phantom: PhantomData,
}
}
fn analyze(&mut self, ssa: &SsaFunction<T>) -> RangeResult {
let cfg = SsaCfg::from_ssa(ssa);
self.initialize(ssa, &cfg);
let converged = self.propagate(ssa, &cfg);
RangeResult {
ranges: std::mem::take(&mut self.ranges),
converged,
}
}
fn initialize<G>(&mut self, ssa: &SsaFunction<T>, cfg: &G)
where
G: RootedGraph + Successors,
{
self.ranges.clear();
self.executable_edges.clear();
self.executable_blocks = BitSet::new(ssa.block_count());
self.ssa_worklist.clear();
self.cfg_worklist.clear();
self.update_counts.clear();
for var in ssa.variables() {
self.ranges.insert(var.id(), ValueRange::top());
}
let entry = cfg.entry().index();
self.mark_block_executable(entry);
for succ in cfg.successors(cfg.entry()) {
self.cfg_worklist.push_back((entry, succ.index()));
}
if let Some(block) = ssa.block(entry) {
self.process_block_definitions(block, ssa);
}
}
fn propagate<G>(&mut self, ssa: &SsaFunction<T>, cfg: &G) -> bool
where
G: RootedGraph + Successors,
{
let work = ssa
.block_count()
.saturating_add(ssa.variable_count())
.saturating_add(16);
let budget = self.max_iterations.saturating_mul(work).saturating_add(64);
let mut steps: usize = 0;
loop {
while let Some((from, to)) = self.cfg_worklist.pop_front() {
steps = steps.saturating_add(1);
if steps > budget {
return false;
}
if self.executable_edges.insert((from, to)) {
self.process_edge(from, to, ssa, cfg);
}
}
let Some(var) = self.ssa_worklist.pop_front() else {
return true;
};
steps = steps.saturating_add(1);
if steps > budget {
return false;
}
self.process_variable_uses(var, ssa, cfg);
}
}
fn process_edge<G>(&mut self, from: usize, to: usize, ssa: &SsaFunction<T>, cfg: &G)
where
G: RootedGraph + Successors,
{
let first_visit = !self.is_block_executable(to);
if first_visit {
self.mark_block_executable(to);
if let Some(block) = ssa.block(to) {
self.process_block_definitions(block, ssa);
}
}
if let Some(block) = ssa.block(to) {
for phi in block.phi_nodes() {
if phi.operand_from(from).is_some() {
let new_range = self.evaluate_phi(phi, to);
self.update_range(phi.result(), &new_range);
}
}
}
if first_visit && let Some(block) = ssa.block(to) {
self.propagate_outgoing_edges(to, block, cfg);
}
}
fn process_block_definitions(&mut self, block: &SsaBlock<T>, ssa: &SsaFunction<T>) {
for instr in block.instructions() {
self.update_instruction_defs(instr, ssa);
}
}
fn process_variable_uses<G>(&mut self, var: SsaVarId, ssa: &SsaFunction<T>, cfg: &G)
where
G: RootedGraph + Successors,
{
if let Some(ssa_var) = ssa.variable(var) {
for use_site in ssa_var.uses() {
let block_id = use_site.block;
if !self.is_block_executable(block_id) {
continue;
}
if use_site.is_phi_operand {
if let Some(block) = ssa.block(block_id)
&& let Some(phi) = block.phi(use_site.instruction)
{
let new_range = self.evaluate_phi(phi, block_id);
self.update_range(phi.result(), &new_range);
}
} else if let Some(block) = ssa.block(block_id)
&& let Some(instr) = block.instruction(use_site.instruction)
{
self.update_instruction_defs(instr, ssa);
if instr.is_terminator() {
self.propagate_outgoing_edges(block_id, block, cfg);
}
}
}
}
}
fn update_instruction_defs(&mut self, instr: &SsaInstruction<T>, ssa: &SsaFunction<T>) {
let primary = instr.op().dest();
let range = self.evaluate_instruction(instr.op(), ssa);
for def in instr.defs() {
if Some(def) == primary {
self.update_range(def, &range);
} else {
self.update_range(def, &ValueRange::top());
}
}
}
fn propagate_outgoing_edges<G>(&mut self, block_id: usize, block: &SsaBlock<T>, cfg: &G)
where
G: RootedGraph + Successors,
{
match block.terminator_op() {
Some(SsaOp::Branch {
condition,
true_target,
false_target,
}) => {
let range = self.get_range(*condition);
if let Some(val) = range.as_constant() {
if val != 0 {
self.add_cfg_edge(block_id, *true_target);
} else {
self.add_cfg_edge(block_id, *false_target);
}
} else if range.always_equal_to(0) == Some(true) {
self.add_cfg_edge(block_id, *false_target);
} else if range.is_always_positive() {
self.add_cfg_edge(block_id, *true_target);
} else if range.is_top() {
} else {
self.add_cfg_edge(block_id, *true_target);
self.add_cfg_edge(block_id, *false_target);
}
}
Some(SsaOp::Switch {
value,
targets,
default,
}) => {
let range = self.get_range(*value);
if let Some(idx) = range.as_constant().and_then(|i| usize::try_from(i).ok()) {
if let Some(&target) = targets.get(idx) {
self.add_cfg_edge(block_id, target);
} else {
self.add_cfg_edge(block_id, *default);
}
} else {
for &target in targets {
self.add_cfg_edge(block_id, target);
}
self.add_cfg_edge(block_id, *default);
}
}
Some(SsaOp::Jump { target }) => {
self.add_cfg_edge(block_id, *target);
}
Some(
SsaOp::Return { .. }
| SsaOp::Throw { .. }
| SsaOp::Rethrow
| SsaOp::EndFinally
| SsaOp::EndFilter { .. }
| SsaOp::InterruptReturn,
) => {}
_ => {
let node = NodeId::new(block_id);
for succ in cfg.successors(node) {
self.add_cfg_edge(block_id, succ.index());
}
}
}
}
fn add_cfg_edge(&mut self, from: usize, to: usize) {
if !self.executable_edges.contains(&(from, to)) {
self.cfg_worklist.push_back((from, to));
}
}
fn is_block_executable(&self, block: usize) -> bool {
self.executable_blocks.contains_checked(block)
}
fn mark_block_executable(&mut self, block: usize) {
self.executable_blocks.insert_checked(block);
}
fn evaluate_phi(&self, phi: &PhiNode, block_id: usize) -> ValueRange {
let mut result = ValueRange::bottom();
let mut has_executable_operand = false;
for operand in phi.operands() {
let pred = operand.predecessor();
if !self.executable_edges.contains(&(pred, block_id)) {
continue;
}
has_executable_operand = true;
let op_range = self.get_range(operand.value());
result = result.join(&op_range);
if result.is_top() {
break;
}
}
if !has_executable_operand {
return ValueRange::top();
}
result
}
fn width_of(ssa: &SsaFunction<T>, var: SsaVarId) -> Option<u32> {
ssa.variable(var).and_then(|v| T::bit_width(v.var_type()))
}
fn wrap_to_width(range: ValueRange, width_bits: Option<u32>) -> ValueRange {
let Some(width) = width_bits else {
return ValueRange::top();
};
if width == 0 || width > 64 {
return ValueRange::top();
}
let Some(shift) = width.checked_sub(1) else {
return ValueRange::top();
};
let Some(magnitude) = 1i64.checked_shl(shift) else {
return ValueRange::top();
};
let Some(upper) = magnitude.checked_sub(1) else {
return ValueRange::top();
};
let lower = magnitude.saturating_neg();
match (range.min(), range.max()) {
(Some(min), Some(max)) if min >= lower && max <= upper => range,
_ => ValueRange::top(),
}
}
fn evaluate_instruction(&self, op: &SsaOp<T>, ssa: &SsaFunction<T>) -> ValueRange {
let dest_width = op.dest().and_then(|dest| Self::width_of(ssa, dest));
match op {
SsaOp::Const { value, .. } => {
if let Some(v) = value.as_i64() {
ValueRange::constant(v)
} else {
ValueRange::top()
}
}
SsaOp::Copy { src, .. } => self.get_range(*src),
SsaOp::Add { left, right, .. } => {
let l = self.get_range(*left);
let r = self.get_range(*right);
Self::wrap_to_width(l.add(&r), dest_width)
}
SsaOp::Sub { left, right, .. } => {
let l = self.get_range(*left);
let r = self.get_range(*right);
Self::wrap_to_width(l.sub(&r), dest_width)
}
SsaOp::Mul { left, right, .. } => {
let l = self.get_range(*left);
let r = self.get_range(*right);
Self::wrap_to_width(l.mul(&r), dest_width)
}
SsaOp::Neg { operand, .. } => {
let value = self.get_range(*operand);
let negated = match (value.min(), value.max()) {
(Some(min), Some(max)) => match (min.checked_neg(), max.checked_neg()) {
(Some(neg_min), Some(neg_max)) => {
ValueRange::bounded(neg_max.min(neg_min), neg_max.max(neg_min))
}
_ => ValueRange::top(),
},
_ => ValueRange::top(),
};
Self::wrap_to_width(negated, dest_width)
}
SsaOp::Shl { value, amount, .. } => {
let val_range = self.get_range(*value);
let amt_range = self.get_range(*amount);
if let Some(amt) = amt_range.as_constant()
&& (0..64).contains(&amt)
&& let Ok(shift) = u32::try_from(amt)
&& let (Some(min), Some(max)) = (val_range.min(), val_range.max())
&& let (Some(new_min), Some(new_max)) =
(min.checked_shl(shift), max.checked_shl(shift))
&& new_min.checked_shr(shift) == Some(min)
&& new_max.checked_shr(shift) == Some(max)
{
return Self::wrap_to_width(
ValueRange::bounded(new_min.min(new_max), new_min.max(new_max)),
dest_width,
);
}
ValueRange::top()
}
SsaOp::Or { left, right, .. } | SsaOp::Xor { left, right, .. } => {
let l = self.get_range(*left);
let r = self.get_range(*right);
match (l.max(), r.max()) {
(Some(l_max), Some(r_max))
if l.is_always_non_negative()
&& r.is_always_non_negative()
&& l_max >= 0
&& r_max >= 0 =>
{
let bound = l_max.max(r_max);
let bits = i64::BITS.saturating_sub(bound.leading_zeros());
match 1i64.checked_shl(bits).map(|v| v.saturating_sub(1)) {
Some(all_ones) if all_ones >= 0 => ValueRange::bounded(0, all_ones),
_ => ValueRange::non_negative(),
}
}
_ => ValueRange::top(),
}
}
SsaOp::And { left, right, .. } => {
let r = self.get_range(*right);
if let Some(mask) = r.as_constant() {
r.and_constant(mask)
} else {
let l = self.get_range(*left);
if let Some(mask) = l.as_constant() {
l.and_constant(mask)
} else {
ValueRange::top()
}
}
}
SsaOp::Shr {
value,
amount,
unsigned,
..
} => {
let val_range = self.get_range(*value);
let amt_range = self.get_range(*amount);
if let Some(amt) = amt_range.as_constant()
&& (0..64).contains(&amt)
&& *unsigned
&& val_range.is_always_non_negative()
&& let (Some(min), Some(max)) = (val_range.min(), val_range.max())
{
let new_min = min >> amt;
let new_max = max >> amt;
return ValueRange::bounded(new_min, new_max);
}
ValueRange::top()
}
SsaOp::Rem { left, right, .. } => {
let r = self.get_range(*right);
if let Some(n) = r.as_constant()
&& n > 0
{
let l = self.get_range(*left);
if l.is_always_non_negative() {
return ValueRange::bounded(0, n.saturating_sub(1));
}
}
ValueRange::top()
}
SsaOp::ArrayLength { .. } => ValueRange::non_negative(),
SsaOp::NewArr { .. }
| SsaOp::NewObj { .. }
| SsaOp::Box { .. }
| SsaOp::LoadToken { .. } => ValueRange::top(),
SsaOp::Ceq { .. } | SsaOp::Clt { .. } | SsaOp::Cgt { .. } => ValueRange::bounded(0, 1),
_ => ValueRange::top(),
}
}
fn get_range(&self, var: SsaVarId) -> ValueRange {
self.ranges.get(&var).cloned().unwrap_or_default()
}
fn update_range(&mut self, var: SsaVarId, new_range: &ValueRange) {
let old_range = self.ranges.get(&var).cloned().unwrap_or_default();
let revisions = self.update_counts.entry(var).or_insert(0);
*revisions = revisions.saturating_add(1);
let next_range = if *revisions > WIDEN_AFTER_REVISIONS {
old_range.widen(new_range)
} else {
new_range.clone()
};
if next_range != old_range {
self.ranges.insert(var, next_range);
self.ssa_worklist.push_back(var);
}
}
}
#[derive(Debug)]
struct RangeResult {
ranges: HashMap<SsaVarId, ValueRange>,
converged: bool,
}
impl RangeResult {
fn get_range(&self, var: SsaVarId) -> Option<&ValueRange> {
if !self.converged {
return None;
}
self.ranges.get(&var)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
events::EventLog,
ir::{
block::SsaBlock,
instruction::SsaInstruction,
phi::{PhiNode, PhiOperand},
value::ConstValue,
variable::{DefSite, SsaVarId, VariableOrigin},
},
testing::{MockTarget, MockType, mock_terminator_at, run_mock_pass_boundary},
};
#[test]
fn ranges_overlap_basics() {
let a = ValueRange::bounded(0, 5);
let b = ValueRange::bounded(10, 15);
assert!(!ranges_overlap(&a, &b));
let c = ValueRange::bounded(0, 10);
let d = ValueRange::bounded(5, 15);
assert!(ranges_overlap(&c, &d));
let e = ValueRange::bounded(5, 10);
assert!(ranges_overlap(&e, &e));
let top = ValueRange::top();
assert!(ranges_overlap(&top, &a));
let bottom = ValueRange::bottom();
assert!(!ranges_overlap(&bottom, &a));
}
fn make_result(entries: Vec<(SsaVarId, ValueRange)>) -> RangeResult {
RangeResult {
ranges: entries.into_iter().collect(),
converged: true,
}
}
#[test]
fn unconverged_result_yields_no_ranges() {
let var = SsaVarId::from_index(0);
let result = RangeResult {
ranges: [(var, ValueRange::constant(5))].into_iter().collect(),
converged: false,
};
assert_eq!(result.get_range(var), None);
}
#[test]
fn try_simplify_clt_always_true() {
let v0 = SsaVarId::from_index(0);
let v1 = SsaVarId::from_index(1);
let dest = SsaVarId::from_index(2);
let result = make_result(vec![
(v0, ValueRange::bounded(0, 5)),
(v1, ValueRange::bounded(10, 20)),
]);
let op: SsaOp<MockTarget> = SsaOp::Clt {
dest,
left: v0,
right: v1,
unsigned: false,
};
assert_eq!(try_simplify_comparison(&op, &result), Some((dest, true)));
}
#[test]
fn try_simplify_cgt_always_true() {
let v0 = SsaVarId::from_index(0);
let v1 = SsaVarId::from_index(1);
let dest = SsaVarId::from_index(2);
let result = make_result(vec![
(v0, ValueRange::bounded(100, 200)),
(v1, ValueRange::bounded(0, 50)),
]);
let op: SsaOp<MockTarget> = SsaOp::Cgt {
dest,
left: v0,
right: v1,
unsigned: false,
};
assert_eq!(try_simplify_comparison(&op, &result), Some((dest, true)));
}
#[test]
fn try_simplify_ceq_never() {
let v0 = SsaVarId::from_index(0);
let v1 = SsaVarId::from_index(1);
let dest = SsaVarId::from_index(2);
let result = make_result(vec![
(v0, ValueRange::bounded(0, 5)),
(v1, ValueRange::bounded(10, 20)),
]);
let op: SsaOp<MockTarget> = SsaOp::Ceq {
dest,
left: v0,
right: v1,
};
assert_eq!(try_simplify_comparison(&op, &result), Some((dest, false)));
}
#[test]
fn try_simplify_ceq_constants_equal() {
let v0 = SsaVarId::from_index(0);
let v1 = SsaVarId::from_index(1);
let dest = SsaVarId::from_index(2);
let result = make_result(vec![
(v0, ValueRange::constant(42)),
(v1, ValueRange::constant(42)),
]);
let op: SsaOp<MockTarget> = SsaOp::Ceq {
dest,
left: v0,
right: v1,
};
assert_eq!(try_simplify_comparison(&op, &result), Some((dest, true)));
}
fn instr(op: SsaOp<MockTarget>) -> SsaInstruction<MockTarget> {
SsaInstruction::synthetic(op)
}
fn local_at(
ssa: &mut SsaFunction<MockTarget>,
idx: u16,
block: usize,
instr: usize,
) -> SsaVarId {
ssa.create_variable(
VariableOrigin::Local(idx),
0,
DefSite::instruction(block, instr),
MockType::I32,
)
}
#[test]
fn range_propagation_through_copy() {
let mut ssa: SsaFunction<MockTarget> = SsaFunction::new(0, 2);
let v0 = local_at(&mut ssa, 0, 0, 0);
let v1 = local_at(&mut ssa, 1, 0, 1);
let mut block = SsaBlock::new(0);
block.add_instruction(instr(SsaOp::Const {
dest: v0,
value: ConstValue::I32(10),
}));
block.add_instruction(instr(SsaOp::Copy { dest: v1, src: v0 }));
block.add_instruction(instr(SsaOp::Return { value: Some(v1) }));
ssa.add_block(block);
ssa.recompute_uses();
let log: EventLog<MockTarget> = EventLog::new();
let method = 0u32;
let changed = run_mock_pass_boundary(&mut ssa, "copy range propagation", |ssa| {
run(ssa, &method, &log, 20)
});
assert!(
!changed,
"copy-only range propagation should not rewrite SSA"
);
}
#[test]
fn range_on_add_propagates() {
let mut ssa: SsaFunction<MockTarget> = SsaFunction::new(0, 3);
let v0 = local_at(&mut ssa, 0, 0, 0);
let v1 = local_at(&mut ssa, 1, 0, 1);
let v2 = local_at(&mut ssa, 2, 0, 2);
let mut block = SsaBlock::new(0);
block.add_instruction(instr(SsaOp::Const {
dest: v0,
value: ConstValue::I32(5),
}));
block.add_instruction(instr(SsaOp::Const {
dest: v1,
value: ConstValue::I32(3),
}));
block.add_instruction(instr(SsaOp::Add {
dest: v2,
left: v0,
right: v1,
flags: None,
}));
block.add_instruction(instr(SsaOp::Return { value: Some(v2) }));
ssa.add_block(block);
ssa.recompute_uses();
let log: EventLog<MockTarget> = EventLog::new();
let method = 0u32;
let changed = run_mock_pass_boundary(&mut ssa, "add range propagation", |ssa| {
run(ssa, &method, &log, 20)
});
assert!(
!changed,
"range propagation through add should not rewrite SSA"
);
}
#[test]
fn range_simplifies_branch_with_constant_condition() {
let mut ssa: SsaFunction<MockTarget> = SsaFunction::new(0, 2);
let v0 = local_at(&mut ssa, 0, 0, 0);
let mut b0 = SsaBlock::new(0);
b0.add_instruction(instr(SsaOp::Const {
dest: v0,
value: ConstValue::I32(1),
}));
b0.add_instruction(instr(SsaOp::Branch {
condition: v0,
true_target: 1,
false_target: 2,
}));
ssa.add_block(b0);
let mut b1 = SsaBlock::new(1);
b1.add_instruction(instr(SsaOp::Return { value: None }));
ssa.add_block(b1);
let mut b2 = SsaBlock::new(2);
b2.add_instruction(instr(SsaOp::Return { value: None }));
ssa.add_block(b2);
ssa.recompute_uses();
let log: EventLog<MockTarget> = EventLog::new();
let method = 0u32;
let changed = run_mock_pass_boundary(&mut ssa, "constant branch range folding", |ssa| {
run(ssa, &method, &log, 20)
});
assert!(changed, "constant branch should be simplified");
assert!(matches!(
mock_terminator_at(&ssa, 0),
SsaOp::Jump { target: 1 }
));
}
#[test]
fn and_with_a_negative_mask_does_not_prove_a_constant() {
let mut ssa: SsaFunction<MockTarget> = SsaFunction::new(0, 4);
let x = local_at(&mut ssa, 0, 0, 0);
let mask = local_at(&mut ssa, 1, 0, 1);
let masked = local_at(&mut ssa, 2, 0, 2);
let mut b0 = SsaBlock::new(0);
b0.add_instruction(instr(SsaOp::LoadArg {
dest: x,
arg_index: 0,
}));
b0.add_instruction(instr(SsaOp::Const {
dest: mask,
value: ConstValue::I32(-16),
}));
b0.add_instruction(instr(SsaOp::And {
dest: masked,
left: x,
right: mask,
flags: None,
}));
b0.add_instruction(instr(SsaOp::Branch {
condition: masked,
true_target: 1,
false_target: 2,
}));
ssa.add_block(b0);
let mut b1 = SsaBlock::new(1);
b1.add_instruction(instr(SsaOp::Return { value: None }));
ssa.add_block(b1);
let mut b2 = SsaBlock::new(2);
b2.add_instruction(instr(SsaOp::Return { value: None }));
ssa.add_block(b2);
ssa.recompute_uses();
let log: EventLog<MockTarget> = EventLog::new();
let method = 0u32;
run(&mut ssa, &method, &log, 20);
assert!(
matches!(mock_terminator_at(&ssa, 0), SsaOp::Branch { .. }),
"the branch on `x & -16` must survive; got {:?}",
mock_terminator_at(&ssa, 0)
);
}
#[test]
fn and_with_a_non_negative_mask_still_bounds_the_result() {
let range = ValueRange::top().and_constant(15);
assert_eq!(range.min(), Some(0));
assert_eq!(range.max(), Some(15));
let unconstrained = ValueRange::top().and_constant(-16);
assert!(
unconstrained.as_constant().is_none(),
"a negative mask must not yield a constant, got {unconstrained:?}"
);
}
#[test]
fn single_block_no_branch_no_changes() {
let mut ssa: SsaFunction<MockTarget> = SsaFunction::new(0, 1);
let v0 = SsaVarId::from_index(0);
ssa.create_variable(
VariableOrigin::Local(0),
0,
DefSite::instruction(0, 0),
MockType::I32,
);
let mut block = SsaBlock::new(0);
block.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: v0,
value: ConstValue::I32(42),
}));
block.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: Some(v0) }));
ssa.add_block(block);
ssa.recompute_uses();
let log: EventLog<MockTarget> = EventLog::new();
let method = 0u32;
let changed = run_mock_pass_boundary(&mut ssa, "single-block range pass", |ssa| {
run(ssa, &method, &log, 20)
});
assert!(!changed);
}
#[test]
fn comparison_folding_with_ranges() {
let mut ssa: SsaFunction<MockTarget> = SsaFunction::new(0, 3);
let v0 = local_at(&mut ssa, 0, 0, 0);
let v1 = local_at(&mut ssa, 1, 0, 1);
let v2 = local_at(&mut ssa, 2, 0, 2);
let mut block = SsaBlock::new(0);
block.add_instruction(instr(SsaOp::Const {
dest: v0,
value: ConstValue::I32(1),
}));
block.add_instruction(instr(SsaOp::Const {
dest: v1,
value: ConstValue::I32(100),
}));
block.add_instruction(instr(SsaOp::Clt {
dest: v2,
left: v0,
right: v1,
unsigned: false,
}));
block.add_instruction(instr(SsaOp::Return { value: Some(v2) }));
ssa.add_block(block);
ssa.recompute_uses();
let log: EventLog<MockTarget> = EventLog::new();
let method = 0u32;
let changed = run_mock_pass_boundary(&mut ssa, "comparison range folding", |ssa| {
run(ssa, &method, &log, 20)
});
assert!(changed, "range-known comparison should fold");
assert!(log.has(EventKind::ConstantFolded));
}
#[test]
fn range_propagation_does_not_crash_with_phi() {
let mut ssa: SsaFunction<MockTarget> = SsaFunction::new(0, 4);
let v0 = local_at(&mut ssa, 0, 0, 0);
let v1 = local_at(&mut ssa, 1, 1, 0);
let phi_var =
ssa.create_variable(VariableOrigin::Local(2), 0, DefSite::phi(2), MockType::I32);
let cond = local_at(&mut ssa, 3, 0, 1);
let mut b0 = SsaBlock::new(0);
b0.add_instruction(instr(SsaOp::Const {
dest: v0,
value: ConstValue::I32(0),
}));
b0.add_instruction(instr(SsaOp::Const {
dest: cond,
value: ConstValue::I32(1),
}));
b0.add_instruction(instr(SsaOp::Branch {
condition: cond,
true_target: 1,
false_target: 2,
}));
ssa.add_block(b0);
let mut b1 = SsaBlock::new(1);
b1.add_instruction(instr(SsaOp::Const {
dest: v1,
value: ConstValue::I32(10),
}));
b1.add_instruction(instr(SsaOp::Jump { target: 2 }));
ssa.add_block(b1);
let mut b2 = SsaBlock::new(2);
let mut phi = PhiNode::new(phi_var, VariableOrigin::Local(2));
phi.add_operand(PhiOperand::new(v0, 0));
phi.add_operand(PhiOperand::new(v1, 1));
b2.add_phi(phi);
b2.add_instruction(instr(SsaOp::Return {
value: Some(phi_var),
}));
ssa.add_block(b2);
ssa.recompute_uses();
let log: EventLog<MockTarget> = EventLog::new();
let method = 0u32;
let changed = run_mock_pass_boundary(&mut ssa, "phi range propagation", |ssa| {
run(ssa, &method, &log, 20)
});
assert!(changed, "constant branch before phi should simplify");
}
#[test]
fn out_of_range_branch_targets_do_not_panic() {
let mut ssa: SsaFunction<MockTarget> = SsaFunction::new(0, 1);
let cond = ssa.create_variable(
VariableOrigin::Argument(0),
0,
DefSite::entry(),
MockType::I32,
);
let mut b0 = SsaBlock::new(0);
b0.add_instruction(instr(SsaOp::Branch {
condition: cond,
true_target: 1,
false_target: 99,
}));
ssa.add_block(b0);
let mut b1 = SsaBlock::new(1);
b1.add_instruction(instr(SsaOp::Return { value: None }));
ssa.add_block(b1);
ssa.recompute_uses();
let log: EventLog<MockTarget> = EventLog::new();
let method = 0u32;
let changed = run_mock_pass_boundary(&mut ssa, "out-of-range branch target", |ssa| {
run(ssa, &method, &log, 20)
});
assert!(!changed, "unconstrained branch must not be simplified");
}
#[test]
fn ranges_overlap_edge_cases() {
let a = ValueRange::bounded(0, 5);
let b = ValueRange::bounded(6, 10);
assert!(
!ranges_overlap(&a, &b),
"adjacent ranges should not overlap"
);
let c = ValueRange::bounded(5, 5);
let d = ValueRange::bounded(5, 10);
assert!(
ranges_overlap(&c, &d),
"single point should overlap if same value"
);
let e = ValueRange::bounded(-10, -1);
let f = ValueRange::bounded(-5, 5);
assert!(
ranges_overlap(&e, &f),
"negative ranges should overlap correctly"
);
let g = ValueRange::bounded(-10, -5);
let h = ValueRange::bounded(-4, 5);
assert!(
!ranges_overlap(&g, &h),
"non-overlapping negatives should not overlap"
);
}
#[test]
fn try_simplify_clt_always_false() {
let v0 = SsaVarId::from_index(0);
let v1 = SsaVarId::from_index(1);
let dest = SsaVarId::from_index(2);
let result = make_result(vec![
(v0, ValueRange::bounded(10, 20)),
(v1, ValueRange::bounded(0, 5)),
]);
let op: SsaOp<MockTarget> = SsaOp::Clt {
dest,
left: v0,
right: v1,
unsigned: false,
};
assert_eq!(try_simplify_comparison(&op, &result), Some((dest, false)));
}
#[test]
fn try_simplify_cgt_always_false() {
let v0 = SsaVarId::from_index(0);
let v1 = SsaVarId::from_index(1);
let dest = SsaVarId::from_index(2);
let result = make_result(vec![
(v0, ValueRange::bounded(0, 5)),
(v1, ValueRange::bounded(10, 20)),
]);
let op: SsaOp<MockTarget> = SsaOp::Cgt {
dest,
left: v0,
right: v1,
unsigned: false,
};
assert_eq!(try_simplify_comparison(&op, &result), Some((dest, false)));
}
}