@@ -53,15 +53,69 @@ pub(super) fn compute_loan_liveness<'tcx>(
5353 // Record the loan as being live on entry to this point.
5454 live_loans. insert ( node. point , loan_idx) ;
5555
56- // Continuing traversal will depend on whether the loan is killed at this point.
56+ // Here, we have a conundrum. There's currently a weakness in our theory, in that
57+ // we're using a single notion of reachability to represent what used to be _two_
58+ // different transitive closures. It didn't seem impactful when coming up with the
59+ // single-graph and reachability through space (regions) + time (CFG) concepts, but in
60+ // practice the combination of time-traveling with kills is more impactful than
61+ // initially anticipated.
62+ //
63+ // Kills should prevent a loan from reaching its successor points in the CFG, but not
64+ // while time-traveling: we're not actually at that CFG point, but looking for
65+ // predecessor regions that contain the loan. One of the two TCs we had pushed the
66+ // transitive subset edges to each point instead of having backward edges, and the
67+ // problem didn't exist before. In the abstract, naive reachability is not enough to
68+ // model this, we'd need a slightly different solution. For example, maybe with a
69+ // two-step traversal:
70+ // - at each point we first traverse the subgraph (and possibly time-travel) looking for
71+ // exit nodes while ignoring kills,
72+ // - and then when we're back at the current point, we continue normally.
73+ //
74+ // Another (less annoying) subtlety is that kills and the loan use-map are
75+ // flow-insensitive. Kills can actually appear in places before a loan is introduced, or
76+ // at a location that is actually unreachable in the CFG from the introduction point,
77+ // and these can also be encountered during time-traveling.
78+ //
79+ // The simplest change that made sense to "fix" the issues above is taking into
80+ // account kills that are:
81+ // - reachable from the introduction point
82+ // - encountered during forward traversal. Note that this is not transitive like the
83+ // two-step traversal described above: only kills encountered on exit via a backward
84+ // edge are ignored.
85+ //
86+ // In our test suite, there are a couple of cases where kills are encountered while
87+ // time-traveling, however as far as we can tell, always in cases where they would be
88+ // unreachable. We have reason to believe that this is a property of the single-graph
89+ // approach (but haven't proved it yet):
90+ // - reachable kills while time-traveling would also be encountered via regular
91+ // traversal
92+ // - it makes _some_ sense to ignore unreachable kills, but subtleties around dead code
93+ // in general need to be better thought through (like they were for NLLs).
94+ // - ignoring kills is a conservative approximation: the loan is still live and could
95+ // cause false positive errors at another place access. Soundness issues in this
96+ // domain should look more like the absence of reachability instead.
97+ //
98+ // This is enough in practice to pass tests, and therefore is what we have implemented
99+ // for now.
100+ //
101+ // FIXME: all of the above. Analyze potential unsoundness, possibly in concert with a
102+ // borrowck implementation in a-mir-formality, fuzzing, or manually crafting
103+ // counter-examples.
104+
105+ // Continuing traversal will depend on whether the loan is killed at this point, and
106+ // whether we're time-traveling.
57107 let current_location = liveness. location_from_point ( node. point ) ;
58108 let is_loan_killed =
59109 kills. get ( & current_location) . is_some_and ( |kills| kills. contains ( & loan_idx) ) ;
60110
61111 for succ in outgoing_edges ( & graph, node) {
62- // If the loan is killed at this point, it is killed _on exit_.
112+ // If the loan is killed at this point, it is killed _on exit_. But only during
113+ // forward traversal.
63114 if is_loan_killed {
64- continue ;
115+ let destination = liveness. location_from_point ( succ. point ) ;
116+ if current_location. is_predecessor_of ( destination, body) {
117+ continue ;
118+ }
65119 }
66120 stack. push ( succ) ;
67121 }
@@ -130,6 +184,16 @@ impl<'tcx> KillsCollector<'_, 'tcx> {
130184 /// Records the borrows on the specified place as `killed`. For example, when assigning to a
131185 /// local, or on a call's return destination.
132186 fn record_killed_borrows_for_place ( & mut self , place : Place < ' tcx > , location : Location ) {
187+ // For the reasons described in graph traversal, we also filter out kills
188+ // unreachable from the loan's introduction point, as they would stop traversal when
189+ // e.g. checking for reachability in the subset graph through invariance constraints
190+ // higher up.
191+ let filter_unreachable_kills = |loan| {
192+ let introduction = self . borrow_set [ loan] . reserve_location ;
193+ let reachable = introduction. is_predecessor_of ( location, self . body ) ;
194+ reachable
195+ } ;
196+
133197 let other_borrows_of_local = self
134198 . borrow_set
135199 . local_map
@@ -143,7 +207,10 @@ impl<'tcx> KillsCollector<'_, 'tcx> {
143207 // `places_conflict` for every borrow.
144208 if place. projection . is_empty ( ) {
145209 if !self . body . local_decls [ place. local ] . is_ref_to_static ( ) {
146- self . kills . entry ( location) . or_default ( ) . extend ( other_borrows_of_local) ;
210+ self . kills
211+ . entry ( location)
212+ . or_default ( )
213+ . extend ( other_borrows_of_local. filter ( |& loan| filter_unreachable_kills ( loan) ) ) ;
147214 }
148215 return ;
149216 }
@@ -152,15 +219,17 @@ impl<'tcx> KillsCollector<'_, 'tcx> {
152219 // pair of array indices are not equal, so that when `places_conflict` returns true, we
153220 // will be assured that two places being compared definitely denotes the same sets of
154221 // locations.
155- let definitely_conflicting_borrows = other_borrows_of_local. filter ( |& i| {
156- places_conflict (
157- self . tcx ,
158- self . body ,
159- self . borrow_set [ i] . borrowed_place ,
160- place,
161- PlaceConflictBias :: NoOverlap ,
162- )
163- } ) ;
222+ let definitely_conflicting_borrows = other_borrows_of_local
223+ . filter ( |& i| {
224+ places_conflict (
225+ self . tcx ,
226+ self . body ,
227+ self . borrow_set [ i] . borrowed_place ,
228+ place,
229+ PlaceConflictBias :: NoOverlap ,
230+ )
231+ } )
232+ . filter ( |& loan| filter_unreachable_kills ( loan) ) ;
164233
165234 self . kills . entry ( location) . or_default ( ) . extend ( definitely_conflicting_borrows) ;
166235 }
0 commit comments