Change:
place: &Place<'tcx> ) { debug!("kill_borrows_on_place: place={:?}", place); <del> // Handle the `Place::Local(..)` case first and exit early. if let Place::Base(PlaceBase::Local(local)) = place { if let Some(borrow_indices) = self.borrow_set.local_map.get(&local) { debug!("kill_borrows_on_place: borrow_indices={:?}", borrow_indices); sets.kill_all(borrow_indices); </del> <ins> if let Some(local) = place.base_local() { let other_borrows_of_local = self .borrow_set .local_map .get(&local) .into_iter() .flat_map(|bs| bs.into_iter()); // If the borrowed place is a local with no projections, all other borrows of this // local must conflict. This is purely an optimization so we don't have to call // `places_conflict` for every borrow. if let Place::Base(PlaceBase::Local(_)) = place { sets.kill_all(other_borrows_of_local); </ins> return; } <del> } // Otherwise, look at all borrows that are live and if they conflict with the assignment // into our place then we can kill them. let mut borrows = sets.on_entry.clone(); let _ = borrows.union(sets.gen_set); for borrow_index in borrows.iter() { let borrow_data = &self.borrows()[borrow_index]; debug!( "kill_borrows_on_place: borrow_index={:?} borrow_data={:?}", borrow_index, borrow_data, ); </del> // By passing `PlaceConflictBias::NoOverlap`, we conservatively assume that any given // pair of array indices are unequal, so that when `places_conflict` returns true, we // will be assured that two places being compared definitely denotes the same sets of // locations. <del> if places_conflict::places_conflict( self.tcx, self.body, &borrow_data.borrowed_place, place, places_conflict::PlaceConflictBias::NoOverlap, ) { debug!( "kill_borrows_on_place: (kill) borrow_index={:?} borrow_data={:?}", borrow_index, borrow_data, ); sets.kill(borrow_index); } </del> <ins> let definitely_conflicting_borrows = other_borrows_of_local .filter(|&&i| { places_conflict::places_conflict( self.tcx, self.body, &self.borrow_set.borrows[i].borrowed_place, place, places_conflict::PlaceConflictBias::NoOverlap) }); sets.kill_all(definitely_conflicting_borrows); </ins> } } }

Discussion:
Spawned off of discussion with and their work on PR There is a dataflow subroutine for , , in PR to reads from the field of (even though I am pretty sure no one was ever supposed to do so; its inclusion in this part of the API was a wart): I think this was written with the assumption that (aka on-entry U gen-set) would represent an upper-bound on the things that should be in the kill set. (And then the subsequent loop determines which of those things to actually put into the kill set.) The problem with that is that when we are computing the transfer-function initially, is going to be the empty-set, at least for almost all basic blocks. That all you have to work with in dataflow. So you end up with a kill-set here that is an under-approximation to what it should actually be, if you are assuming that we're supposed to start with the complete (post dataflow) on-entry and combine that with gen-set to get the upper bound on what to use for the kill-set. Why hasn't this been a problem for us so far? Well, here's my theory: With respect to soundness: if the kill-set for is a subset of what it should "truly be", then I think the compiler ends up treating borrows as living longer than they should, and so it ends up detecting conflicts that are not actually there, and so the compiler rejects code that you might have other expected it to accept.With respect to expressiveness: We don't actually get into the problematic part of as often as you might think, because there's a special dodge at the start of the function: You combine the two points made above, and you end up with things like the following example: <details<summaryClick to see the example Rust code </summary</details(In that example, is basically the same as , except that its wrapped in a unary tuple . I think the intention is that if is accepted, then so should . But NLL currently accepts and )
I'm not 100% sure what action is best here. I think my game plan is roughly: [ ] Double-check the hypothesis that is always the null-set for this part of the code. [ ] If true, then removing its usage from that part of the code.

(That should allow to revise PR so that the methods and friends no longer read from [ ] Experiment with computing the "correct" kill-set. My assumption is that doing so will increase the number of programs we accept. Hopefully it will do so without exposing soundness bugs elsewhere. [ ] But even if its correct, the question will be whether we can afford, computation time-wise,. to do it in the compiler. I am currently assuming that the reason for this loop over on-entry U gen-set is to avoid trying to loop over the whole universe of bits here. [ ] (Update: This is covered in PR ; it avoids looping over the whole universe of bits by mapping the place to its base and then feeding that into the local-to-borrows map.) [ ] (Also it would be good to try to evaluate whether this is even worth doing. Maybe no one cares about this oversight.)
cc
Currently, in the "special dodge", we loop over all borrows of that same local. Could we simply filter that list using when we have a projection to a field?
I'm not sure if I understood the question, but: to support the "special dodge", we specifically build up a that maps each local to all of the borrows of that local. We don't have an analogous map for all places in a given MIR. (and I suspect we do not want to pay the time+memory expense of building up such a map.)
or are you saying we should map each to its base local, and then feed that into the for the basis for the filter here? I hadn't considered whether that could work.
Ah, so only contains the borrows of exactly , not ?
(I think the does contain borrows for as well as , so this may be a reasonable way forward to computing a more complete kill-set and thus get a more accurate transfer-function for the dataflow here.)