use crate::analysis::path_analysis::graph::PathGraph;
use crate::compat::{FxHashMap, FxHashSet};
use rustc_hir::def_id::DefId;
use rustc_middle::{
mir::{
BasicBlock, BinOp, Body, Local, Operand, Place, ProjectionElem, Rvalue, StatementKind,
TerminatorKind,
},
ty::{TyCtxt, TyKind, TypingEnv},
};
use super::{
contract::{ContractExpr, NumericPredicate, Property, PropertyArg, PropertyKind, RelOp},
def_use::{RelevantPlaces, bind_callsite_roots},
helpers::{Checkpoint, CheckpointKind, CheckpointLocation},
report::CheckResult,
target::FunctionTarget,
};
pub(crate) const MAX_AUTO_REPEAT: usize = 16;
const DEFAULT_LOOP_CARRIED_BACKEDGES: usize = 3;
const DEFAULT_NUMERIC_WITNESS_ITERATION: usize = 4;
const MIN_DATAFLOW_REPEAT: usize = 2;
const BRANCH_SENSITIVE_BACKEDGES: usize = DEFAULT_LOOP_CARRIED_BACKEDGES;
#[derive(Clone, Copy, Debug)]
pub enum RepeatStrategy {
Auto,
Fixed(usize),
}
#[derive(Clone, Debug, Default)]
pub(crate) struct RepeatPlan<'tcx> {
pub repeat: usize,
#[allow(dead_code)]
pub dataflow_hints: Vec<DataflowDistanceHint>,
#[allow(dead_code)]
pub numeric_hints: Vec<NumericRangeHint>,
#[allow(dead_code)]
pub summary_findings: Vec<LoopSummaryFinding<'tcx>>,
}
impl<'tcx> RepeatPlan<'tcx> {
fn from_hints(
dataflow_hints: Vec<DataflowDistanceHint>,
numeric_hints: Vec<NumericRangeHint>,
summary_findings: Vec<LoopSummaryFinding<'tcx>>,
) -> Self {
let repeat = dataflow_hints
.iter()
.map(DataflowDistanceHint::calibrated_repeat)
.chain(
numeric_hints
.iter()
.map(NumericRangeHint::calibrated_repeat),
)
.max()
.unwrap_or(0)
.min(MAX_AUTO_REPEAT);
Self {
repeat,
dataflow_hints,
numeric_hints,
summary_findings,
}
}
}
#[derive(Clone, Debug)]
#[allow(dead_code)]
pub(crate) struct DataflowDistanceHint {
pub needed_backedges: usize,
pub scc_entry: usize,
pub checkpoint_block: usize,
pub property_kind: PropertyKind,
pub reason: String,
}
impl DataflowDistanceHint {
fn calibrated_repeat(&self) -> usize {
repeat_for_backedges(self.needed_backedges)
}
}
#[derive(Clone, Debug)]
#[allow(dead_code)]
pub(crate) struct NumericRangeHint {
pub witness_iteration: usize,
pub scc_entry: usize,
pub checkpoint_block: usize,
pub property_kind: PropertyKind,
pub reason: String,
}
impl NumericRangeHint {
fn calibrated_repeat(&self) -> usize {
repeat_for_witness_iteration(self.witness_iteration)
}
}
#[derive(Clone, Debug)]
#[allow(dead_code)]
pub(crate) struct LoopSummaryFinding<'tcx> {
pub checkpoint: CheckpointLocation,
pub checkpoint_index: usize,
pub property_index: usize,
pub property: Property<'tcx>,
pub result: CheckResult,
pub path_description: String,
pub callee_name: String,
pub diagnostic: String,
}
struct SafetySink<'target, 'tcx> {
#[allow(dead_code)]
checkpoint_index: usize,
checkpoint: &'target Checkpoint<'tcx>,
#[allow(dead_code)]
property_index: usize,
property: &'target Property<'tcx>,
roots: RelevantPlaces,
}
pub(crate) struct LoopSensitivityAnalyzer<'tcx> {
tcx: TyCtxt<'tcx>,
}
impl<'tcx> LoopSensitivityAnalyzer<'tcx> {
pub(crate) fn new(tcx: TyCtxt<'tcx>) -> Self {
Self { tcx }
}
pub(crate) fn analyze(&self, target: &FunctionTarget<'tcx>) -> RepeatPlan<'tcx> {
if !self.tcx.is_mir_available(target.def_id) {
return RepeatPlan::default();
}
let sinks = self.collect_sinks(target);
if sinks.is_empty() {
return RepeatPlan::default();
}
let mut graph = PathGraph::new(self.tcx, target.def_id);
graph.find_scc();
let body = self.tcx.optimized_mir(target.def_id);
let dependencies = LocalDependencyIndex::new(self.tcx, target.def_id);
let component_summaries: Vec<_> = loop_components(&graph)
.into_iter()
.map(|component| {
let local_summary = LoopLocalSummary::new(body, &component);
let numeric_summary =
LoopNumericSummary::new(self.tcx, target.def_id, body, &graph, &component);
(component, local_summary, numeric_summary)
})
.collect();
let dataflow_hints =
self.dataflow_distance_hints(&sinks, &graph, &dependencies, &component_summaries);
let numeric_hints =
self.numeric_range_hints(&sinks, &graph, &dependencies, &component_summaries);
RepeatPlan::from_hints(dataflow_hints, numeric_hints, Vec::new())
}
fn dataflow_distance_hints<'target>(
&self,
sinks: &[SafetySink<'target, 'tcx>],
graph: &PathGraph<'_>,
dependencies: &LocalDependencyIndex,
component_summaries: &[(LoopComponent, LoopLocalSummary, LoopNumericSummary)],
) -> Vec<DataflowDistanceHint> {
let mut hints = Vec::new();
for sink in sinks {
if matches!(sink.property.kind, PropertyKind::Unknown | PropertyKind::Or) {
continue;
}
let root_closure = dependencies.closure_from(&sink.roots.locals);
if root_closure.is_empty() {
continue;
}
for (component, local_summary, _) in component_summaries {
if !component_reaches_checkpoint(graph, component, sink.checkpoint.block) {
continue;
}
if root_closure
.iter()
.any(|local| local_summary.assigned_inside.contains(local))
{
let distance_backedges = estimate_dataflow_backedges(
dependencies,
&sink.roots.locals,
local_summary,
)
.unwrap_or(DEFAULT_LOOP_CARRIED_BACKEDGES);
let branch_backedges = estimate_branch_sensitive_backedges(
graph,
component,
dependencies,
&root_closure,
local_summary,
)
.unwrap_or(0);
let needed_backedges = distance_backedges.max(branch_backedges);
hints.push(DataflowDistanceHint {
needed_backedges,
scc_entry: component.entry,
checkpoint_block: sink.checkpoint.block.as_usize(),
property_kind: sink.property.kind.clone(),
reason: format!(
"safety sink depends on loop-carried state; estimated {needed_backedges} backedge(s)"
),
});
break;
}
}
}
hints
}
fn numeric_range_hints<'target>(
&self,
sinks: &[SafetySink<'target, 'tcx>],
graph: &PathGraph<'_>,
dependencies: &LocalDependencyIndex,
component_summaries: &[(LoopComponent, LoopLocalSummary, LoopNumericSummary)],
) -> Vec<NumericRangeHint> {
let mut hints = Vec::new();
for sink in sinks {
if !is_numeric_range_property(&sink.property.kind) {
continue;
}
let root_closure = dependencies.closure_from(&sink.roots.locals);
if root_closure.is_empty() {
continue;
}
for (component, local_summary, numeric_summary) in component_summaries {
if !component_reaches_checkpoint(graph, component, sink.checkpoint.block) {
continue;
}
if !root_closure
.iter()
.any(|local| local_summary.assigned_inside.contains(local))
{
continue;
}
let witness_iteration = match sink.property.kind {
PropertyKind::ValidNum => {
estimate_valid_num_witness(sink.property, &root_closure, numeric_summary)
}
PropertyKind::InBound => {
estimate_inbound_witness(&root_closure, numeric_summary)
}
_ => None,
};
if let Some(witness_iteration) = witness_iteration {
hints.push(NumericRangeHint {
witness_iteration,
scc_entry: component.entry,
checkpoint_block: sink.checkpoint.block.as_usize(),
property_kind: sink.property.kind.clone(),
reason: format!(
"numeric/index sink depends on induction state; first witness iteration {witness_iteration}"
),
});
break;
}
}
}
hints
}
fn collect_sinks<'target>(
&self,
target: &'target FunctionTarget<'tcx>,
) -> Vec<SafetySink<'target, 'tcx>> {
let mut sinks = Vec::new();
let mut checkpoint_index = 0usize;
for checkpoint in all_checkpoints(target) {
let properties = properties_for_checkpoint(target, checkpoint);
if properties.is_empty() {
continue;
}
for (property_index, property) in properties.iter().enumerate() {
let mut roots = RelevantPlaces::from_property(property);
bind_callsite_roots(self.tcx, &mut roots, checkpoint);
if roots.locals.is_empty() {
continue;
}
sinks.push(SafetySink {
checkpoint_index,
checkpoint,
property_index,
property,
roots,
});
}
checkpoint_index += 1;
}
sinks
}
}
fn all_checkpoints<'target, 'tcx>(
target: &'target FunctionTarget<'tcx>,
) -> Vec<&'target Checkpoint<'tcx>> {
target
.checkpoints
.iter()
.chain(
target
.raw_ptr_deref_checks
.iter()
.map(|(checkpoint, _)| checkpoint),
)
.chain(
target
.static_mut_checks
.iter()
.map(|(checkpoint, _)| checkpoint),
)
.collect()
}
fn properties_for_checkpoint<'target, 'tcx>(
target: &'target FunctionTarget<'tcx>,
checkpoint: &'target Checkpoint<'tcx>,
) -> &'target [Property<'tcx>] {
let loc = checkpoint.location();
match checkpoint.kind {
CheckpointKind::RawPtrDeref => target
.raw_ptr_deref_checks
.iter()
.find(|(candidate, _)| candidate.location() == loc)
.map(|(_, properties)| properties.as_slice())
.unwrap_or(&[]),
CheckpointKind::StaticMutAccess => target
.static_mut_checks
.iter()
.find(|(candidate, _)| candidate.location() == loc)
.map(|(_, properties)| properties.as_slice())
.unwrap_or(&[]),
CheckpointKind::UnsafeCall => checkpoint
.callee
.and_then(|callee| target.callee_requires.get(&callee))
.map(Vec::as_slice)
.unwrap_or(&[]),
}
}
#[derive(Clone, Debug)]
struct LoopComponent {
entry: usize,
blocks: FxHashSet<usize>,
}
fn loop_components(graph: &PathGraph<'_>) -> Vec<LoopComponent> {
let mut components = Vec::new();
for block in &graph.cfg.blocks {
let scc = &block.scc;
if block.index != scc.enter || scc.nodes.is_empty() {
continue;
}
let mut blocks = scc.nodes.clone();
blocks.insert(scc.enter);
components.push(LoopComponent {
entry: scc.enter,
blocks,
});
}
components
}
fn component_reaches_checkpoint(
graph: &PathGraph<'_>,
component: &LoopComponent,
checkpoint: BasicBlock,
) -> bool {
let target = checkpoint.as_usize();
if component.blocks.contains(&target) {
return true;
}
let mut stack: Vec<usize> = component.blocks.iter().copied().collect();
let mut seen = FxHashSet::default();
while let Some(block) = stack.pop() {
if block == target {
return true;
}
if !seen.insert(block) {
continue;
}
if block >= graph.cfg.blocks.len() {
continue;
}
for next in &graph.cfg.block(block).next {
stack.push(*next);
}
}
false
}
struct LoopLocalSummary {
assigned_inside: FxHashSet<Local>,
state_locals: FxHashSet<Local>,
}
impl LoopLocalSummary {
fn new(body: &Body<'_>, component: &LoopComponent) -> Self {
let mut assigned_inside = FxHashSet::default();
let mut assigned_outside = FxHashSet::default();
for (block, data) in body.basic_blocks.iter_enumerated() {
let assigned = collect_assigned_locals(data);
if component.blocks.contains(&block.as_usize()) {
assigned_inside.extend(assigned);
} else {
assigned_outside.extend(assigned);
}
}
let mut state_locals = FxHashSet::default();
for local in &assigned_inside {
if assigned_outside.contains(local) || local_is_argument(*local, body) {
state_locals.insert(*local);
}
}
Self {
assigned_inside,
state_locals,
}
}
}
#[derive(Clone, Copy, Debug)]
enum NumericTerm {
Local(Local),
Const(i128),
}
#[derive(Clone, Copy, Debug)]
struct ComparisonFact {
op: BinOp,
lhs: NumericTerm,
rhs: NumericTerm,
}
struct LoopNumericSummary {
initial_constants: FxHashMap<Local, i128>,
steps: FxHashMap<Local, i128>,
guard_upper_bounds: FxHashMap<Local, NumericTerm>,
entry_lower_bounds: FxHashMap<Local, i128>,
}
impl LoopNumericSummary {
fn new<'tcx>(
tcx: TyCtxt<'tcx>,
def_id: DefId,
body: &Body<'tcx>,
graph: &PathGraph<'_>,
component: &LoopComponent,
) -> Self {
let mut initial_constants = FxHashMap::default();
let mut tuple_steps: FxHashMap<Local, (Local, i128)> = FxHashMap::default();
let mut steps = FxHashMap::default();
let mut copy_sources = FxHashMap::default();
let mut comparisons = FxHashMap::default();
for (block, data) in body.basic_blocks.iter_enumerated() {
let in_component = component.blocks.contains(&block.as_usize());
for statement in &data.statements {
let StatementKind::Assign(assign) = &statement.kind else {
continue;
};
let (place, rvalue) = &**assign;
if place_is_indirect_write(place) {
continue;
}
if let Some(source) = plain_copy_source(rvalue) {
copy_sources.insert(place.local, source);
}
if let Some(comparison) = comparison_fact(tcx, def_id, rvalue) {
comparisons.insert(place.local, comparison);
}
if !in_component {
if let Some(value) = rvalue_const_i128(tcx, def_id, rvalue) {
initial_constants.insert(place.local, value);
}
continue;
}
if let Some((source, step)) = increment_source_and_step(tcx, def_id, rvalue) {
if source == place.local {
steps.insert(place.local, step);
} else {
tuple_steps.insert(place.local, (source, step));
}
}
}
}
for block in &component.blocks {
let data = &body.basic_blocks[BasicBlock::from(*block)];
for statement in &data.statements {
let StatementKind::Assign(assign) = &statement.kind else {
continue;
};
let (place, rvalue) = &**assign;
if place_is_indirect_write(place) {
continue;
}
let Some(source_temp) = rvalue_projection_source(rvalue, 0) else {
continue;
};
let Some((source, step)) = tuple_steps.get(&source_temp).copied() else {
continue;
};
if source == place.local {
steps.insert(place.local, step);
}
}
}
let guard_upper_bounds =
collect_loop_guard_upper_bounds(&steps, ©_sources, &comparisons);
let entry_lower_bounds =
collect_entry_lower_bounds(graph, component, ©_sources, &comparisons);
Self {
initial_constants,
steps,
guard_upper_bounds,
entry_lower_bounds,
}
}
}
fn collect_assigned_locals(data: &rustc_middle::mir::BasicBlockData<'_>) -> FxHashSet<Local> {
let mut locals = FxHashSet::default();
for statement in &data.statements {
let StatementKind::Assign(assign) = &statement.kind else {
continue;
};
let (place, _) = &**assign;
if !place_is_indirect_write(place) {
locals.insert(place.local);
}
}
if let TerminatorKind::Call { destination, .. } = &data.terminator().kind {
locals.insert(destination.local);
}
locals
}
fn collect_loop_guard_upper_bounds(
steps: &FxHashMap<Local, i128>,
copy_sources: &FxHashMap<Local, Local>,
comparisons: &FxHashMap<Local, ComparisonFact>,
) -> FxHashMap<Local, NumericTerm> {
let mut bounds = FxHashMap::default();
for comparison in comparisons.values() {
let lhs = resolve_numeric_term(comparison.lhs, copy_sources);
let rhs = resolve_numeric_term(comparison.rhs, copy_sources);
match (comparison.op, lhs, rhs) {
(BinOp::Lt | BinOp::Le, NumericTerm::Local(local), bound)
if steps.contains_key(&local) =>
{
bounds.insert(local, bound);
}
(BinOp::Gt | BinOp::Ge, bound, NumericTerm::Local(local))
if steps.contains_key(&local) =>
{
bounds.insert(local, bound);
}
_ => {}
}
}
bounds
}
fn collect_entry_lower_bounds(
graph: &PathGraph<'_>,
component: &LoopComponent,
copy_sources: &FxHashMap<Local, Local>,
comparisons: &FxHashMap<Local, ComparisonFact>,
) -> FxHashMap<Local, i128> {
let mut bounds: FxHashMap<Local, i128> = FxHashMap::default();
for block in &graph.cfg.blocks {
if component.blocks.contains(&block.index) {
continue;
}
let Some(terminator) = graph.cfg.terminator(block.index) else {
continue;
};
let TerminatorKind::SwitchInt { discr, targets } = &terminator.kind else {
continue;
};
let Some(discr_local) = operand_plain_local(discr) else {
continue;
};
let discr_local = resolve_local_copy(discr_local, copy_sources);
let Some(comparison) = comparisons.get(&discr_local).copied() else {
continue;
};
for successor in switch_successors(targets) {
if !block_reaches_component(graph, successor.block, component) {
continue;
}
let Some((local, lower_bound)) =
lower_bound_from_branch(comparison, successor.value, copy_sources)
else {
continue;
};
bounds
.entry(local)
.and_modify(|existing| *existing = (*existing).max(lower_bound))
.or_insert(lower_bound);
}
}
bounds
}
#[derive(Clone, Copy)]
struct SwitchSuccessor {
block: usize,
value: u128,
}
fn switch_successors(targets: &rustc_middle::mir::SwitchTargets) -> Vec<SwitchSuccessor> {
let explicit: Vec<_> = targets.iter().collect();
let mut successors: Vec<_> = explicit
.iter()
.map(|(value, target)| SwitchSuccessor {
block: target.as_usize(),
value: *value,
})
.collect();
let otherwise_value = if explicit.iter().any(|(value, _)| *value == 0) {
1
} else {
0
};
successors.push(SwitchSuccessor {
block: targets.otherwise().as_usize(),
value: otherwise_value,
});
successors
}
fn block_reaches_component(graph: &PathGraph<'_>, start: usize, component: &LoopComponent) -> bool {
let mut stack = vec![start];
let mut seen = FxHashSet::default();
while let Some(block) = stack.pop() {
if component.blocks.contains(&block) {
return true;
}
if !seen.insert(block) || block >= graph.cfg.blocks.len() {
continue;
}
for next in &graph.cfg.block(block).next {
stack.push(*next);
}
}
false
}
fn estimate_branch_sensitive_backedges(
graph: &PathGraph<'_>,
component: &LoopComponent,
dependencies: &LocalDependencyIndex,
root_closure: &FxHashSet<Local>,
local_summary: &LoopLocalSummary,
) -> Option<usize> {
if !component_has_internal_branch(graph, component) {
return None;
}
let sink_state_reassigned = root_closure
.iter()
.any(|local| local_summary.state_locals.contains(local));
let multi_source_assignment = root_closure.iter().any(|local| {
local_summary.assigned_inside.contains(local)
&& dependencies
.sources_by_dest
.get(local)
.is_some_and(|sources| sources.len() > 1)
});
(sink_state_reassigned || multi_source_assignment).then_some(BRANCH_SENSITIVE_BACKEDGES)
}
fn component_has_internal_branch(graph: &PathGraph<'_>, component: &LoopComponent) -> bool {
component.blocks.iter().any(|block| {
graph
.cfg
.block(*block)
.next
.iter()
.filter(|next| component.blocks.contains(next))
.take(2)
.count()
>= 2
})
}
fn lower_bound_from_branch(
comparison: ComparisonFact,
branch_value: u128,
copy_sources: &FxHashMap<Local, Local>,
) -> Option<(Local, i128)> {
let is_true = branch_value != 0;
let lhs = resolve_numeric_term(comparison.lhs, copy_sources);
let rhs = resolve_numeric_term(comparison.rhs, copy_sources);
match (is_true, comparison.op, lhs, rhs) {
(false, BinOp::Lt, NumericTerm::Local(local), NumericTerm::Const(bound)) => {
Some((local, bound))
}
(false, BinOp::Le, NumericTerm::Local(local), NumericTerm::Const(bound)) => {
Some((local, bound.checked_add(1)?))
}
(false, BinOp::Gt, NumericTerm::Const(bound), NumericTerm::Local(local)) => {
Some((local, bound))
}
(false, BinOp::Ge, NumericTerm::Const(bound), NumericTerm::Local(local)) => {
Some((local, bound.checked_add(1)?))
}
(true, BinOp::Ge, NumericTerm::Local(local), NumericTerm::Const(bound)) => {
Some((local, bound))
}
(true, BinOp::Gt, NumericTerm::Local(local), NumericTerm::Const(bound)) => {
Some((local, bound.checked_add(1)?))
}
(true, BinOp::Le, NumericTerm::Const(bound), NumericTerm::Local(local)) => {
Some((local, bound))
}
(true, BinOp::Lt, NumericTerm::Const(bound), NumericTerm::Local(local)) => {
Some((local, bound.checked_add(1)?))
}
_ => None,
}
}
fn repeat_for_backedges(needed_backedges: usize) -> usize {
if needed_backedges == 0 {
0
} else {
needed_backedges
.saturating_sub(1)
.max(MIN_DATAFLOW_REPEAT)
.min(MAX_AUTO_REPEAT)
}
}
fn repeat_for_witness_iteration(witness_iteration: usize) -> usize {
witness_iteration.saturating_sub(2).min(MAX_AUTO_REPEAT)
}
fn estimate_dataflow_backedges(
dependencies: &LocalDependencyIndex,
roots: &FxHashSet<Local>,
local_summary: &LoopLocalSummary,
) -> Option<usize> {
let mut best_state_distance = 0usize;
for root in roots {
let mut visited = FxHashSet::default();
best_state_distance = best_state_distance.max(max_state_distance_from(
dependencies,
*root,
local_summary,
0,
&mut visited,
));
}
if best_state_distance == 0 {
None
} else {
Some(best_state_distance)
}
}
fn max_state_distance_from(
dependencies: &LocalDependencyIndex,
local: Local,
local_summary: &LoopLocalSummary,
distance: usize,
visited: &mut FxHashSet<Local>,
) -> usize {
if !visited.insert(local) {
return distance;
}
let mut best = distance;
if let Some(sources) = dependencies.sources_by_dest.get(&local) {
for source in sources {
let next_distance = distance + usize::from(local_summary.state_locals.contains(source));
let mut branch_visited = visited.clone();
best = best.max(max_state_distance_from(
dependencies,
*source,
local_summary,
next_distance,
&mut branch_visited,
));
}
}
best
}
fn is_numeric_range_property(kind: &PropertyKind) -> bool {
matches!(kind, PropertyKind::ValidNum | PropertyKind::InBound)
}
fn estimate_valid_num_witness(
property: &Property<'_>,
root_closure: &FxHashSet<Local>,
numeric_summary: &LoopNumericSummary,
) -> Option<usize> {
let violation_value = valid_num_violation_value(property)?;
root_closure
.iter()
.filter_map(|local| {
let init = numeric_summary.initial_constants.get(local).copied()?;
let step = numeric_summary.steps.get(local).copied()?;
witness_iteration_for_threshold(init, step, violation_value)
})
.min()
}
fn estimate_inbound_witness(
root_closure: &FxHashSet<Local>,
numeric_summary: &LoopNumericSummary,
) -> Option<usize> {
let mut fallback = false;
let mut best = None;
for local in root_closure {
let Some(init) = numeric_summary.initial_constants.get(local).copied() else {
continue;
};
let Some(step) = numeric_summary.steps.get(local).copied() else {
continue;
};
if step == 0 {
continue;
}
fallback = true;
let Some(guard_bound) = numeric_summary.guard_upper_bounds.get(local).copied() else {
continue;
};
let Some(bound_lower) = numeric_term_lower_bound(guard_bound, numeric_summary) else {
continue;
};
let Some(witness) = witness_iteration_for_threshold(init, step, bound_lower) else {
continue;
};
best = Some(best.map_or(witness, |current: usize| current.min(witness)));
}
best.or_else(|| fallback.then_some(DEFAULT_NUMERIC_WITNESS_ITERATION))
}
fn valid_num_violation_value(property: &Property<'_>) -> Option<i128> {
if !matches!(property.kind, PropertyKind::ValidNum) {
return None;
}
let Some(PropertyArg::Predicates(predicates)) = property.args.first() else {
return None;
};
predicates
.iter()
.filter_map(simple_upper_bound_violation_value)
.min()
}
fn simple_upper_bound_violation_value(predicate: &NumericPredicate<'_>) -> Option<i128> {
match (&predicate.lhs, predicate.op, &predicate.rhs) {
(lhs, RelOp::Lt, rhs) if expr_is_place(lhs) => expr_const_i128(rhs),
(lhs, RelOp::Le, rhs) if expr_is_place(lhs) => expr_const_i128(rhs)?.checked_add(1),
(lhs, RelOp::Gt, rhs) if expr_is_place(rhs) => expr_const_i128(lhs),
(lhs, RelOp::Ge, rhs) if expr_is_place(rhs) => expr_const_i128(lhs)?.checked_add(1),
_ => None,
}
}
fn expr_is_place(expr: &ContractExpr<'_>) -> bool {
matches!(expr, ContractExpr::Place(_))
}
fn expr_const_i128(expr: &ContractExpr<'_>) -> Option<i128> {
match expr {
ContractExpr::Const(value) if *value <= i128::MAX as u128 => Some(*value as i128),
_ => None,
}
}
fn numeric_term_lower_bound(term: NumericTerm, summary: &LoopNumericSummary) -> Option<i128> {
match term {
NumericTerm::Const(value) => Some(value),
NumericTerm::Local(local) => summary.entry_lower_bounds.get(&local).copied(),
}
}
fn witness_iteration_for_threshold(init: i128, step: i128, violation_value: i128) -> Option<usize> {
if step <= 0 {
return None;
}
if init >= violation_value {
return Some(0);
}
let delta = violation_value.checked_sub(init)?;
usize::try_from(ceil_div_i128(delta, step)).ok()
}
fn ceil_div_i128(lhs: i128, rhs: i128) -> i128 {
debug_assert!(lhs >= 0);
debug_assert!(rhs > 0);
(lhs + rhs - 1) / rhs
}
fn local_is_argument(local: Local, body: &Body<'_>) -> bool {
let index = local.as_usize();
index > 0 && index <= body.arg_count
}
fn rvalue_const_i128<'tcx>(
tcx: TyCtxt<'tcx>,
def_id: DefId,
rvalue: &Rvalue<'tcx>,
) -> Option<i128> {
match rvalue {
Rvalue::Use(operand, ..) | Rvalue::Cast(_, operand, _) => {
operand_const_i128(tcx, def_id, operand)
}
_ => None,
}
}
fn plain_copy_source(rvalue: &Rvalue<'_>) -> Option<Local> {
let Rvalue::Use(Operand::Copy(place) | Operand::Move(place), ..) = rvalue else {
return None;
};
place.projection.is_empty().then_some(place.local)
}
fn comparison_fact<'tcx>(
tcx: TyCtxt<'tcx>,
def_id: DefId,
rvalue: &Rvalue<'tcx>,
) -> Option<ComparisonFact> {
let Rvalue::BinaryOp(op, operands) = rvalue else {
return None;
};
if !matches!(
op,
BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge | BinOp::Eq | BinOp::Ne
) {
return None;
}
Some(ComparisonFact {
op: *op,
lhs: numeric_term_from_operand(tcx, def_id, &operands.0)?,
rhs: numeric_term_from_operand(tcx, def_id, &operands.1)?,
})
}
fn numeric_term_from_operand<'tcx>(
tcx: TyCtxt<'tcx>,
def_id: DefId,
operand: &Operand<'tcx>,
) -> Option<NumericTerm> {
operand_plain_local(operand)
.map(NumericTerm::Local)
.or_else(|| operand_const_i128(tcx, def_id, operand).map(NumericTerm::Const))
}
fn resolve_local_copy(local: Local, copy_sources: &FxHashMap<Local, Local>) -> Local {
let mut current = local;
let mut seen = FxHashSet::default();
while seen.insert(current) {
let Some(next) = copy_sources.get(¤t).copied() else {
break;
};
current = next;
}
current
}
fn resolve_numeric_term(term: NumericTerm, copy_sources: &FxHashMap<Local, Local>) -> NumericTerm {
match term {
NumericTerm::Local(local) => NumericTerm::Local(resolve_local_copy(local, copy_sources)),
NumericTerm::Const(value) => NumericTerm::Const(value),
}
}
fn increment_source_and_step<'tcx>(
tcx: TyCtxt<'tcx>,
def_id: DefId,
rvalue: &Rvalue<'tcx>,
) -> Option<(Local, i128)> {
let Rvalue::BinaryOp(op, operands) = rvalue else {
return None;
};
let lhs_local = operand_plain_local(&operands.0);
let rhs_local = operand_plain_local(&operands.1);
let lhs_const = operand_const_i128(tcx, def_id, &operands.0);
let rhs_const = operand_const_i128(tcx, def_id, &operands.1);
match op {
BinOp::Add | BinOp::AddWithOverflow | BinOp::AddUnchecked => match (lhs_local, rhs_local) {
(Some(local), None) => Some((local, rhs_const?)),
(None, Some(local)) => Some((local, lhs_const?)),
_ => None,
},
BinOp::Sub | BinOp::SubWithOverflow | BinOp::SubUnchecked => match (lhs_local, rhs_const) {
(Some(local), Some(value)) => Some((local, -value)),
_ => None,
},
_ => None,
}
}
fn rvalue_projection_source(rvalue: &Rvalue<'_>, field_index: usize) -> Option<Local> {
let Rvalue::Use(Operand::Copy(place) | Operand::Move(place), ..) = rvalue else {
return None;
};
(first_field_projection(place) == Some(field_index)).then_some(place.local)
}
fn operand_plain_local(operand: &Operand<'_>) -> Option<Local> {
match operand {
Operand::Copy(place) | Operand::Move(place) if place.projection.is_empty() => {
Some(place.local)
}
Operand::Copy(_) | Operand::Move(_) | Operand::Constant(_) => None,
#[cfg(rapx_rustc_ge_196)]
Operand::RuntimeChecks(_) => None,
}
}
fn operand_const_i128<'tcx>(
tcx: TyCtxt<'tcx>,
def_id: DefId,
operand: &Operand<'tcx>,
) -> Option<i128> {
let Operand::Constant(constant) = operand else {
return None;
};
let typing_env = TypingEnv::post_analysis(tcx, def_id);
match constant.const_.ty().kind() {
TyKind::Bool => constant
.const_
.try_eval_bool(tcx, typing_env)
.map(|value| if value { 1 } else { 0 }),
TyKind::Int(_) | TyKind::Uint(_) => constant
.const_
.try_eval_bits(tcx, typing_env)
.and_then(|bits| {
if bits <= i128::MAX as u128 {
Some(bits as i128)
} else {
None
}
}),
_ => None,
}
}
fn first_field_projection(place: &Place<'_>) -> Option<usize> {
for projection in place.projection.iter() {
if let ProjectionElem::Field(field, _) = projection {
return Some(field.as_usize());
}
}
None
}
struct LocalDependencyIndex {
sources_by_dest: FxHashMap<Local, FxHashSet<Local>>,
}
impl LocalDependencyIndex {
fn new(tcx: TyCtxt<'_>, def_id: DefId) -> Self {
let body = tcx.optimized_mir(def_id);
let mut sources_by_dest: FxHashMap<Local, FxHashSet<Local>> = FxHashMap::default();
for data in body.basic_blocks.iter() {
for statement in &data.statements {
let StatementKind::Assign(assign) = &statement.kind else {
continue;
};
let (place, rvalue) = &**assign;
if place_is_indirect_write(place) {
continue;
}
let mut sources = FxHashSet::default();
collect_rvalue_sources(rvalue, &mut sources);
if !sources.is_empty() {
sources_by_dest
.entry(place.local)
.or_default()
.extend(sources);
}
}
if let TerminatorKind::Call {
args, destination, ..
} = &data.terminator().kind
{
let mut sources = FxHashSet::default();
for arg in args {
collect_operand_sources(&arg.node, &mut sources);
}
if !sources.is_empty() {
sources_by_dest
.entry(destination.local)
.or_default()
.extend(sources);
}
}
}
Self { sources_by_dest }
}
fn closure_from(&self, roots: &FxHashSet<Local>) -> FxHashSet<Local> {
let mut closure = FxHashSet::default();
let mut stack: Vec<Local> = roots.iter().copied().collect();
while let Some(local) = stack.pop() {
if !closure.insert(local) {
continue;
}
if let Some(sources) = self.sources_by_dest.get(&local) {
for source in sources {
stack.push(*source);
}
}
}
closure
}
}
fn collect_rvalue_sources(rvalue: &Rvalue<'_>, out: &mut FxHashSet<Local>) {
match rvalue {
Rvalue::Use(operand, ..) => collect_operand_sources(operand, out),
Rvalue::Repeat(operand, _) => collect_operand_sources(operand, out),
Rvalue::Ref(_, _, place) | Rvalue::RawPtr(_, place) | Rvalue::Discriminant(place) => {
out.insert(place.local);
}
Rvalue::Cast(_, operand, _) | Rvalue::UnaryOp(_, operand) => {
collect_operand_sources(operand, out);
}
Rvalue::BinaryOp(_, operands) => {
collect_operand_sources(&operands.0, out);
collect_operand_sources(&operands.1, out);
}
Rvalue::Aggregate(_, operands) => {
for operand in operands {
collect_operand_sources(operand, out);
}
}
Rvalue::CopyForDeref(place) => {
out.insert(place.local);
}
#[cfg(not(rapx_rustc_ge_196))]
Rvalue::ShallowInitBox(operand, _) => collect_operand_sources(operand, out),
Rvalue::ThreadLocalRef(_) => {}
#[cfg(not(rapx_rustc_ge_196))]
Rvalue::NullaryOp(..) => {}
_ => {}
}
}
fn collect_operand_sources(operand: &Operand<'_>, out: &mut FxHashSet<Local>) {
match operand {
Operand::Copy(place) | Operand::Move(place) => {
out.insert(place.local);
}
Operand::Constant(_) => {}
#[cfg(rapx_rustc_ge_196)]
Operand::RuntimeChecks(_) => {}
}
}
fn place_is_indirect_write(place: &Place<'_>) -> bool {
place
.projection
.iter()
.any(|projection| matches!(projection, ProjectionElem::Deref))
}