Repository navigation
Improve live drop precision #161627
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve live drop precision #161627
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,16 +2,14 @@ | |
| //! | ||
| //! This contains the dataflow analysis used to track `Qualif`s on complex control-flow graphs. | ||
|
|
||
| use std::fmt; | ||
| use std::marker::PhantomData; | ||
|
|
||
| use rustc_index::bit_set::MixedBitSet; | ||
| use rustc_middle::mir::visit::Visitor; | ||
| use rustc_middle::mir::{ | ||
| self, BasicBlock, CallReturnPlaces, Local, Location, Statement, StatementKind, | ||
| }; | ||
| use rustc_mir_dataflow::fmt::DebugWithContext; | ||
| use rustc_mir_dataflow::{Analysis, JoinSemiLattice}; | ||
| use rustc_mir_dataflow::Analysis; | ||
|
|
||
| use super::{ConstCx, Qualif, qualifs}; | ||
|
|
||
|
|
@@ -24,26 +22,25 @@ use super::{ConstCx, Qualif, qualifs}; | |
| /// borrowed place must contain the qualif. | ||
| struct TransferFunction<'mir, 'tcx, Q> { | ||
| ccx: &'mir ConstCx<'mir, 'tcx>, | ||
| state: &'mir mut State, | ||
| state: &'mir mut MixedBitSet<Local>, | ||
| _qualif: PhantomData<Q>, | ||
| } | ||
|
|
||
| impl<'mir, 'tcx, Q> TransferFunction<'mir, 'tcx, Q> | ||
| where | ||
| Q: Qualif, | ||
| { | ||
| fn new(ccx: &'mir ConstCx<'mir, 'tcx>, state: &'mir mut State) -> Self { | ||
| fn new(ccx: &'mir ConstCx<'mir, 'tcx>, state: &'mir mut MixedBitSet<Local>) -> Self { | ||
| TransferFunction { ccx, state, _qualif: PhantomData } | ||
| } | ||
|
|
||
| fn initialize_state(&mut self) { | ||
| self.state.qualif.clear(); | ||
| self.state.borrow.clear(); | ||
| self.state.clear(); | ||
|
|
||
| for arg in self.ccx.body.args_iter() { | ||
| let arg_ty = self.ccx.body.local_decls[arg].ty; | ||
| if Q::in_any_value_of_ty(self.ccx, arg_ty) { | ||
| self.state.qualif.insert(arg); | ||
| self.state.insert(arg); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -63,15 +60,15 @@ where | |
|
|
||
| match (value, place.as_ref()) { | ||
| (true, mir::PlaceRef { local, .. }) => { | ||
| self.state.qualif.insert(local); | ||
| self.state.insert(local); | ||
| } | ||
|
|
||
| // For now, we do not clear the qualif if a local is overwritten in full by | ||
| // an unqualified rvalue (e.g. `y = 5`). This is to be consistent | ||
| // with aggregates where we overwrite all fields with assignments, which would not | ||
| // get this feature. | ||
| (false, mir::PlaceRef { local: _, projection: &[] }) => { | ||
| // self.state.qualif.remove(*local); | ||
| // self.state.remove(*local); | ||
| } | ||
|
|
||
| _ => {} | ||
|
|
@@ -140,12 +137,8 @@ where | |
| if let mir::Operand::Move(place) = operand | ||
| && let Some(local) = place.as_local() | ||
| { | ||
| // For backward compatibility with the MaybeMutBorrowedLocals used in an earlier | ||
| // implementation we retain qualif if a local had been borrowed before. This might | ||
| // not be strictly necessary since the local is no longer initialized. | ||
| if !self.state.borrow.contains(local) { | ||
| self.state.qualif.remove(local); | ||
| } | ||
| // The local is no longer initialized so we can remove the qualif. | ||
| self.state.remove(local); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -155,8 +148,7 @@ where | |
| rvalue: &mir::Rvalue<'tcx>, | ||
| location: Location, | ||
| ) { | ||
| let qualif = | ||
| qualifs::in_rvalue::<Q, _>(self.ccx, &mut |l| self.state.qualif.contains(l), rvalue); | ||
| let qualif = qualifs::in_rvalue::<Q, _>(self.ccx, &mut |l| self.state.contains(l), rvalue); | ||
| if !place.is_indirect() { | ||
| self.assign_qualif_direct(place, qualif); | ||
| } | ||
|
|
@@ -174,8 +166,7 @@ where | |
| if !borrowed_place.is_indirect() && self.address_of_allows_mutation() { | ||
| let place_ty = borrowed_place.ty(self.ccx.body, self.ccx.tcx).ty; | ||
| if Q::in_any_value_of_ty(self.ccx, place_ty) { | ||
| self.state.qualif.insert(borrowed_place.local); | ||
| self.state.borrow.insert(borrowed_place.local); | ||
| self.state.insert(borrowed_place.local); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -185,8 +176,7 @@ where | |
| { | ||
| let place_ty = borrowed_place.ty(self.ccx.body, self.ccx.tcx).ty; | ||
| if Q::in_any_value_of_ty(self.ccx, place_ty) { | ||
| self.state.qualif.insert(borrowed_place.local); | ||
| self.state.borrow.insert(borrowed_place.local); | ||
| self.state.insert(borrowed_place.local); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -198,8 +188,7 @@ where | |
| && (mutability.is_mut() || !target.is_freeze(self.ccx.tcx, self.ccx.typing_env)) | ||
| { | ||
| if Q::in_any_value_of_ty(self.ccx, *target) { | ||
| self.state.qualif.insert(borrowed_place.local); | ||
| self.state.borrow.insert(borrowed_place.local); | ||
| self.state.insert(borrowed_place.local); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -220,8 +209,7 @@ where | |
| fn visit_statement(&mut self, statement: &Statement<'tcx>, location: Location) { | ||
| match statement.kind { | ||
| StatementKind::StorageDead(local) => { | ||
| self.state.qualif.remove(local); | ||
| self.state.borrow.remove(local); | ||
| self.state.remove(local); | ||
| } | ||
| _ => self.super_statement(statement, location), | ||
| } | ||
|
|
@@ -251,91 +239,26 @@ where | |
| FlowSensitiveAnalysis { ccx, _qualif: PhantomData } | ||
| } | ||
|
|
||
| fn transfer_function(&self, state: &'mir mut State) -> TransferFunction<'mir, 'tcx, Q> { | ||
| fn transfer_function( | ||
| &self, | ||
| state: &'mir mut MixedBitSet<Local>, | ||
| ) -> TransferFunction<'mir, 'tcx, Q> { | ||
| TransferFunction::<Q>::new(self.ccx, state) | ||
| } | ||
| } | ||
|
|
||
| #[derive(Debug, PartialEq, Eq)] | ||
| /// The state for the `FlowSensitiveAnalysis` dataflow analysis. This domain is likely homogeneous, | ||
| /// and has a big size, so we use a bitset that can be sparse (c.f. issue #134404). | ||
| pub(super) struct State { | ||
| /// Describes whether a local contains qualif. | ||
| pub qualif: MixedBitSet<Local>, | ||
| /// Describes whether a local's address escaped and it might become qualified as a result an | ||
| /// indirect mutation. | ||
| pub borrow: MixedBitSet<Local>, | ||
|
Comment on lines
-265
to
-267
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks kind of suspicious. The comment is correct that mutable borrows might change the qualif. We use qualif also e.g. for interior mutability, where Why is it correct to entirely stop tracking borrows here?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like we barely used
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a difference between
Qualif collection eagerly walks through a borrow to its local and collects everything and acts upon it. The thing that was changed here is that const qualif stops doing 1. |
||
| } | ||
|
|
||
| impl Clone for State { | ||
| fn clone(&self) -> Self { | ||
| State { qualif: self.qualif.clone(), borrow: self.borrow.clone() } | ||
| } | ||
|
|
||
| // Data flow engine when possible uses `clone_from` for domain values. | ||
| // Providing an implementation will avoid some intermediate memory allocations. | ||
| fn clone_from(&mut self, other: &Self) { | ||
| self.qualif.clone_from(&other.qualif); | ||
| self.borrow.clone_from(&other.borrow); | ||
| } | ||
| } | ||
|
|
||
| impl State { | ||
| #[inline] | ||
| pub(super) fn contains(&self, local: Local) -> bool { | ||
| self.qualif.contains(local) | ||
| } | ||
| } | ||
|
|
||
| impl<C> DebugWithContext<C> for State { | ||
| fn fmt_with(&self, ctxt: &C, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| f.write_str("qualif: ")?; | ||
| self.qualif.fmt_with(ctxt, f)?; | ||
| f.write_str(" borrow: ")?; | ||
| self.borrow.fmt_with(ctxt, f)?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| fn fmt_diff_with(&self, old: &Self, ctxt: &C, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| if self == old { | ||
| return Ok(()); | ||
| } | ||
|
|
||
| if self.qualif != old.qualif { | ||
| f.write_str("qualif: ")?; | ||
| self.qualif.fmt_diff_with(&old.qualif, ctxt, f)?; | ||
| f.write_str("\n")?; | ||
| } | ||
|
|
||
| if self.borrow != old.borrow { | ||
| f.write_str("borrow: ")?; | ||
| self.borrow.fmt_diff_with(&old.borrow, ctxt, f)?; | ||
| f.write_str("\n")?; | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
| } | ||
|
|
||
| impl JoinSemiLattice for State { | ||
| fn join(&mut self, other: &Self) -> bool { | ||
| self.qualif.join(&other.qualif) || self.borrow.join(&other.borrow) | ||
| } | ||
| } | ||
|
|
||
| impl<'tcx, Q> Analysis<'tcx> for FlowSensitiveAnalysis<'_, 'tcx, Q> | ||
| where | ||
| Q: Qualif, | ||
| { | ||
| type Domain = State; | ||
| /// Describes whether a local contains qualif. This domain is likely homogeneous, and has a big | ||
| /// size, so we use a bitset that can be sparse (c.f. issue #134404). | ||
| type Domain = MixedBitSet<Local>; | ||
|
|
||
| const NAME: &'static str = Q::ANALYSIS_NAME; | ||
|
|
||
| fn bottom_value(&self, body: &mir::Body<'tcx>) -> Self::Domain { | ||
| State { | ||
| qualif: MixedBitSet::new_empty(body.local_decls.len()), | ||
| borrow: MixedBitSet::new_empty(body.local_decls.len()), | ||
| } | ||
| MixedBitSet::new_empty(body.local_decls.len()) | ||
| } | ||
|
|
||
| fn initialize_start_block(&self, _body: &mir::Body<'tcx>, state: &mut Self::Domain) { | ||
|
|
||
This file was deleted.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With proposed semantics from move elimination RFC, move deallocates the local, and any borrows become invalid. In contrast, this is far from obvious why it would be correct now. Consider the following example running under tree borrows:
View changes since the review
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code only runs when
IS_CLEARED_ON_MOVEis set. So qualifs remain in control of whether this should be done. And fordropit seems fine since the drop flag is indeed cleared on move.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example passes const checks and executes live drop. I can't see how this can be fine, besides arguing that program has undefined behavior.
If the flag was actually unset at drop time, the drop wouldn't run.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a = a;does indeed not clear the drop flag ona, so if this analysis thinks that that's a move fromathen that does seem incorrect -- but that doesn't invalidate the general point about moves I think?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is definitely a room for some kind of justification, my point is simply that it requires something beyond operational semantics.
a = ais irrelevant, you can remove it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, but not in the right spot -- the move does unconditionally clear the drop flag. It does not make sense to me that we'd check
borrow.containsthere.It is the
a = None;where we should checkborrow.contains: ifahas been borrowed before, then we should not analyze the value on the right.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah no it just works out -- the UB we detect is just barely enough to avoid "calling non-const function" in the code that we accept based on
shared_borrow_allows_mutation.That is an accident though...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The analysis, as implemented now, is in some ways both more precise and less precise than drop elaboration. It doesn't predict a syntactical absence of non-const drops after drop elaboration. Rather, it tries to predict whether they might be executed when the code runs.
For example, it concludes that
let a: Option<NonConstDrop> = None;is a not live non-const drop, even though drop is present after drop elaboration.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah so it's a funny mix. For
let a: Option<NonConstDrop> = None;with nothing else in the MIR body we don't have to rely on any UB though. So while I agree it makes the correctness statement more tricky, I am not otherwise concerned.The
shared_borrow_allows_mutationsituation is more tricky though. But I guess it's not worth a breaking change, we just have to update the comments to explain that this is fine because const-eval reliably aborts on the UB this is based on.Which brings us back to this PR and the new example -- there, const-eval does not reliably abort, so even if this is UB, we can't use that UB. I think ideally we'd make it so that we do indeed mark a local as "does not need drop" when it is moved-out-of (what the PR does), and additionally we inhibit value-based reasoning on assignments to locals after the local has been (mutably) borrowed. That should work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With MIR move elimination semantics, an allocation is deallocated after move. Once implemented, I would expect it would take an extra effort to miss such UB.