rustc_borrowck/polonius/
loan_liveness.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxIndexSet};
4use rustc_middle::mir::visit::Visitor;
5use rustc_middle::mir::{
6    Body, Local, Location, Place, Rvalue, Statement, StatementKind, Terminator, TerminatorKind,
7};
8use rustc_middle::ty::{RegionVid, TyCtxt};
9use rustc_mir_dataflow::points::PointIndex;
10
11use super::{LiveLoans, LocalizedOutlivesConstraintSet};
12use crate::constraints::OutlivesConstraint;
13use crate::dataflow::BorrowIndex;
14use crate::region_infer::values::LivenessValues;
15use crate::type_check::Locations;
16use crate::{BorrowSet, PlaceConflictBias, places_conflict};
17
18/// Compute loan reachability, stop at kills, and trace loan liveness throughout the CFG, by
19/// traversing the full graph of constraints that combines:
20/// - the localized constraints (the physical edges),
21/// - with the constraints that hold at all points (the logical edges).
22pub(super) fn compute_loan_liveness<'tcx>(
23    tcx: TyCtxt<'tcx>,
24    body: &Body<'tcx>,
25    liveness: &LivenessValues,
26    outlives_constraints: impl Iterator<Item = OutlivesConstraint<'tcx>>,
27    borrow_set: &BorrowSet<'tcx>,
28    localized_outlives_constraints: &LocalizedOutlivesConstraintSet,
29) -> LiveLoans {
30    let mut live_loans = LiveLoans::new(borrow_set.len());
31
32    // FIXME: it may be preferable for kills to be encoded in the edges themselves, to simplify and
33    // likely make traversal (and constraint generation) more efficient. We also display kills on
34    // edges when visualizing the constraint graph anyways.
35    let kills = collect_kills(body, tcx, borrow_set);
36
37    // Create the full graph with the physical edges we've localized earlier, and the logical edges
38    // of constraints that hold at all points.
39    let logical_constraints =
40        outlives_constraints.filter(|c| matches!(c.locations, Locations::All(_)));
41    let graph = LocalizedConstraintGraph::new(&localized_outlives_constraints, logical_constraints);
42    let mut visited = FxHashSet::default();
43    let mut stack = Vec::new();
44
45    // Compute reachability per loan by traversing each loan's subgraph starting from where it is
46    // introduced.
47    for (loan_idx, loan) in borrow_set.iter_enumerated() {
48        visited.clear();
49        stack.clear();
50
51        let start_node = LocalizedNode {
52            region: loan.region,
53            point: liveness.point_from_location(loan.reserve_location),
54        };
55        stack.push(start_node);
56
57        while let Some(node) = stack.pop() {
58            if !visited.insert(node) {
59                continue;
60            }
61
62            // Record the loan as being live on entry to this point.
63            live_loans.insert(node.point, loan_idx);
64
65            // Here, we have a conundrum. There's currently a weakness in our theory, in that
66            // we're using a single notion of reachability to represent what used to be _two_
67            // different transitive closures. It didn't seem impactful when coming up with the
68            // single-graph and reachability through space (regions) + time (CFG) concepts, but in
69            // practice the combination of time-traveling with kills is more impactful than
70            // initially anticipated.
71            //
72            // Kills should prevent a loan from reaching its successor points in the CFG, but not
73            // while time-traveling: we're not actually at that CFG point, but looking for
74            // predecessor regions that contain the loan. One of the two TCs we had pushed the
75            // transitive subset edges to each point instead of having backward edges, and the
76            // problem didn't exist before. In the abstract, naive reachability is not enough to
77            // model this, we'd need a slightly different solution. For example, maybe with a
78            // two-step traversal:
79            // - at each point we first traverse the subgraph (and possibly time-travel) looking for
80            //   exit nodes while ignoring kills,
81            // - and then when we're back at the current point, we continue normally.
82            //
83            // Another (less annoying) subtlety is that kills and the loan use-map are
84            // flow-insensitive. Kills can actually appear in places before a loan is introduced, or
85            // at a location that is actually unreachable in the CFG from the introduction point,
86            // and these can also be encountered during time-traveling.
87            //
88            // The simplest change that made sense to "fix" the issues above is taking into
89            // account kills that are:
90            // - reachable from the introduction point
91            // - encountered during forward traversal. Note that this is not transitive like the
92            //   two-step traversal described above: only kills encountered on exit via a backward
93            //   edge are ignored.
94            //
95            // In our test suite, there are a couple of cases where kills are encountered while
96            // time-traveling, however as far as we can tell, always in cases where they would be
97            // unreachable. We have reason to believe that this is a property of the single-graph
98            // approach (but haven't proved it yet):
99            // - reachable kills while time-traveling would also be encountered via regular
100            //   traversal
101            // - it makes _some_ sense to ignore unreachable kills, but subtleties around dead code
102            //   in general need to be better thought through (like they were for NLLs).
103            // - ignoring kills is a conservative approximation: the loan is still live and could
104            //   cause false positive errors at another place access. Soundness issues in this
105            //   domain should look more like the absence of reachability instead.
106            //
107            // This is enough in practice to pass tests, and therefore is what we have implemented
108            // for now.
109            //
110            // FIXME: all of the above. Analyze potential unsoundness, possibly in concert with a
111            // borrowck implementation in a-mir-formality, fuzzing, or manually crafting
112            // counter-examples.
113
114            // Continuing traversal will depend on whether the loan is killed at this point, and
115            // whether we're time-traveling.
116            let current_location = liveness.location_from_point(node.point);
117            let is_loan_killed =
118                kills.get(&current_location).is_some_and(|kills| kills.contains(&loan_idx));
119
120            for succ in graph.outgoing_edges(node) {
121                // If the loan is killed at this point, it is killed _on exit_. But only during
122                // forward traversal.
123                if is_loan_killed {
124                    let destination = liveness.location_from_point(succ.point);
125                    if current_location.is_predecessor_of(destination, body) {
126                        continue;
127                    }
128                }
129                stack.push(succ);
130            }
131        }
132    }
133
134    live_loans
135}
136
137/// The localized constraint graph indexes the physical and logical edges to compute a given node's
138/// successors during traversal.
139struct LocalizedConstraintGraph {
140    /// The actual, physical, edges we have recorded for a given node.
141    edges: FxHashMap<LocalizedNode, FxIndexSet<LocalizedNode>>,
142
143    /// The logical edges representing the outlives constraints that hold at all points in the CFG,
144    /// which we don't localize to avoid creating a lot of unnecessary edges in the graph. Some CFGs
145    /// can be big, and we don't need to create such a physical edge for every point in the CFG.
146    logical_edges: FxHashMap<RegionVid, FxIndexSet<RegionVid>>,
147}
148
149/// A node in the graph to be traversed, one of the two vertices of a localized outlives constraint.
150#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
151struct LocalizedNode {
152    region: RegionVid,
153    point: PointIndex,
154}
155
156impl LocalizedConstraintGraph {
157    /// Traverses the constraints and returns the indexed graph of edges per node.
158    fn new<'tcx>(
159        constraints: &LocalizedOutlivesConstraintSet,
160        logical_constraints: impl Iterator<Item = OutlivesConstraint<'tcx>>,
161    ) -> Self {
162        let mut edges: FxHashMap<_, FxIndexSet<_>> = FxHashMap::default();
163        for constraint in &constraints.outlives {
164            let source = LocalizedNode { region: constraint.source, point: constraint.from };
165            let target = LocalizedNode { region: constraint.target, point: constraint.to };
166            edges.entry(source).or_default().insert(target);
167        }
168
169        let mut logical_edges: FxHashMap<_, FxIndexSet<_>> = FxHashMap::default();
170        for constraint in logical_constraints {
171            logical_edges.entry(constraint.sup).or_default().insert(constraint.sub);
172        }
173
174        LocalizedConstraintGraph { edges, logical_edges }
175    }
176
177    /// Returns the outgoing edges of a given node, not its transitive closure.
178    fn outgoing_edges(&self, node: LocalizedNode) -> impl Iterator<Item = LocalizedNode> + use<'_> {
179        // The outgoing edges are:
180        // - the physical edges present at this node,
181        // - the materialized logical edges that exist virtually at all points for this node's
182        //   region, localized at this point.
183        let physical_edges =
184            self.edges.get(&node).into_iter().flat_map(|targets| targets.iter().copied());
185        let materialized_edges =
186            self.logical_edges.get(&node.region).into_iter().flat_map(move |targets| {
187                targets
188                    .iter()
189                    .copied()
190                    .map(move |target| LocalizedNode { point: node.point, region: target })
191            });
192        physical_edges.chain(materialized_edges)
193    }
194}
195
196/// Traverses the MIR and collects kills.
197fn collect_kills<'tcx>(
198    body: &Body<'tcx>,
199    tcx: TyCtxt<'tcx>,
200    borrow_set: &BorrowSet<'tcx>,
201) -> BTreeMap<Location, BTreeSet<BorrowIndex>> {
202    let mut collector = KillsCollector { borrow_set, tcx, body, kills: BTreeMap::default() };
203    for (block, data) in body.basic_blocks.iter_enumerated() {
204        collector.visit_basic_block_data(block, data);
205    }
206    collector.kills
207}
208
209struct KillsCollector<'a, 'tcx> {
210    body: &'a Body<'tcx>,
211    tcx: TyCtxt<'tcx>,
212    borrow_set: &'a BorrowSet<'tcx>,
213
214    /// The set of loans killed at each location.
215    kills: BTreeMap<Location, BTreeSet<BorrowIndex>>,
216}
217
218// This visitor has a similar structure to the `Borrows` dataflow computation with respect to kills,
219// and the datalog polonius fact generation for the `loan_killed_at` relation.
220impl<'tcx> KillsCollector<'_, 'tcx> {
221    /// Records the borrows on the specified place as `killed`. For example, when assigning to a
222    /// local, or on a call's return destination.
223    fn record_killed_borrows_for_place(&mut self, place: Place<'tcx>, location: Location) {
224        // For the reasons described in graph traversal, we also filter out kills
225        // unreachable from the loan's introduction point, as they would stop traversal when
226        // e.g. checking for reachability in the subset graph through invariance constraints
227        // higher up.
228        let filter_unreachable_kills = |loan| {
229            let introduction = self.borrow_set[loan].reserve_location;
230            let reachable = introduction.is_predecessor_of(location, self.body);
231            reachable
232        };
233
234        let other_borrows_of_local = self
235            .borrow_set
236            .local_map
237            .get(&place.local)
238            .into_iter()
239            .flat_map(|bs| bs.iter())
240            .copied();
241
242        // If the borrowed place is a local with no projections, all other borrows of this
243        // local must conflict. This is purely an optimization so we don't have to call
244        // `places_conflict` for every borrow.
245        if place.projection.is_empty() {
246            if !self.body.local_decls[place.local].is_ref_to_static() {
247                self.kills
248                    .entry(location)
249                    .or_default()
250                    .extend(other_borrows_of_local.filter(|&loan| filter_unreachable_kills(loan)));
251            }
252            return;
253        }
254
255        // By passing `PlaceConflictBias::NoOverlap`, we conservatively assume that any given
256        // pair of array indices are not equal, so that when `places_conflict` returns true, we
257        // will be assured that two places being compared definitely denotes the same sets of
258        // locations.
259        let definitely_conflicting_borrows = other_borrows_of_local
260            .filter(|&i| {
261                places_conflict(
262                    self.tcx,
263                    self.body,
264                    self.borrow_set[i].borrowed_place,
265                    place,
266                    PlaceConflictBias::NoOverlap,
267                )
268            })
269            .filter(|&loan| filter_unreachable_kills(loan));
270
271        self.kills.entry(location).or_default().extend(definitely_conflicting_borrows);
272    }
273
274    /// Records the borrows on the specified local as `killed`.
275    fn record_killed_borrows_for_local(&mut self, local: Local, location: Location) {
276        if let Some(borrow_indices) = self.borrow_set.local_map.get(&local) {
277            self.kills.entry(location).or_default().extend(borrow_indices.iter());
278        }
279    }
280}
281
282impl<'tcx> Visitor<'tcx> for KillsCollector<'_, 'tcx> {
283    fn visit_statement(&mut self, statement: &Statement<'tcx>, location: Location) {
284        // Make sure there are no remaining borrows for locals that have gone out of scope.
285        if let StatementKind::StorageDead(local) = statement.kind {
286            self.record_killed_borrows_for_local(local, location);
287        }
288
289        self.super_statement(statement, location);
290    }
291
292    fn visit_assign(&mut self, place: &Place<'tcx>, rvalue: &Rvalue<'tcx>, location: Location) {
293        // When we see `X = ...`, then kill borrows of `(*X).foo` and so forth.
294        self.record_killed_borrows_for_place(*place, location);
295        self.super_assign(place, rvalue, location);
296    }
297
298    fn visit_terminator(&mut self, terminator: &Terminator<'tcx>, location: Location) {
299        // A `Call` terminator's return value can be a local which has borrows, so we need to record
300        // those as killed as well.
301        if let TerminatorKind::Call { destination, .. } = terminator.kind {
302            self.record_killed_borrows_for_place(destination, location);
303        }
304
305        self.super_terminator(terminator, location);
306    }
307}