-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
every match arm reads the match's borrowed input #50783
every match arm reads the match's borrowed input #50783
Conversation
r? @estebank (rust_highfive has picked a reviewer for you, use r? to override) |
(To be clear: hypothetically, it should be fine to make the changes to the MIR codegen happen even without |
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 code looks correct from a quick skim. As this is gated behind a nightly flag, I'm leaning towards merge as-is, although I need to read through it again before approving.
CC @nikomatsakis and @arielb1 for second and third pair of eyes that were involved in the original discussion.
--> $DIR/issue-27282-move-ref-mut-into-guard.rs:24:18 | ||
| | ||
LL | if { (|| { let bar = foo; bar.take() })(); false } => {}, | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot move out of borrowed content |
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.
I haven't dug too much on the desugaring code, but it seems to me that there could be a way to use foo
's span for this error, which would be more appropriate for this and the following error.
| || - | ||
| ||_____| | ||
| |______borrow of `b` occurs here | ||
| borrow later used here |
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.
Note to self, dig for other cases of diagnostics emitting the same span and rework the emitter to just use one span with multiple labels
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.
Also, the AST based errors avoid emitting the two labels if they end up in the same span, NLL should do the same.
--> $DIR/issue-41962.rs:17:9 | ||
| | ||
LL | if let Some(thing) = maybe { | ||
| ^ ----- value moved here |
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.
As mentioned below for match
errors, this span is suboptimal.
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.
Note to self, improve the multiline spans when multiple spans are involved
I agree the diagnostics are suboptimal My preference: land these changes, since they should only affect the diagnostics for NLL, and file a follow-up bug to address them. |
☔ The latest upstream changes (presumably #50615) made this pull request unmergeable. Please resolve the merge conflicts. |
src/librustc_mir/borrow_check/mod.rs
Outdated
Reservation(WriteKind::MutableBorrow(BorrowKind::Mut { .. })) | ||
| Write(WriteKind::MutableBorrow(BorrowKind::Mut { .. })) => { | ||
Reservation(WriteKind::MutableBorrow(borrow_kind @ BorrowKind::Unique)) | ||
| Reservation(WriteKind::MutableBorrow(borrow_kind @ BorrowKind::Mut { .. })) |
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.
Doesn't match the style of the surrounding code (|
aligned to the above line).
@bors r+ |
📌 Commit ea9afcb has been approved by |
src/librustc_mir/interpret/step.rs
Outdated
// FIXME: is there some dynamic semantics we should attach to | ||
// these? Or am I correct in thinking that the inerpreter | ||
// is solely intended for borrowck'ed code? | ||
ReadForMatch(..) => {} |
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.
Correct, no dynamic semantics.
@@ -2559,6 +2567,7 @@ enum ContextKind { | |||
CallDest, | |||
Assert, | |||
Yield, | |||
ReadForMatch, |
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.
we may want to optimize this out when we do other lowerings...maybe as part of the transform/erase_regions.rs
file (it only exists for regions, anyway?) probably there's a better spot
@@ -55,16 +66,34 @@ impl<'a, 'gcx, 'tcx> Builder<'a, 'gcx, 'tcx> { | |||
// HACK(eddyb) Work around the above issue by adding a dummy inspection | |||
// of `discriminant_place`, specifically by applying `Rvalue::Discriminant` | |||
// (which will work regardless of type) and storing the result in a temp. | |||
// | |||
// FIXME: would just the borrow into `borrowed_input_temp` | |||
// also achieve the desired effect here? TBD. |
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.
is this still TBD?
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.
I think the last time I tried removing eddyb's code (labelled "HACK"), it was with a version of my own code that was totally broken, and so I ended up drawing an unjustified conclusion about needing to keep eddyb's code.
So, I would like to revisit the question. My plan is this: I'll try removing eddyb's hack. If I discover that doing so breaks something, then I'll put it back and revise (or remove) this FIXME comment.
If I discover that removing eddyb's code does not cause any tests to break ... then I might revise the PR accordingly and request a follow-up review of the result.
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.
Oh, never mind: I cannot replace eddyb's hack with the borrowed input temp. @eddyb is just injecting a dummy temp that is a read of the discriminant. I'm trying to inject a borrow of the input, but I'm relying on NLL to deal with the fact that I plug in re_empty
for the region.
So if we were to remove eddyb's hack, it would need to wait until after we turn NLL on.
/// Injects a borrow of `place`. The region is unknown at this point; we rely on NLL | ||
/// inference to find an appropriate one. Therefore you can only call this when NLL | ||
/// is turned on. | ||
fn inject_borrow<'a, 'gcx, 'tcx>(tcx: ty::TyCtxt<'a, 'gcx, 'tcx>, |
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.
Total nit: I would call this injected_borrow
, since it just returns the borrow, it doesn't actually insert it into any data structure
// the variant for an enum while you are in | ||
// the midst of matching on it. | ||
// | ||
// FIXME: I originally had put this at the |
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.
Why is this a FIXME? It is an interesting note 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 fixme was for the follow-on bullet about fine tuning where the ReadForMatch
belongs.
Luckily, @arielb1 seems to have already done the hard work of evaluating some seemingly simpler alternatives and then uncovering reasons why they won't work.
I think I'll just remove the FIXME at this point.
src/librustc_mir/borrow_check/mod.rs
Outdated
| Write(WriteKind::MutableBorrow(borrow_kind @ BorrowKind::Mut { .. })) => | ||
{ | ||
let is_local_mutation_allowed = match borrow_kind { | ||
BorrowKind::Unique => LocalMutationIsAllowed::Yes, |
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.
Ugh, this flag blows my mind every time. I don't quite get what Yes
makes sense here, in any case.
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.
I couldn't find a better name for it. No
means that this access is a "direct mutation", and therefore is illegal to do for a non-mut
local, while Yes means that this is an "indirect mutation", and therefore is legal to do to a non-
mut` local.
Ah - maybe that is the way to name it: MutationDirectness::Direct/MutationDirectness::Indirect
?
Urgh, I had those comments hanging around, apparently. |
@bors r-, to address @nikomatsakis' comments and the merge conflict |
@@ -53,14 +53,25 @@ impl<'tcx> Index<BorrowIndex> for BorrowSet<'tcx> { | |||
} | |||
} | |||
|
|||
/// Every two-phase borrow has *exactly one* use (or else it is not a | |||
/// proper two-phase borrow under our current definition. However, not |
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.
Missing end parentheses ( proper two-phase borrow under our current definition).
)
// the midst of matching on it. | ||
// | ||
// FIXME: I originally had put this at the | ||
// start of each elem of arm_blocks (see |
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 theoretically "right" place to put this is "after all arms", just before the This is incorrect, we need the discriminant intact in more placesunreachable
of the FalseEdges
, where we witness the exhaustiveness of the match.
Note that this (as well as your code, and soundly) allows for mutating the discriminant in a guard that later diverges:
let mut foo = Some(4);
match foo {
_ if { if maybe {
// modify the guard, then *diverge*
foo = None;
panic!()
} else { false } } => {},
_ => {}
}
- The
unreachable
at the end of the false-edges chain. That's the "canonical"unreachable
for borrowck purposes, but is not accessible to LLVM:
https://github.com/pnkfelix/rust/blob/ea9afcb6a7c49752271652d40ca7d365057c82fc/src/librustc_mir/build/matches/mod.rs#L195 - The
unreachable
at the end of the real edges chain. That's the "canonical"unreachable
for trans purposes:
https://github.com/pnkfelix/rust/blob/ea9afcb6a7c49752271652d40ca7d365057c82fc/src/librustc_mir/build/matches/mod.rs#L215 - An
unreachable
at the end of the true edge of each guard that is in unreachable code
https://github.com/pnkfelix/rust/blob/ea9afcb6a7c49752271652d40ca7d365057c82fc/src/librustc_mir/build/matches/mod.rs#L585
The unureachable
at (3) can only be reached through a false edge, and I think we can replace it by going to the same place that the false edge goes - i.e. to candidate.next_candidate_pre_binding_block
, and therefore eventualy to a (2) unreachable
- to avoid adding "unnecessary" unreachable terminators.
At every point in "user code" the (2) or (3) unreachable
can be reached, following the false edges will reach the (1) unreachable
, so we can add the test there.
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.
Wait, we still need the check to make sure you are not "cutting the branch you are standing on" and then diverging:
This would be a good test:
#![feature(nll)]
struct ForceFnOnce;
fn main() {
let mut x = &mut Some(&2);
let force_fn_once = ForceFnOnce;
match x {
&mut Some(&_) if {
// ForceFnOnce needed to exploit #27282
(|| { *x = None; drop(force_fn_once); })();
false
} => {}
&mut Some(&a) if {
println!("{}", a);
panic!()
} => {}
&mut _ => {}
}
}
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.
Or, if you still think you can put the check just before the guard:
#![feature(nll)]
struct ForceFnOnce;
fn main() {
let mut x = &mut Some(&2);
let force_fn_once = ForceFnOnce;
match x {
&mut Some(&_) if {
// ForceFnOnce needed to exploit #27282
(|| { *x = None; drop(force_fn_once); })();
false
} => {}
&mut Some(&2) /* this segfaults if we corrupted the discriminant */ if {
panic!()
} => {}
&mut _ => {}
}
}
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.
@arielb1 I'm having a little trouble parsing your comment at the top here.
I think that when you wrote:
Note that this (as well as your code, and soundly) allows for mutating the discriminant in a guard that later diverges
the "this" in that sentence is referencing your original (now struckout) suggestion of
The theoretically "right" place to put this is "after all arms", just before the unreachable of the FalseEdges
(and then the rest of that text, including the enumerated list with three different unreachable's
, was you working out a justification for the struckout suggestion, right?)
Is the above understanding correct?
And so the summary conclusion is ... we certainly cannot rely on just injecting the ReadForMatch
one time at the end of the whole match
(at one of the unreachable points), because we need to retain integrity of the data structure in the face of arms that diverge.
The examples in your most recent comments: Those are meant to be concrete tests showing that the currently implemented strategy (of injecting a ReadForMatch
at the start of each arm) is *necessary.*, right? I've been trying to figure out if either of them are illustrating a flaw/oversight in the current strategy.
Hmm okay I'm going to need to incorporate @arielb1 's feedback into this PR. |
r? @arielb1 |
ea9afcb
to
e0236d9
Compare
(This is just the data structure changes and some boilerplate match code that followed from it; the actual emission of these statements comes in a follow-up commit.)
Also, turn on ReadForMatch emission by default (when using NLL).
…to my code. In particular, I am adding an implicit injected borrow on the pattern matches, and when we go around the loop, the compiler is reporting that this injected borrow is conflicting with the move of the original value when the match succeeds.
For some reason, allowing restricted mutation in match arms exposed an obvious case where a unique borrow can indeed fail, namely something like: ```rust match b { ... ref mut r if { (|| { let bar = &mut *r; **bar = false; })(); false } => { &mut *r } // ~~~~~~~ // | // This ends up holding a `&unique` borrow of `r`, but there ends up being an // implicit shared borrow in the guard thanks to rust-lang#49870 ... } ```
```rust fn main() { fn reuse<X>(_: &mut X) {} let mut t = 2f64; match t { ref mut _b if { false } => { reuse(_b); } _ => {} } } ``` Note: The way this is currently written is confusing; when `autoref` is off, then the arm body bindings (introduced by `bind_matched_candidate_for_arm_body`) are *also* used for the guard. (Any attempt to fix this needs to still ensure that the bindings used by the guard are introduced before the guard is evaluated.) (Once we turn NLL on by default, we can presumably simplify all of that.)
…elsewhere in file.
…post-NLL future. Driveby: just inline the two-line `fn inject_borrow` into its one call site and remove its definition.
d3a931a
to
9d5cdc9
Compare
@bors r=nikomatsakis |
📌 Commit 9d5cdc9 has been approved by |
…ake-three, r=nikomatsakis every match arm reads the match's borrowed input This PR changes the `match` codegen under NLL (and just NLL, at least for now) to make the following adjustments: * It adds a `-Z disable-ast-check-for-mutation-in-guard` which, as described, turns off the naive (conservative but also not 100% sound) check for mutation in guards of match arms. * We now borrow the match input at the outset and emit a special `ReadForMatch` statement (that, according to the *static* semantics, reads that borrowed match input) at the start of each match arm. The intent here is to catch cases where the match guard mutates the match input, either via an independent borrow or via `ref mut` borrows in that arm's pattern. * In order to ensure that `ref mut` borrows do not actually conflict with the emitted `ReadForMatch` statements, I expanded the two-phase-borrow system slightly, and also changed the MIR code gen so that under NLL, when there is a guard on a match arm, then each pattern variable ends up having *three* temporaries associated with it: 1. The first temporary will hold the substructure being matched; this is what we will move the (substructural) value into *if* the guard succeeds. 2. The second temporary also corresponds to the same value as the first, but we are just constructing this temporarily for use during the scope of the guard; it is unaliased and its sole referrer is the third temporary. 3. The third temporary is a reference to the second temporary. * (This sounds complicated, I know, but its actually *simpler* than what I was doing before and had checked into the repo, which was to sometimes construct the final value and then take a reference to it before evaluating the guard. See also PR #49870.) Fix #27282 This also provides a path towards resolving #24535 aka rust-lang/rfcs#1006, at least once the `-Z disable-ast-check-for-mutation-in-guard` is just turned on by default (under NLL, that is. It is not sound under AST-borrowck). * But I did not want to make `#![feature(nll)]` imply that as part of this PR; that seemed like too drastic a change to me.
☀️ Test successful - status-appveyor, status-travis |
Rebase of rust-lang#50783 has accidentally revived the flag (which should be renamed to `-Z codegen-time-graph` by rust-lang#50615).
Remove the unused `-Z trans-time-graph` flag. Rebase of rust-lang#50783 has accidentally revived the flag (which should be renamed to `-Z codegen-time-graph` by rust-lang#50615).
…ate-in-arm, r=nikomatsakis [NLL] make temp for each candidate in `match` arm In NLL, `ref mut` patterns leverage the two-phase borrow infrastructure to allow the shared borrows within a guard before the "activation" of the mutable borrow when we begin execution of the match arm's body. (There is further discussion of this on PR #50783.) To accommodate the restrictions we impose on two-phase borrows (namely that there is a one-to-one mapping between each activation and the original initialization), this PR is making separate temps for each candidate pattern. So in an arm like this: ```rust PatA(_, ref mut ident) | PatB(ref mut ident) | PatC(_, _, ref mut ident) | PatD(ref mut ident) if guard_stuff(ident) => ... ``` instead of 3 temps (two for the guard and one for the arm body), we now have 4 + 2 temps associated with `ident`: one for each candidate plus the actual temp that the guard uses directly, and then the sixth is the temp used in the arm body. Fix #51348
This PR changes the
match
codegen under NLL (and just NLL, at least for now) to make the following adjustments:-Z disable-ast-check-for-mutation-in-guard
which, as described, turns off the naive (conservative but also not 100% sound) check for mutation in guards of match arms.ReadForMatch
statement (that, according to the static semantics, reads that borrowed match input) at the start of each match arm. The intent here is to catch cases where the match guard mutates the match input, either via an independent borrow or viaref mut
borrows in that arm's pattern.ref mut
borrows do not actually conflict with the emittedReadForMatch
statements, I expanded the two-phase-borrow system slightly, and also changed the MIR code gen so that under NLL, when there is a guard on a match arm, then each pattern variable ends up having three temporaries associated with it:Fix #27282
This also provides a path towards resolving #24535 aka rust-lang/rfcs#1006, at least once the
-Z disable-ast-check-for-mutation-in-guard
is just turned on by default (under NLL, that is. It is not sound under AST-borrowck).#![feature(nll)]
imply that as part of this PR; that seemed like too drastic a change to me.