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

Immovable generators #45337

Merged
merged 6 commits into from
Jan 24, 2018
Merged

Immovable generators #45337

merged 6 commits into from
Jan 24, 2018

Conversation

Zoxc
Copy link
Contributor

@Zoxc Zoxc commented Oct 17, 2017

This adds support for immovable generators which allow you to borrow local values inside generator across suspension points. These are declared using a static keyword:

let mut generator = static || {
    let local = &Vec::new();
    yield;
    local.push(0i8);
};
generator.resume();

// ERROR moving the generator after it has resumed would invalidate the interior reference
// drop(generator);

Region inference is no longer affected by the types stored in generators so the regions inside should be similar to other code (and unaffected by the presence of yield expressions). The borrow checker is extended to pick up the slack so interior references still result in errors for movable generators. This fixes #44197, #45259 and #45093.

This PR depends on PR #44917 (immovable types), I suggest potential reviewers ignore the first commit as it adds immovable types.

@rust-highfive
Copy link
Collaborator

r? @arielb1

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

@Zoxc Zoxc force-pushed the gen-static branch 2 times, most recently from c512527 to 0ffc23a Compare October 17, 2017 05:52
@@ -404,6 +404,19 @@ pub fn super_relate_tys<'a, 'gcx, 'tcx, R>(relation: &mut R,
Ok(tcx.mk_generator(a_id, substs, interior))
}

(&ty::TyGeneratorWitness(a_types), &ty::TyGeneratorWitness(b_types)) =>
{
// FIXME: Relate this without using tuples
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't you replicate the zip loop that the TyTuple branch does?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had to deal with the Binder, which needs an intermediate type. I could create one though.

Copy link
Contributor

Choose a reason for hiding this comment

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

You could implement Relate for &'tcx Slice<Ty<'tcx>> using the same algorithm as Substs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed this to use an intermediate type to make it clear the logic only applies to TyGeneratorWitness.

substs.upvar_tys(def_id, self).chain(iter::once(interior.witness)).map(|ty| {
ty::TyGenerator(def_id, substs, _) => {
// Note that the interior types are ignored here.
// Any type reachable inside the interior must also be reachable
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't feel right. If the generator captures an upvar through a mutable reference, the generator doesn't have dropck constraints:

#![feature(generators, generator_trait)]

use std::cell::RefCell;
use std::ops::Generator;

fn static_alloc<'a, T>(t: T) -> &'a mut T {
    // this is an unsafe function, but it is sound (not running destructors
    // is safe), can be implemented using a static dropless arena of
    // `ManuallyDrop` data, and was planned to be added to the standard
    // library at some point.
    unsafe {
        use std::mem;
        let mut b = Box::new(t);
        let r = &mut *(&mut *b as *mut T);
        mem::forget(b);
        r
    }
}

fn main() {
    let (cell, mut gen);
    cell = Box::new(RefCell::new(0));
    let ref_ = static_alloc(Some(cell.borrow_mut()));
    // the upvar is the non-dropck `&mut Option<Ref<'a, i32>>`.
    gen = || {
        // but the generator can use it to drop a `Ref<'a, i32>`.
        let _d = ref_.take();
        yield;
    };
    gen.resume();
    // drops the RefCell and then the Ref, leading to use-after-free
}

Copy link
Contributor

Choose a reason for hiding this comment

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

It might be a better idea to treat generators like trait object and have them always be dtorck. If generators are "supposed to be" captured by an impl Trait or trait object, this shouldn't be a problem in practice..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This gives the following error.:

error[E0597]: `ref_` does not live long enough
  --> dtorck.rs:27:18
   |
25 |     gen = || {
   |           -- capture occurs here
26 |         // but the generator can use it to drop a `Ref<'a, i32>`.
27 |         let _d = ref_.take();
   |                  ^^^^ does not live long enough
...
32 | }
   | - borrowed value dropped before borrower
   |
   = note: values in a scope are dropped in the opposite order they are created

error: aborting due to previous error

Note that upvars are still checked. Even if we didn't ignore the interior, would higher-ranked regions have any effect on dtorck?

Copy link
Contributor

Choose a reason for hiding this comment

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

Does this still give the error with your PR? the only upvar should be ref_, which should have an empty dtorck constraint. Maybe I just need to check this PR out to see that this holds?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That error is from this PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

Could you make this into a test?

}
ty::TyGeneratorWitness(ts) => {
// FIXME: Should we skip the binder here?
stack.extend(ts.skip_binder().iter().cloned().rev());
Copy link
Contributor

Choose a reason for hiding this comment

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

TyFnPtr skips binders blindly in the next 5 lines, so sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok. Removed this FIXME.

Lvalue::Projection(ref proj) => {
match proj.elem {
// For derefs there's no underlying local that gets borrowed
// It is either a Box or some already borrowed local.
Copy link
Contributor

Choose a reason for hiding this comment

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

// It is either a Box or some already borrowed local.

nit: it can also be a borrow from something based on an upvar, e.g. &fcx.tcx.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was thinking of MIR locals here.

Copy link
Contributor

@arielb1 arielb1 Oct 18, 2017

Choose a reason for hiding this comment

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

(*fcx).tcx is not an already borrowed MIR local, it's from the place where fcx was allocated from, which can be everywhere (e.g. it could be a field of an arena or Vec, which are not a Box).

I just want to avoid comments that lie to avoid rumors spreading.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed this comment.

@@ -369,6 +402,12 @@ fn locals_live_across_suspend_points<'a, 'tcx>(tcx: TyCtxt<'a, 'tcx, 'tcx>,
// Mark locals without storage statements as always having live storage
live_locals.union(&ignored.0);

if !movable {
// For immovable generators we consider borrowed locals to always be live.
Copy link
Contributor

@arielb1 arielb1 Oct 17, 2017

Choose a reason for hiding this comment

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

You could also only promote locals that are borrowed across a yield if you don't want to promote everything.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd need a liveness analysis which properly handles borrows then. I'm waiting on NLL before I do that. My plan is to have a pass which optimizes Storage* instructions based on NLL and then let the generator transformation only use the dataflow analysis on those instructions.

let witness = fcx.tcx.mk_generator_witness(ty::Binder(type_list));

debug!("Types in generator after region replacement {:?}, span = {:?}",
witness, body.value.span);

// Unify the tuple with the witness
Copy link
Contributor

Choose a reason for hiding this comment

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

not a tuple anymore.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.


debug!("Types in generator {:?}, span = {:?}", tuple, body.value.span);
let mut counter = 0;
let type_list = fcx.tcx.fold_regions(&type_list, &mut false, |_, current_depth| {
Copy link
Contributor

@arielb1 arielb1 Oct 17, 2017

Choose a reason for hiding this comment

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

Maybe add a comment to the following effect:

The types in the generator interior contain lifetimes local to the generator itself, which should not be exposed outside of the generator. Therefore, we replace these lifetimes with existentially-bound lifetimes, which reflect the exact value of the lifetimes not being known by users.

These lifetimes are used in auto trait impl checking (for example, if a Sync generator contains an &'α T, we need to check whether &'α T: Sync), so knowledge of the exact relationships between them isn't particularly important.

Note that lifetimes in the type of a local that is currently dead need not be alive. It's up to the generator to ensure that captured lifetimes that are actually used are alive.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that lifetimes in the type of a local that is currently dead need not be alive. It's up to the generator to ensure that captured lifetimes that are actually used are alive.

I'm not sure what you mean by this. I added your other paragraphs as comments.

@@ -0,0 +1,19 @@
// Copyright 2017 The Rust Project Developers. See the COPYRIGHT
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 add a test that static generators actually work - that you can yield *b and that it stays valid?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You could do yield *b in movable generator, given NLL. Did you mean I should use the reference after the yield so the test still works with NLL?

Copy link
Contributor

Choose a reason for hiding this comment

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

That's one thing. Another thing is that you should have a test that actually runs the generator and asserts that the references have the same value between resumes, to quickly catch if some change horribly breaks immovable generator codegen.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed this test.

@@ -0,0 +1,19 @@
// Copyright 2017 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you also add an rpass test for the "vexpr lifetime" case (aka #44197)?

Copy link
Contributor

Choose a reason for hiding this comment

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

I also need to think of more tests to add here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added a test case for #44197.

@kennytm kennytm added the S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. label Oct 17, 2017
@bors
Copy link
Contributor

bors commented Oct 21, 2017

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

@carols10cents
Copy link
Member

Triage ping @Zoxc -- looks like there are merge conflicts again and some test failures!

@Zoxc
Copy link
Contributor Author

Zoxc commented Oct 30, 2017

This PR is blocked on landing #44917.

@Zoxc Zoxc force-pushed the gen-static branch 3 times, most recently from 398c882 to 233b261 Compare November 5, 2017 18:58
@bors
Copy link
Contributor

bors commented Nov 5, 2017

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

@bors
Copy link
Contributor

bors commented Nov 8, 2017

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

@bors
Copy link
Contributor

bors commented Nov 15, 2017

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

@kennytm
Copy link
Member

kennytm commented Nov 22, 2017

triage: still blocked by #44917.

@carols10cents carols10cents added S-blocked Status: Blocked on something else such as an RFC or other implementation work. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Nov 27, 2017
@kennytm kennytm removed the S-blocked Status: Blocked on something else such as an RFC or other implementation work. label Dec 6, 2017
@nikomatsakis
Copy link
Contributor

As an aside, It doesn't seem to me that creating a "static generator" has to be unsafe, but I don't mind that it is. Am I missing something though? That is, is it UB to create a static generator under some conditions (even if you never invoke next?).

I would have thought that the proof burden would be:

  • Once you invoke next the first time,
  • all further calls must use the same pointer.

@arielb1
Copy link
Contributor

arielb1 commented Jan 19, 2018

@nikomatsakis

In the generator's contents signature, all regions are replaced with bound regions. This means a generator type would be something of the form:

⊢ AutoTrait auto trait
⊢ ∀'a,'b. {Foo1<'a>, Foo2<'b>}: AutoTrait
----------------
⊢ Generator#1234<'x, 'y, T, may_contain=∃'a,'b. {Foo1<'a>, Foo2<'b>}, return_type=T): AutoTrait

Basically, the generator contains some of the interior times for some set of substituted lifetimes, so if an OIBITauto trait holds for every substitution of the lifetimes, then it holds for the generator.

Note that we lose all information about relations between the lifetimes - we might have had impl Sync for Foo1<'static>, and the generator might have only ever stored a Foo1<'static>, but we don't try to handle that. I think this should be not that bad in practice because the generator's contents only get used for proving auto traits, which should not have weird lifetime constraints.

@arielb1
Copy link
Contributor

arielb1 commented Jan 19, 2018

It doesn't seem to me that creating a "static generator" has to be unsafe,

ATM static generators use the "standard" safe Generator trait. This means that static generators, once created, are a safety hazard - moving them and calling resume again would cause UB.

The above is probably a bad idea - it is very easy to make an unsafe API using them - and we should create an new ImmovableGenerator trait with an unsafe resume method.

@nikomatsakis
Copy link
Contributor

@arielb1 oh, I hadn't even looked at the trait part of this PR, I just assumed we would have an ImmobileGenerator trait =)

I think that's what we want, yeah.

@Zoxc
Copy link
Contributor Author

Zoxc commented Jan 22, 2018

Am I missing something though? That is, is it UB to create a static generator under some conditions (even if you never invoke next?).

It is always safe to create a generator.

I would have thought that the proof burden would be:

Once you invoke next the first time,
all further calls must use the same pointer.

You have to ensure that once you call resume the generator cannot move in memory. You cannot move it between resume calls.

@nikomatsakis
Copy link
Contributor

It is always safe to create a generator.

By this I guess you mean a non-static generator?

You have to ensure that once you call resume the generator cannot move in memory. You cannot move it between resume calls.

Right, and for this reason I think that the resume method ought to be unsafe.

But I think we can tweak this in follow-up work. I'm pretty happy with this PR.

r=me once rebased.

@kennytm kennytm added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jan 22, 2018
@Zoxc
Copy link
Contributor Author

Zoxc commented Jan 23, 2018

@bors r=nikomatsakis

@bors
Copy link
Contributor

bors commented Jan 23, 2018

📌 Commit 55c6c88 has been approved by nikomatsakis

@bors
Copy link
Contributor

bors commented Jan 23, 2018

⌛ Testing commit 55c6c88 with merge fdecb05...

bors added a commit that referenced this pull request Jan 23, 2018
Immovable generators

This adds support for immovable generators which allow you to borrow local values inside generator across suspension points. These are declared using a `static` keyword:
```rust
let mut generator = static || {
    let local = &Vec::new();
    yield;
    local.push(0i8);
};
generator.resume();

// ERROR moving the generator after it has resumed would invalidate the interior reference
// drop(generator);
```

Region inference is no longer affected by the types stored in generators so the regions inside should be similar to other code (and unaffected by the presence of `yield` expressions). The borrow checker is extended to pick up the slack so interior references still result in errors for movable generators. This fixes #44197, #45259 and #45093.

This PR depends on [PR #44917 (immovable types)](#44917), I suggest potential reviewers ignore the first commit as it adds immovable types.
@bors
Copy link
Contributor

bors commented Jan 24, 2018

☀️ Test successful - status-appveyor, status-travis
Approved by: nikomatsakis
Pushing fdecb05 to master...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

impl Generator complains about missing named lifetime if yielded expression contains a borrow
10 participants