@@ -54,15 +54,69 @@ pub(super) fn compute_loan_liveness<'tcx>(
54
54
// Record the loan as being live on entry to this point.
55
55
live_loans. insert ( node. point , loan_idx) ;
56
56
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.
58
108
let current_location = liveness. location_from_point ( node. point ) ;
59
109
let is_loan_killed =
60
110
kills. get ( & current_location) . is_some_and ( |kills| kills. contains ( & loan_idx) ) ;
61
111
62
112
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.
64
115
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
+ }
66
120
}
67
121
stack. push ( succ) ;
68
122
}
@@ -133,6 +187,16 @@ impl<'tcx> KillsCollector<'_, 'tcx> {
133
187
/// Records the borrows on the specified place as `killed`. For example, when assigning to a
134
188
/// local, or on a call's return destination.
135
189
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
+
136
200
let other_borrows_of_local = self
137
201
. borrow_set
138
202
. local_map
@@ -146,7 +210,10 @@ impl<'tcx> KillsCollector<'_, 'tcx> {
146
210
// `places_conflict` for every borrow.
147
211
if place. projection . is_empty ( ) {
148
212
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) ) ) ;
150
217
}
151
218
return ;
152
219
}
@@ -155,15 +222,17 @@ impl<'tcx> KillsCollector<'_, 'tcx> {
155
222
// pair of array indices are not equal, so that when `places_conflict` returns true, we
156
223
// will be assured that two places being compared definitely denotes the same sets of
157
224
// 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) ) ;
167
236
168
237
self . kills . entry ( location) . or_default ( ) . extend ( definitely_conflicting_borrows) ;
169
238
}
0 commit comments