@@ -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