@@ -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 }
@@ -135,6 +189,16 @@ impl<'tcx> KillsCollector<'_, 'tcx> {
135189 /// Records the borrows on the specified place as `killed`. For example, when assigning to a
136190 /// local, or on a call's return destination.
137191 fn record_killed_borrows_for_place ( & mut self , place : Place < ' tcx > , location : Location ) {
192+ // For the reasons described in graph traversal, we also filter out kills
193+ // unreachable from the loan's introduction point, as they would stop traversal when
194+ // e.g. checking for reachability in the subset graph through invariance constraints
195+ // higher up.
196+ let filter_unreachable_kills = |loan| {
197+ let introduction = self . borrow_set [ loan] . reserve_location ;
198+ let reachable = introduction. is_predecessor_of ( location, self . body ) ;
199+ reachable
200+ } ;
201+
138202 let other_borrows_of_local = self
139203 . borrow_set
140204 . local_map
@@ -148,7 +212,10 @@ impl<'tcx> KillsCollector<'_, 'tcx> {
148212 // `places_conflict` for every borrow.
149213 if place. projection . is_empty ( ) {
150214 if !self . body . local_decls [ place. local ] . is_ref_to_static ( ) {
151- self . kills . entry ( location) . or_default ( ) . extend ( other_borrows_of_local) ;
215+ self . kills
216+ . entry ( location)
217+ . or_default ( )
218+ . extend ( other_borrows_of_local. filter ( |& loan| filter_unreachable_kills ( loan) ) ) ;
152219 }
153220 return ;
154221 }
@@ -157,15 +224,17 @@ impl<'tcx> KillsCollector<'_, 'tcx> {
157224 // pair of array indices are not equal, so that when `places_conflict` returns true, we
158225 // will be assured that two places being compared definitely denotes the same sets of
159226 // locations.
160- let definitely_conflicting_borrows = other_borrows_of_local. filter ( |& i| {
161- places_conflict (
162- self . tcx ,
163- self . body ,
164- self . borrow_set [ i] . borrowed_place ,
165- place,
166- PlaceConflictBias :: NoOverlap ,
167- )
168- } ) ;
227+ let definitely_conflicting_borrows = other_borrows_of_local
228+ . filter ( |& i| {
229+ places_conflict (
230+ self . tcx ,
231+ self . body ,
232+ self . borrow_set [ i] . borrowed_place ,
233+ place,
234+ PlaceConflictBias :: NoOverlap ,
235+ )
236+ } )
237+ . filter ( |& loan| filter_unreachable_kills ( loan) ) ;
169238
170239 self . kills . entry ( location) . or_default ( ) . extend ( definitely_conflicting_borrows) ;
171240 }
0 commit comments