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(¤t_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}