Skip to content

[nll] optimize + fix check_if_reassignment_to_immutable_state #53189

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

Closed
nikomatsakis opened this issue Aug 8, 2018 · 10 comments · Fixed by #53258
Closed

[nll] optimize + fix check_if_reassignment_to_immutable_state #53189

nikomatsakis opened this issue Aug 8, 2018 · 10 comments · Fixed by #53258
Assignees
Labels
A-NLL Area: Non-lexical lifetimes (NLL) NLL-performant Working towards the "performance is good" goal NLL-sound Working towards the "invalid code does not compile" goal T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Milestone

Comments

@nikomatsakis
Copy link
Contributor

check_if_reassignment_to_immutable_state has the of handling assignments to immutable local variables. These are a bit tricky: you are allowed to assign to an immutable local variable, but only if it has never been assigned to before. Actually, the logic implemented in the NLL borrow checker is kind of wrong (or at least inconsistent with the AST checker). It's also just slow.

Here is an example of the sort of assignments that should be accepted (playground):

#![feature(nll)]

fn main() {
    let x: (u32, u32);
    
    if true {
        x = (1, 2);
    } else {
        x = (3, 4);
    }
    
    drop(x);
}

This example is rightly rejected (but we should check to ensure we have a test) (playground):

#![feature(nll)]

fn main() {
    let x: (u32, u32);
    x = (1, 2);
    x = (3, 4);
    drop(x);
}

This example should be rejected but currently is not (playground):

#![feature(nll)]

fn main() {
    let x: (u32, u32);
    
    x.0 = 22;
}

In addition to being slightly wrong, the current implementation is also slow. It iterates over all initialized paths and checks that the path being assigned to (e.g., x.0 in the third example) is disjoint from all of them:

for i in flow_state.ever_inits.iter_incoming() {
let init = self.move_data.inits[i];
let init_place = &self.move_data.move_paths[init.path].place;
if places_conflict::places_conflict(self.tcx, self.mir, &init_place, place, Deep) {
self.report_illegal_reassignment(context, (place, span), init.span, err_place);
break;
}
}

I will write below how I think it should work.

@nikomatsakis nikomatsakis added T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. WG-compiler-nll NLL-performant Working towards the "performance is good" goal NLL-sound Working towards the "invalid code does not compile" goal A-NLL Area: Non-lexical lifetimes (NLL) labels Aug 8, 2018
@nikomatsakis nikomatsakis added this to the Rust 2018 RC milestone Aug 8, 2018
@nikomatsakis
Copy link
Contributor Author

I think what we should do:

  • Distinguish the case of assignment directly to an immutable local (x = ...) from any other place.
    • For assignments to an immutable local, we find the move-path-index and just check the "ever initialized" bit directly.
    • For assignments to any other place, we use the self.access_place method with LocalMutationIsAllowed::No (unlike now, where we use ExceptUpvars).

@nikomatsakis
Copy link
Contributor Author

nikomatsakis commented Aug 8, 2018

As a bonus, I think we could remove ExceptUpvars entirely. It is only used in two places -- the one above, and here:

/// Simulates mutation of a place
fn mutate_place(
&mut self,
context: Context,
place: &Place<'tcx>,
kind: ShallowOrDeep,
_mode: MutateMode,
) {
self.access_place(
context,
place,
(kind, Write(WriteKind::Mutate)),
LocalMutationIsAllowed::ExceptUpvars,
);
}

and both should now be just No.

@nikomatsakis
Copy link
Contributor Author

@spastorino would like to take this on.

@pnkfelix
Copy link
Member

pnkfelix commented Aug 8, 2018

At some point I thought we had considered allowing a structs fields to be initialized independently .... don’t remember how far that went at the moment ....

@nikomatsakis
Copy link
Contributor Author

@pnkfelix

We were considering it, but we don't presently allow it. Even with NLL this does not compile, for example:

#![feature(nll)]

fn main() {
    let x: (u32, u32);
    x.0 = 1;
    x.1 = 2;
    println!("{:?}", x);
}

@nikomatsakis
Copy link
Contributor Author

It's true though that the existing borrow checker is sort of inconsistent here -- if you do let mut x, then it permits x.0 = 1 and x.1 = 2 but rejects using x as a whole (just like NLL does).

@nikomatsakis
Copy link
Contributor Author

I was suggesting that we match the existing behavior because it's simple to do — I am not opposed to being smarter later on though.

I guess to do this the "right" way we would do something like:

  • Find the move path corresponding to the place being assigned
    • If there is no precise match, that is an error (e.g., assigning to a single field of a drop struct)
  • If there is a precise match, check its bit

Sound about right @pnkfelix ?

@memoryruins
Copy link
Contributor

memoryruins commented Aug 8, 2018

Following a similar table format from #53114:

Sample Current NLL Example
let x: (u32, u32); x.0 = 1; x.1 = 2; ✔️ playground
let mut x: (u32, u32); x.0 = 1; x.1 = 2; ✔️ ⚠️ playground
println!("{:?}", x); playground

@nikomatsakis
Copy link
Contributor Author

@memoryruins I guess that the ⚠️ in that table refers to the fact that you get a warning about the mut not being needed? That would be false if we take the simplified version, I guess.

In any case, I can go either way here. I'd be fine with matching the AST checker, because it's easy to do, and then trying to revisit this whole question more carefully (in particular, I would want later references to x to work).

But it's also not that hard to make the current system more efficient, and I think it is more internally consistent than what the AST checker does.

@memoryruins
Copy link
Contributor

Correct, the ⚠️ refers to the following warning:

4 |     let mut x: (u32, u32);
  |         ----^
  |         |
  |         help: remove this `mut`

bors added a commit that referenced this issue Aug 19, 2018
…-immutable-state, r=pnkfelix

optimize reassignment immutable state

This is the "simple fix" when it comes to checking for reassignment. We just shoot for compatibility with the AST-based checker. Makes no attempt to solve #21232.

I opted for this simpler fix because I didn't want to think about complications [like the ones described here](#21232 (comment)).

Let's do some profiling measurements.

Fixes #53189

r? @pnkfelix
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-NLL Area: Non-lexical lifetimes (NLL) NLL-performant Working towards the "performance is good" goal NLL-sound Working towards the "invalid code does not compile" goal T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants