Skip to content

Commit 8da2740

Browse files
committed
deal with naive reachability weakness
it's a bit mind-bending
1 parent 94a2e55 commit 8da2740

File tree

1 file changed

+82
-13
lines changed

1 file changed

+82
-13
lines changed

compiler/rustc_borrowck/src/polonius/loan_liveness.rs

+82-13
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,69 @@ pub(super) fn compute_loan_liveness<'tcx>(
5454
// Record the loan as being live on entry to this point.
5555
live_loans.insert(node.point, loan_idx);
5656

57-
// Continuing traversal will depend on whether the loan is killed at this point.
57+
// Here, we have a conundrum. There's currently a weakness in our theory, in that
58+
// we're using a single notion of reachability to represent what used to be _two_
59+
// different transitive closures. It didn't seem impactful when coming up with the
60+
// single-graph and reachability through space (regions) + time (CFG) concepts, but in
61+
// practice the combination of time-traveling with kills is more impactful than
62+
// initially anticipated.
63+
//
64+
// Kills should prevent a loan from reaching its successor points in the CFG, but not
65+
// while time-traveling: we're not actually at that CFG point, but looking for
66+
// predecessor regions that contain the loan. One of the two TCs we had pushed the
67+
// transitive subset edges to each point instead of having backward edges, and the
68+
// problem didn't exist before. In the abstract, naive reachability is not enough to
69+
// model this, we'd need a slightly different solution. For example, maybe with a
70+
// two-step traversal:
71+
// - at each point we first traverse the subgraph (and possibly time-travel) looking for
72+
// exit nodes while ignoring kills,
73+
// - and then when we're back at the current point, we continue normally.
74+
//
75+
// Another (less annoying) subtlety is that kills and the loan use-map are
76+
// flow-insensitive. Kills can actually appear in places before a loan is introduced, or
77+
// at a location that is actually unreachable in the CFG from the introduction point,
78+
// and these can also be encountered during time-traveling.
79+
//
80+
// The simplest change that made sense to "fix" the issues above is taking into
81+
// account kills that are:
82+
// - reachable from the introduction point
83+
// - encountered during forward traversal. Note that this is not transitive like the
84+
// two-step traversal described above: only kills encountered on exit via a backward
85+
// edge are ignored.
86+
//
87+
// In our test suite, there are a couple of cases where kills are encountered while
88+
// time-traveling, however as far as we can tell, always in cases where they would be
89+
// unreachable. We have reason to believe that this is a property of the single-graph
90+
// approach (but haven't proved it yet):
91+
// - reachable kills while time-traveling would also be encountered via regular
92+
// traversal
93+
// - it makes _some_ sense to ignore unreachable kills, but subtleties around dead code
94+
// in general need to be better thought through (like they were for NLLs).
95+
// - ignoring kills is a conservative approximation: the loan is still live and could
96+
// cause false positive errors at another place access. Soundness issues in this
97+
// domain should look more like the absence of reachability instead.
98+
//
99+
// This is enough in practice to pass tests, and therefore is what we have implemented
100+
// for now.
101+
//
102+
// FIXME: all of the above. Analyze potential unsoundness, possibly in concert with a
103+
// borrowck implementation in a-mir-formality, fuzzing, or manually crafting
104+
// counter-examples.
105+
106+
// Continuing traversal will depend on whether the loan is killed at this point, and
107+
// whether we're time-traveling.
58108
let current_location = liveness.location_from_point(node.point);
59109
let is_loan_killed =
60110
kills.get(&current_location).is_some_and(|kills| kills.contains(&loan_idx));
61111

62112
for succ in outgoing_edges(&graph, node) {
63-
// If the loan is killed at this point, it is killed _on exit_.
113+
// If the loan is killed at this point, it is killed _on exit_. But only during
114+
// forward traversal.
64115
if is_loan_killed {
65-
continue;
116+
let destination = liveness.location_from_point(succ.point);
117+
if current_location.is_predecessor_of(destination, body) {
118+
continue;
119+
}
66120
}
67121
stack.push(succ);
68122
}
@@ -133,6 +187,16 @@ impl<'tcx> KillsCollector<'_, 'tcx> {
133187
/// Records the borrows on the specified place as `killed`. For example, when assigning to a
134188
/// local, or on a call's return destination.
135189
fn record_killed_borrows_for_place(&mut self, place: Place<'tcx>, location: Location) {
190+
// For the reasons described in graph traversal, we also filter out kills
191+
// unreachable from the loan's introduction point, as they would stop traversal when
192+
// e.g. checking for reachability in the subset graph through invariance constraints
193+
// higher up.
194+
let filter_unreachable_kills = |loan| {
195+
let introduction = self.borrow_set[loan].reserve_location;
196+
let reachable = introduction.is_predecessor_of(location, self.body);
197+
reachable
198+
};
199+
136200
let other_borrows_of_local = self
137201
.borrow_set
138202
.local_map
@@ -146,7 +210,10 @@ impl<'tcx> KillsCollector<'_, 'tcx> {
146210
// `places_conflict` for every borrow.
147211
if place.projection.is_empty() {
148212
if !self.body.local_decls[place.local].is_ref_to_static() {
149-
self.kills.entry(location).or_default().extend(other_borrows_of_local);
213+
self.kills
214+
.entry(location)
215+
.or_default()
216+
.extend(other_borrows_of_local.filter(|&loan| filter_unreachable_kills(loan)));
150217
}
151218
return;
152219
}
@@ -155,15 +222,17 @@ impl<'tcx> KillsCollector<'_, 'tcx> {
155222
// pair of array indices are not equal, so that when `places_conflict` returns true, we
156223
// will be assured that two places being compared definitely denotes the same sets of
157224
// locations.
158-
let definitely_conflicting_borrows = other_borrows_of_local.filter(|&i| {
159-
places_conflict(
160-
self.tcx,
161-
self.body,
162-
self.borrow_set[i].borrowed_place,
163-
place,
164-
PlaceConflictBias::NoOverlap,
165-
)
166-
});
225+
let definitely_conflicting_borrows = other_borrows_of_local
226+
.filter(|&i| {
227+
places_conflict(
228+
self.tcx,
229+
self.body,
230+
self.borrow_set[i].borrowed_place,
231+
place,
232+
PlaceConflictBias::NoOverlap,
233+
)
234+
})
235+
.filter(|&loan| filter_unreachable_kills(loan));
167236

168237
self.kills.entry(location).or_default().extend(definitely_conflicting_borrows);
169238
}

0 commit comments

Comments
 (0)