-
Notifications
You must be signed in to change notification settings - Fork 13.3k
[WIP] Maybe initialized liveness #45794
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
[WIP] Maybe initialized liveness #45794
Conversation
Thanks for the pull request, and welcome! The Rust team is excited to review your changes, and you should hear from @nikomatsakis (or someone else) soon. If any changes to this PR are deemed necessary, please add them as extra commits. This ensures that the reviewer can see what has changed since they last reviewed the code. Due to the way GitHub handles out-of-date commits, this should also make it reasonably obvious what issues have or haven't been addressed. Large or tricky changes may require several passes of review and changes. Please see the contribution instructions for more information. |
This is for #45665 |
@@ -79,8 +88,12 @@ impl<'cx, 'gcx, 'tcx> ConstraintGeneration<'cx, 'gcx, 'tcx> { | |||
.drop | |||
.simulate_block(self.mir, bb, |location, live_locals| { | |||
for live_local in live_locals.iter() { | |||
let live_local_ty = self.mir.local_decls[live_local].ty; | |||
self.add_drop_live_constraint(live_local_ty, location); | |||
if let LookupResult::Exact(mpi) = self.move_data.rev_lookup.find(&Lvalue::Local(live_local)) { |
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.
@nikomatsakis this should always be true. Your comment in #45665 suggested that move_data.move_path_index
was somehow available here, but I'm not sure if it's missing something (looks cut off in the comment there) or only worked in earlier versions of the code. It's only available on MoveOutIndex, and I'm not sure how to get this through a local. Should I create a method on MovePathLookup
to avoid the if check, or is there something existing that I could have used and just missed?
@@ -38,7 +38,7 @@ pub struct Borrows<'a, 'gcx: 'tcx, 'tcx: 'a> { | |||
location_map: FxHashMap<Location, BorrowIndex>, | |||
region_map: FxHashMap<Region<'tcx>, FxHashSet<BorrowIndex>>, | |||
region_span_map: FxHashMap<RegionKind, Span>, | |||
nonlexical_regioncx: Option<&'a RegionInferenceContext>, | |||
nonlexical_regioncx: Option<RegionInferenceContext>, |
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.
@nikomatsakis I'm not sure if this will be an issue in future work for NLL. Should I add the comments from gitter 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.
The problem, I believe, is that FlowInProgress<T>
is invariant with respect to T
. That means that it doesn't allow any lifetimes in T
to be approximated. This is unfortunate, but ok, not much we can do easily do about it. This is also why you have to introduce 'a
above, rather than just relying on 'cx
.
In any case, because of that, when we have a FlowInProgress<Borrows<'a...>>
and a FlowInProgress<MoveUninits<'a...>>
in the same function call (as you do now), then the lifetime 'a
for both of them has to be precisely the same. This means it has to live as long as the longest thing, which then lives longer than the variable nonlexical_regioncx
.
Anyway, taking ownership is actually fine.
A whole suite of tests fails now. Looks very related, maybe you forgot a check whether mir-borrowck is activated?
|
It's definitely related. I'll have to look into this when I get home later tonight. |
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.
Looks good! I left a few comments, but the biggest thing is that we need a test. I think a ui/nll
test would be good -- we should be able to have two functions, one of which errors and one of which doesn't. Something like this:
struct Wrap<'p> { p: &'p mut i32 }
impl Drop for Wrap {
fn drop(&mut self) {
*self.p += 1;
}
}
fn foo() {
let mut x = 0;
let wrap = Wrap { p: &mut x };
x += 1; //~ ERROR because of dtor
}
fn bar() {
let mut x = 0;
let wrap = Wrap { p: &mut x };
mem::drop(wrap);
x += 1; // OK, drop is inert
}
It'd be great to add more tests that involve fragments, too. e.g. I'd like the following tests. Imagine we have a struct Foo<'p> { a: String, b: Wrap<'p> }
. Then:
- where we move from
foo.a
only. In that case, an error should still result, since the dtor offoo.b
will still execute. - where we move from
foo.a
andfoo.b
and hence no error should be reported - where we move from
foo.b
but notfoo.a
; I believe your current code will report an error, which is fine, but we should be able to make it so that no error occurs in the future
src/librustc_mir/borrow_check/mod.rs
Outdated
todo.push(sibling); | ||
macro_rules! has_any_child_of_impl { | ||
($MaybeTLvals:ident) => { | ||
impl<'b, 'gcx, 'tcx> FlowInProgress<$MaybeTLvals<'b, '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.
I think we could forego the macro and do instead:
impl<'tcx, T: HasMoveData<'tcx>> FlowInProgress<T> { ... }
@@ -38,7 +38,7 @@ pub struct Borrows<'a, 'gcx: 'tcx, 'tcx: 'a> { | |||
location_map: FxHashMap<Location, BorrowIndex>, | |||
region_map: FxHashMap<Region<'tcx>, FxHashSet<BorrowIndex>>, | |||
region_span_map: FxHashMap<RegionKind, Span>, | |||
nonlexical_regioncx: Option<&'a RegionInferenceContext>, | |||
nonlexical_regioncx: Option<RegionInferenceContext>, |
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 problem, I believe, is that FlowInProgress<T>
is invariant with respect to T
. That means that it doesn't allow any lifetimes in T
to be approximated. This is unfortunate, but ok, not much we can do easily do about it. This is also why you have to introduce 'a
above, rather than just relying on 'cx
.
In any case, because of that, when we have a FlowInProgress<Borrows<'a...>>
and a FlowInProgress<MoveUninits<'a...>>
in the same function call (as you do now), then the lifetime 'a
for both of them has to be precisely the same. This means it has to live as long as the longest thing, which then lives longer than the variable nonlexical_regioncx
.
Anyway, taking ownership is actually fine.
regioncx: &'cx mut RegionInferenceContext, | ||
mir: &'cx Mir<'tcx>, | ||
liveness: &'cx LivenessResults, | ||
struct ConstraintGeneration<'a, 'cx: 'a, 'gcx: 'tcx, 'tcx: 'cx> { |
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.
Rather than this, let's rename 'a
to 'cx
and rename 'cx
to 'init
. My reasoning:
- I prefer not to use
'a
or other single-letter names, which I want to reserve for things local to a function signature.- I know most of the compiler doesn't follow this naming scheme. =) Gotta start somewhere though!
- I prefer for
'cx
to be contravariant by convention.- I know the name stinks but it's the one I've been using, we can rename later.
- I prefer then to use
'init
for the lifetime associated with theMaybeInitializedLvals
value.
if let LookupResult::Exact(mpi) = | ||
self.move_data.rev_lookup.find(&Lvalue::Local(live_local)) | ||
{ | ||
if self.flow_inits.has_any_child_of(mpi).is_some() { |
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.
👍 IFIUK, this is basically the "simple" version, in which we consider the entire variable droppable if any part of it is droppable. Seems good as a starting point.
☔ The latest upstream changes (presumably #45668) made this pull request unmergeable. Please resolve the merge conflicts. |
@oli-obk the tests that are failing already had mir-borrowck activated. @nikomatsakis the current failing tests are all caused because there's now an error reported on the
If so, then would the right fix for now be to make those tests pass by adding in the (temporarily expected) error and adding a comment that this should be fixed by a future NLL story (is there a useful name or description I could add there)? I will work on adding the new tests tomorrow. |
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.
@nikomatsakis This is what I could finish this morning, I'll circle back after work to follow up on fixes for the broken tests and to clean up the new test. Let me know if anything is going in the wrong direction.
|
||
fn move_wrap<'p>(_b: Wrap<'p>) { } | ||
|
||
fn baz_a() { |
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.
@nikomatsakis Is this along the lines of what you were expecting?
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.
yep, but we need a x += 1
at the end of each test to show where we expect (or do not expect) errors to occur, I think
| ------ borrow of `x` occurs here | ||
24 | x += 1; //~ ERROR because of dtor | ||
25 | } | ||
| ^ assignment to borrowed `x` occurs 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.
@nikomatsakis these are the StorageDead statements that are getting errors, this is also what's breaking in the compile-fail tests.
@Nashenas88 I don't expect any mir-borrowck failures with this code -- aren't we still gating on |
src/librustc_mir/borrow_check/mod.rs
Outdated
// contain non-lexical lifetimes. It will have a lifetime tied | ||
// to the inference context. | ||
let mut mir: Mir<'tcx> = input_mir.clone(); | ||
let num_region_variables = { |
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 should be
let num_region_variables = if !tcx.sess.opts.debugging_opts.nll { 0 } else {
...;
};
@@ -1467,8 +1481,8 @@ impl<'b, 'gcx, 'tcx> InProgress<'b, 'gcx, 'tcx> { | |||
} | |||
} | |||
|
|||
impl<'b, 'gcx, 'tcx> FlowInProgress<MaybeUninitializedLvals<'b, 'gcx, 'tcx>> { | |||
fn has_any_child_of(&self, mpi: MovePathIndex) -> Option<MovePathIndex> { | |||
impl<'tcx, T> FlowInProgress<T> where T: HasMoveData<'tcx> + BitDenotation<Idx = MovePathIndex> { |
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.
👍
|
||
use super::subtype; | ||
use super::LivenessResults; | ||
use super::ToRegionIndex; | ||
use super::region_infer::RegionInferenceContext; | ||
|
||
pub(super) fn generate_constraints<'a, 'gcx, 'tcx>( | ||
infcx: &InferCtxt<'a, 'gcx, 'tcx>, | ||
pub(super) fn generate_constraints<'init, '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.
thanks for obliging me =)
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.
Any time 😄
self.liveness | ||
.drop | ||
.simulate_block(self.mir, bb, |location, live_locals| { | ||
self.liveness.drop.simulate_block( |
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.
Before we call simulate_block
, we need to do:
self.flow_inits.reset_to_entry_of(bb);
This will reset the state to the entry of the basic block. Heh, I just realized a kind of annoying problem. The flow_inits
wants to walk forward, whereas liveness wants to walk backwards. This means we can't readily "update" the flow-in-progress for each step as we go.
I think then that we what we ought to do is to walk the liveness backwards and collect the live variables into a vector. Something like this:
let mut live_locals: Vec<(Location, Vec<Local>)> = vec![];
self.liveness.drop.simulate_block(self.mir, bb, |location, live_locals| {
live_locals.push((location, live_locals.iter().collect()));
});
self.flow_inits.reset_to_entry_of(bb);
for (index, statement) in block_data.statements.iter() {
let location = Location { block: bb, statement: index };
let (location2, live_locals) = live_locals.pop().unwrap();
assert_eq!(location, location2);
// code you had before:
let mpi = self.move_data.rev_lookup.find_local(live_local);
if self.flow_inits.has_any_child_of(mpi).is_some() {
let live_local_ty = self.mir.local_decls[live_local].ty;
self.add_drop_live_constraint(live_local_ty, location);
}
self.flow_inits.reconstruct_statement_effect(location, statement);
}
// same as above, but for terminator, probably can attach some kind of helper function
error[E0506]: cannot assign to `x` because it is borrowed (Mir) | ||
--> /home/paulf/rust/src/test/ui/nll/maybe-initialized-drop.rs:46:2 | ||
| | ||
42 | let wrap = Wrap { p: &mut x }; |
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 see; this is a bit unfortunate, seems like a separate concern. Not 100% sure what is happening here.
@@ -0,0 +1,46 @@ | |||
error[E0506]: cannot assign to `x` because it is borrowed (Ast) |
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.
So I can't seem to get the ui tests to output the Mir/NLL output. If I add the revisions comment, it fails to even recognize this file and considers every error a new error that wasn't expected. Removing the revisions but keeping the borrowck and nll compiler flags outputs this file. I tried reading the markdown file for tests, but it doesn't say revisions are supported for ui tests. What should I do 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.
Wait, why do you want/have revisions in this test? Also, I feel like these tests are a good example of where I would prefer a compile-fail
test, at least until such time as the ui
tests permit (and enforce) //~ ERROR
annotations. In a compile-fail test, we could put (in the test itself) //~ ERROR (Ast)
for where the old borrow checker reports an error and //~ ERROR (Mir)
for where the new borrow checker -- with NLL enabled -- reports an error. The latter ought to I guess be a subset of the former. It's easier to read this "inline" than to read the stderr.
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.
(In particular, this test is about testing that errors occur, not necessarily what we emit when they do.)
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.
But I guess I'm missing the point. The point is that you only see (Ast)
errors here, 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.
In which case it seems like a bug in the branch =)
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.
Sorry, I completely misunderstood the output of what was going on. I thought certain errors were only showing up when I had the revisions comment, but it's actually the -Z nll
flag that silences the Mir errors. I'll need to look into this more. (My other confusion was that adding in the revisions comment cause the test to ignore the .stderr file, the expected errors section comes out blank, I thought they were related).
let foo = Foo { a: s, b: wrap }; | ||
move_string(foo.a); | ||
move_wrap(foo.b); | ||
x += 1; // OK, drops are inert |
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.
@nikomatsakis is this comment right? Does it make sense?
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.
That comment seems correct to me.
}); | ||
|
||
self.flow_inits.reset_to_entry_of(bb); | ||
for index in 0 .. self.mir.basic_blocks()[bb].statements.len() { |
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 statement
value isn't used by reconstruct_statement_effect
, so I just iterate over the number of statements. Is that still useful as a sanity check, or should we just use the data in all_live_locals
?
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, I see, let's just re-use the data. Note that there should be one final entry to pop, corresponding to the terminator
. If we just collect this into a while let Some((location, live_locals)) = all_live_locals.pop() { .. }
loop, as you suggest, then we can cleanly combine those two things. We just need to be careful not to invoke reconstruct_statement_effect
when we are on the last item but rather reconstruct_terminator_effect
(or nothing at all, since we don't need the final result I don't think).
5b5c240
to
83d60d8
Compare
☔ The latest upstream changes (presumably #45757) made this pull request unmergeable. Please resolve the merge conflicts. |
When we get around to resolving regions, we really ought to take region obligations into account. There is one case where they are presently being ignored. Keep ignoring them there for now but leave a TODO.
This helps make its inputs and outputs more clear.
This new way is **slightly** less expressive (I would be shocked if it affects any code, though) when it comes to higher-ranked bounds or a few other weird tricks. But we don't handle those consistently regardless, and the new way does not require normalization and is just wildly simpler.
Instead, just search the param env predicates directly. This is equivalent to what we were doing before but more efficient.
We've kind of got the same information twice in the MIR, between the return-type field and the local-decls. Seems un-great.
The inference README, in particular, was VERY out of date!
This restores the behavior of regionck with respect to the free-region-map: that is, it collects all the relations from the fn and its closures. This feels a bit fishy but it's the behavior we've had for some time, and it will go away with NLL, so seems best to just keep it.
…ata impl, ensure mir is immutably borrowed after renumbering, and add find_local fn to simplify mpi lookup in constraint generation
… bugs, and update tests based on feedback
83d60d8
to
69a0edf
Compare
Closing this PR. Please re-open it against the |
The constraint generation now only adds a drop live constraint if the lvalue may be initialized.