Skip to content
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

[MIR-borrowck] Two phase borrows #46537

Merged
merged 23 commits into from
Dec 15, 2017
Merged

Conversation

pnkfelix
Copy link
Member

@pnkfelix pnkfelix commented Dec 6, 2017

This adds limited support for two-phase borrows as described in
http://smallcultfollowing.com/babysteps/blog/2017/03/01/nested-method-calls-via-two-phase-borrowing/

The support is off by default; you opt into it via the flag -Z two-phase-borrows

I have written "limited support" above because there are simple variants of the simple v.push(v.len()) example that one would think should work but currently do not, such as the one documented in the test compile-fail/borrowck/two-phase-reservation-sharing-interference-2.rs

(To be clear, that test is not describing something that is unsound. It is just providing an explicit example of a limitation in the implementation given in this PR. I have ideas on how to fix, but I want to land the work that is in this PR first, so that I can stop repeatedly rebasing this branch.)

@rust-highfive
Copy link
Collaborator

r? @arielb1

(rust_highfive has picked a reviewer for you, use r? to override)


fn main() {
let mut v = vec![0, 1, 2];
v.push(v.len());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huzzah.

lookup_result: LookupResult,
each_child: F)
where F: FnMut(MovePathIndex)
pub(crate) fn place_needs_drop<'a, 'gcx, 'tcx>(tcx: TyCtxt<'a, 'gcx, 'tcx>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: formatting is funky. I'm moving these days towards rustfmt-style:

pub(crate) fn place_needs_drop(
    tcx: TyCTxt<...>
    ...,
) -> bool {
    ...
}

@kennytm kennytm added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Dec 6, 2017
Copy link
Contributor

@nikomatsakis nikomatsakis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left various documentation nits.

I'd personally prefer to tweak the "use closures for everything" change to dataflow as well; either to keep the tcx or to thread through the required state as a generic that implements a trait with the method definitions, rather than various closures. I found closures a bit confusing because (a) they move the code out from the fn body into its caller, but (imo) it sort of logically belongs in the callee; (b) they suggest that caller can alter this behavior at will, but I'm not sure that's true; (c) they tend to play poorly with the borrow checker anyway vs a trait, since the borrow checker won't allow two closures to share a mutable borrow (since it doesn't know they don't reach one another).

Anyway, there is one test I didn't see. We want to make sure that people don't start a shared borrow during the reservation period that then continues to be used after the mutable borrow has been activated. Did you have a test for this that I missed? (I actually don't know if the code will handle this correctly or not. I think maybe not, because I believe it requires some kind of special check in the borrow checker at activation time.)

fn read(_: &i32) { } 

fn ok() {
    let mut x = 3;
    let y = &mut x;
    { let z = &x; read(z); }
    *y += 1;
}

fn not_ok() {
    let mut x = 3;
    let y = &mut x;
    let z = &x;
    *y += 1;
    read(z); //~ ERROR somewhere in here
}

erased_ty.needs_drop(gcx, param_env)
}

pub(crate) fn on_lookup_result_bits<'tcx, F, U>(move_data: &MoveData<'tcx>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love to see a comment. e.g. what the heck is has_uniform_drop_state supposed to mean?

move_path_index: MovePathIndex,
mut each_child: F)
where F: FnMut(MovePathIndex)
pub(crate) fn on_all_children_bits<'tcx, F, U>(move_data: &MoveData<'tcx>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: formatting


// let gcx = tcx.global_tcx();
// let erased_ty = gcx.lift(&tcx.erase_regions(&ty)).unwrap();
// if erased_ty.needs_drop(gcx, ctxt.param_env) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Dead code.

location,
|path, s| Self::update_bits(sets, path, s)
)
|lv| place_contents_drop_state_cannot_differ(self.tcx, self.mir, lv),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern with this is that there is more-or-less only one "right thing" you can do here in this closure, right? Or is there ever a time that we want other behavior?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If indeed there all closures are always the same, why not make on_lookup_result_bits abstract over some type T: OnLookupResultBitHelpers, which is only implemented by TyCtxt or something?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One advantage to this approach is that its trivial to add instrumentation to the individual closures. Also, you need both the tcx and the mir, so to get the full effect here (in terms of avoiding having to thread either of those down) I'd have to at the very least make a struct that held both of them, rather than just passing either on its own.

Having said that, I was considering moving to a trait instead of arbitrary closures, largely because I wanted a single thing to carry both of the closures that I occasionally pass in. So I'll look into that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Oh, maybe you mean make it a method on tcx that takes the mir as an argument, and then pass in a closure that does |arg| self.tcx.new_method(self.mir, arg) ...? Not sure how that's better/clearer that |arg| current_func(self.tcx, self.mir, arg), though...)

///
/// When its false, no local clone is constucted; instead a
/// mutable reference directly into `on_entry` is passed along via
/// `SetTriple.on_entry` instead.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the intention that, if this returns false, the on_entry bit set will not be modified by statement_effect and terminator_effect (but merely used as immutable input)? If so, let's say so explicitly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, yes, that is indeed my intent. Will fix.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(The fact that a mutable reference is passed is just an artifact of how support for the true case is implemented.)

}
}

struct FindPlaceUses<'a, 'b: 'a, 'tcx: 'a> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the purpose of this struct should be documented here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I think at this point this file is getting quite complex. We might consider breaking out submodules.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I addressed the doc request but chose not to fragment the file into submodules; I want to wait until after the impl period to do the latter, for the sake of everyone rebasing...)

PlaceContext::Validate => false,

// pure overwrites of an lvalue do not activate it. (note
// LvalueContext::Call is solely about dest lvalue)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: s/LvalueContext/PlaceContext/

// (This can certainly happen in valid code. I
// just want to know about it in the short
// term.)
info!("encountered use of Place {:?} of borrow_idx {:?} \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: debug! ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops. will fix.

fn terminator_effect(&self,
sets: &mut BlockSets<BorrowIndex>,
location: Location) {
fn statement_effect_on_activations(&self,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's mildly surprising that this function also applies the statement effect on reservations. Perhaps rename to statement_effect_on_activations_and_reservations?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Alternatively move that call into the caller, but I think it's maybe nice to have them grouped together here? I guess I could go either way, just want the name to be clear on what is affected.)

Copy link
Member Author

@pnkfelix pnkfelix Dec 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I considered having that call in the caller, but I thought at some point there might be a case where the activation effect would want to observe state from both before and after the reservation effect. (This turned out not to be the case in the code so far, but I didn't want to build in an assumption that things would stay that way.)

Anyway the idea in my head is more that the ActiveBorrows bitvector holds both activations and reservations state. So maybe the problem is the naming here, I need some way to succinctly say "the combined reservation and activation state."

PhasedBorrows maybe? And statement_effect_on_phased_borrows ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm going to change this so that there's a single method and we pass a is_activations: bool parameter representing the context.

'next_borrow: for i in flow_state.borrows.elems_incoming() {
// FIXME for now, just skip the activation state.
if i.is_activation() { continue 'next_borrow; }
let mut elems_incoming = flow_state.borrows.elems_incoming();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-existing, but I feel like the enclosing function here deserves a comment explaining what it does, and I would expect this (quite clever, actually) bit here to be in that comment (i.e., invokes with either a RESERVATION or ACTIVATION for each borrowed path, whichever is more specific).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(addressed by new commit injected earlier in commit history)

@pnkfelix
Copy link
Member Author

pnkfelix commented Dec 6, 2017

Anyway, there is one test I didn't see. [...]

Ack, you are right! Even after you've pointed out several times that the activation point needs this check, I thought what I had for the reservation point would suffice.

Will fix!

@nikomatsakis
Copy link
Contributor

Update: this test seems to compile for me:

#![allow(dead_code)]

fn read(_: &i32) { } 

fn ok() {
    let mut x = 3;
    let y = &mut x;
    { let z = &x; read(z); }
    *y += 1;
}

fn not_ok() {
    let mut x = 3;
    let y = &mut x;
    let z = &x;
    *y += 1;
    read(z); //~ ERROR somewhere in here
}

Example:

> rustc --stage1 ~/tmp/pnkfelix.rs -Zborrowck=mir -Ztwo-phase-borrows
> // look ma, no errors

@bors
Copy link
Contributor

bors commented Dec 6, 2017

☔ The latest upstream changes (presumably #46268) made this pull request unmergeable. Please resolve the merge conflicts.

@pnkfelix
Copy link
Member Author

pnkfelix commented Dec 7, 2017

@nikomatsakis okay, pushed commit that adds support for checking the activation points explicitly against other borrows that overlap it, and added your test (with some extra variants added to show ... a current oddity in the implementation...)

Going to look into tweaking the "use closures for everything" change to dataflow next, before addressing other nits (and rebasing to resolve the upstream conflicts).

@pnkfelix
Copy link
Member Author

pnkfelix commented Dec 8, 2017

@nikomatsakis what do you think of the name trait PlaceObservations for a trait that gathers together fn needs_drop and fn has_uniform_drop_state?

@pnkfelix
Copy link
Member Author

pnkfelix commented Dec 8, 2017

(and is solely implemented by (tcx, mir) ...)

@carols10cents
Copy link
Member

r? @nikomatsakis

this also doubles as a rereview/question answering ping for you!

@nikomatsakis what do you think of the name trait PlaceObservations for a trait that gathers together fn needs_drop and fn has_uniform_drop_state?

Made `do_dataflow` and related API `pub(crate)`.
Having the HIR local id is useful for cases like understanding the
ReScope identifiers, which are now derived from the HIR local id, and
thus one can map an ReScope back to the HIR node, once one knows what
those local ids are.
@pnkfelix
Copy link
Member Author

pnkfelix commented Dec 13, 2017

Okay I think I have incorporated all review feedback (and gone through yet another semi-nasty rebase....)

Lets get this landed! 😄

…taflow.

This is meant to ease development of multi-stage dataflow analyses
where the output from one analysis is used to initialize the state
for the next; in such a context, you cannot start with `bottom_value`
for all the bits.
… intrablock state.

(Still musing about whether it could make sense to revise the design
here to make these constraints on usage explicit.)
@@ -504,9 +529,9 @@ impl<'cx, 'gcx, 'tcx> DataflowResultsConsumer<'cx, 'tcx> for MirBorrowckCtxt<'cx
let data = domain.borrows();
flow_state.borrows.with_elems_outgoing(|borrows| {
for i in borrows {
let borrow = &data[i];
// FIXME: does this need to care about reserved/active distinction?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no because it doesn't care about the borrow being immutable/mutable.

// a terminator location.
return None;
}

let local = if let StatementKind::Assign(Place::Local(local), _) =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you just switch this to be a .get?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

match self.mir[location.block].statements.get(location.statement_index) {
    Statement { kind: StatementKind::Assign(...), ..) => {
    // ..

if self.reservation_error_reported.contains(&place_span.0) {
debug!("skipping access_place for activation of invalid reservation \
place: {:?} borrow_index: {:?}", place_span.0, borrow_index);
return AccessErrorsReported { mutability_error: false, conflict_error: false };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

conflict_error should return true because an error had already been reported (earlier).

(Read(_), BorrowKind::Shared) => Control::Continue,
// Obviously an activation is compatible with its own reservation;
// so don't check if they interfere.
(Activation(_, activating), _) if index.is_reservation() &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the idea of this? If there's a reservation going around a loop, this could actually cause unsoundness.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First off: Good catch. I definitely need to think more about this.

Explanation: The reason for this check here is to ensure that the activation is not treated as a conflict with its own reservation. (Any time we reach a activating use, it is by definition going to have a reservation for that borrow also in the current flow state -- because if there wasn't a reservation in the flow state, then we would not consider the use an activation.)

But your loop example is important, and may show a big flaw in how I've approached this.

I think I've made a mistake in that the current Reservations dataflow is tracking what borrows may reach a location, but for the scenario I'm trying to catch here, it might be necessary to use a must-reach semantics. Building up that flow analysis and employing it (at least for the desired filter here) might be a way to resolve the loop example you give (because with must-reach, a control-flow merge on entry to the loop will remove the reservations that are introduced within the loop itself).

  • Having said that, such an analysis is a pretty big addition to the framework here. I need to think more on this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Another potential answer, suggested by @nikomatsakis, might be to treat the reservation in a loop as an error.)

Reservation(_) => {
debug!("recording invalid reservation of \
place: {:?}", place_span.0);
this.reservation_error_reported.insert(place_span.0.clone());
Copy link
Contributor

@arielb1 arielb1 Dec 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so this error suppression might actually fix #45697 if it's moved out of this loop.

@arielb1
Copy link
Contributor

arielb1 commented Dec 13, 2017

r=me mod. comments

…minators.

(Same net effect as code from before; just cleaner way to get there.)
…of_scope_at_location.

Instead we are "just" careful to invoke it (which sets up a bunch of kill bits)
before we go into the code that sets up the gen bits.

That way, when the gen bits are set up, they will override any
previously set kill-bits for those reservations or activations.
…efore activation"

In reality the currently generated MIR has at least one of the activations
in a copy that occurs before the merge. But still, good to have a test,
in anticipation of that potentially changing...
Instead, filter out (non-)conflicts of activiations with themselves in
the same manner that we filter out non-conflict between an activation
and its reservation.
@pnkfelix
Copy link
Member Author

pnkfelix commented Dec 14, 2017

Okay I think I've addressed almost all of the comments above.

There are some extant bugs that I'll need to address (I only now attempted turning the flag off by default and seeing what happens with all the current -Z borrowck=mir test cases), but I think I want to land this first and then deal with those afterwards.

  • Huzzah, all the "bugs" identified in the compile-fail tests are not bugs at all, they are showing a feature where -Z borrowck=mir -Z two-phase-borrows behaves somewhat like non-lexical lifetimes even without passing -Z nll

(the five identifies failures are discussed in the details block below)

test errors evaluation
compile-fail/borrowck/borrowck-describe-lvalue.rs 22 expected errors not found test is written assuming lexical lifetimes + single-phase borrows (i.e. just mutably borrows with no use to introduce conflict); will fix
compile-fail/borrowck/borrowck-match-already-borrowed.rs compiled successfully! (ibid)
compile-fail/borrowck/borrowck-overloaded-index-ref-index.rs 2 expected errors not found (ibid)
compile-fail/borrowck/borrowck-union-borrow.rs 6 expected errors not found (ibid)
compile-fail/coerce-overloaded-autoderef.rs 1 unexpected error found, 1 expected error not found lol this is just the same error being reworded, perhaps due to a change to traversal order? (i.e. "mut also borrowed as imm" ⇒ "imm also borrowed as mut")

@pnkfelix
Copy link
Member Author

@bors r+

@bors
Copy link
Contributor

bors commented Dec 15, 2017

📌 Commit 159037e has been approved by pnkfelix

@pnkfelix pnkfelix added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Dec 15, 2017
@arielb1
Copy link
Contributor

arielb1 commented Dec 15, 2017

@bors r-

@arielb1
Copy link
Contributor

arielb1 commented Dec 15, 2017

That should have been an r=arielb1 or r=nikomatsakis

@pnkfelix pnkfelix added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. labels Dec 15, 2017
@arielb1
Copy link
Contributor

arielb1 commented Dec 15, 2017

@bors r+

@bors
Copy link
Contributor

bors commented Dec 15, 2017

📌 Commit 159037e has been approved by arielb1

@arielb1
Copy link
Contributor

arielb1 commented Dec 15, 2017

However, it's probably a wise idea to limit reservations to assignments to temporaries. I don't think MIR construction ever does anything else, but you shouldn't be able to do this:

let mut a = 0;
let mut b = 0;
let mut x: &mut u32 = &mut a;
let y = &mut x;
*y = &mut b; // this borrow should be active
// look at me, no use of `*y`, but:
use(x, &b); // should be illegal

@bors
Copy link
Contributor

bors commented Dec 15, 2017

⌛ Testing commit 159037e with merge 84feab3...

bors added a commit that referenced this pull request Dec 15, 2017
[MIR-borrowck] Two phase borrows

This adds limited support for two-phase borrows as described in
  http://smallcultfollowing.com/babysteps/blog/2017/03/01/nested-method-calls-via-two-phase-borrowing/

The support is off by default; you opt into it via the flag `-Z two-phase-borrows`

I have written "*limited* support" above because there are simple variants of the simple `v.push(v.len())` example that one would think should work but currently do not, such as the one documented in the test compile-fail/borrowck/two-phase-reservation-sharing-interference-2.rs

(To be clear, that test is not describing something that is unsound. It is just providing an explicit example of a limitation in the implementation given in this PR. I have ideas on how to fix, but I want to land the work that is in this PR first, so that I can stop repeatedly rebasing this branch.)
@bors
Copy link
Contributor

bors commented Dec 15, 2017

☀️ Test successful - status-appveyor, status-travis
Approved by: arielb1
Pushing 84feab3 to master...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-review Status: Awaiting review from the assignee but also interested parties.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants