blob: bdc3047e5ba01e596fc110b0131827a515c39a8b [file] [log] [blame]
use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxIndexSet};
use rustc_middle::ty::RegionVid;
use rustc_mir_dataflow::points::PointIndex;
use super::{LiveLoans, LocalizedOutlivesConstraintSet};
use crate::BorrowSet;
use crate::constraints::OutlivesConstraint;
use crate::region_infer::values::LivenessValues;
use crate::type_check::Locations;
/// Compute loan reachability to approximately trace loan liveness throughout the CFG, by
/// traversing the full graph of constraints that combines:
/// - the localized constraints (the physical edges),
/// - with the constraints that hold at all points (the logical edges).
pub(super) fn compute_loan_liveness<'tcx>(
liveness: &LivenessValues,
outlives_constraints: impl Iterator<Item = OutlivesConstraint<'tcx>>,
borrow_set: &BorrowSet<'tcx>,
localized_outlives_constraints: &LocalizedOutlivesConstraintSet,
) -> LiveLoans {
let mut live_loans = LiveLoans::new(borrow_set.len());
// Create the full graph with the physical edges we've localized earlier, and the logical edges
// of constraints that hold at all points.
let logical_constraints =
outlives_constraints.filter(|c| matches!(c.locations, Locations::All(_)));
let graph = LocalizedConstraintGraph::new(&localized_outlives_constraints, logical_constraints);
let mut visited = FxHashSet::default();
let mut stack = Vec::new();
// Compute reachability per loan by traversing each loan's subgraph starting from where it is
// introduced.
for (loan_idx, loan) in borrow_set.iter_enumerated() {
visited.clear();
stack.clear();
let start_node = LocalizedNode {
region: loan.region,
point: liveness.point_from_location(loan.reserve_location),
};
stack.push(start_node);
while let Some(node) = stack.pop() {
if !visited.insert(node) {
continue;
}
// Record the loan as being live on entry to this point if it reaches a live region
// there.
//
// This is an approximation of liveness (which is the thing we want), in that we're
// using a single notion of reachability to represent what used to be _two_ different
// transitive closures. It didn't seem impactful when coming up with the single-graph
// and reachability through space (regions) + time (CFG) concepts, but in practice the
// combination of time-traveling with kills is more impactful than initially
// anticipated.
//
// Kills should prevent a loan from reaching its successor points in the CFG, but not
// while time-traveling: we're not actually at that CFG point, but looking for
// predecessor regions that contain the loan. One of the two TCs we had pushed the
// transitive subset edges to each point instead of having backward edges, and the
// problem didn't exist before. In the abstract, naive reachability is not enough to
// model this, we'd need a slightly different solution. For example, maybe with a
// two-step traversal:
// - at each point we first traverse the subgraph (and possibly time-travel) looking for
// exit nodes while ignoring kills,
// - and then when we're back at the current point, we continue normally.
//
// Another (less annoying) subtlety is that kills and the loan use-map are
// flow-insensitive. Kills can actually appear in places before a loan is introduced, or
// at a location that is actually unreachable in the CFG from the introduction point,
// and these can also be encountered during time-traveling.
//
// The simplest change that made sense to "fix" the issues above is taking into
// account kills that are:
// - reachable from the introduction point
// - encountered during forward traversal. Note that this is not transitive like the
// two-step traversal described above: only kills encountered on exit via a backward
// edge are ignored.
//
// This version of the analysis, however, is enough in practice to pass the tests that
// we care about and NLLs reject, without regressions on crater, and is an actionable
// subset of the full analysis. It also naturally points to areas of improvement that we
// wish to explore later, namely handling kills appropriately during traversal, instead
// of continuing traversal to all the reachable nodes.
//
// FIXME: analyze potential unsoundness, possibly in concert with a borrowck
// implementation in a-mir-formality, fuzzing, or manually crafting counter-examples.
if liveness.is_live_at(node.region, liveness.location_from_point(node.point)) {
live_loans.insert(node.point, loan_idx);
}
for succ in graph.outgoing_edges(node) {
stack.push(succ);
}
}
}
live_loans
}
/// The localized constraint graph indexes the physical and logical edges to compute a given node's
/// successors during traversal.
struct LocalizedConstraintGraph {
/// The actual, physical, edges we have recorded for a given node.
edges: FxHashMap<LocalizedNode, FxIndexSet<LocalizedNode>>,
/// The logical edges representing the outlives constraints that hold at all points in the CFG,
/// which we don't localize to avoid creating a lot of unnecessary edges in the graph. Some CFGs
/// can be big, and we don't need to create such a physical edge for every point in the CFG.
logical_edges: FxHashMap<RegionVid, FxIndexSet<RegionVid>>,
}
/// A node in the graph to be traversed, one of the two vertices of a localized outlives constraint.
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
struct LocalizedNode {
region: RegionVid,
point: PointIndex,
}
impl LocalizedConstraintGraph {
/// Traverses the constraints and returns the indexed graph of edges per node.
fn new<'tcx>(
constraints: &LocalizedOutlivesConstraintSet,
logical_constraints: impl Iterator<Item = OutlivesConstraint<'tcx>>,
) -> Self {
let mut edges: FxHashMap<_, FxIndexSet<_>> = FxHashMap::default();
for constraint in &constraints.outlives {
let source = LocalizedNode { region: constraint.source, point: constraint.from };
let target = LocalizedNode { region: constraint.target, point: constraint.to };
edges.entry(source).or_default().insert(target);
}
let mut logical_edges: FxHashMap<_, FxIndexSet<_>> = FxHashMap::default();
for constraint in logical_constraints {
logical_edges.entry(constraint.sup).or_default().insert(constraint.sub);
}
LocalizedConstraintGraph { edges, logical_edges }
}
/// Returns the outgoing edges of a given node, not its transitive closure.
fn outgoing_edges(&self, node: LocalizedNode) -> impl Iterator<Item = LocalizedNode> {
// The outgoing edges are:
// - the physical edges present at this node,
// - the materialized logical edges that exist virtually at all points for this node's
// region, localized at this point.
let physical_edges =
self.edges.get(&node).into_iter().flat_map(|targets| targets.iter().copied());
let materialized_edges =
self.logical_edges.get(&node.region).into_iter().flat_map(move |targets| {
targets
.iter()
.copied()
.map(move |target| LocalizedNode { point: node.point, region: target })
});
physical_edges.chain(materialized_edges)
}
}