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

Introduce drop range tracking to generator interior analysis #91032

Merged
merged 35 commits into from
Jan 21, 2022

Conversation

eholk
Copy link
Contributor

@eholk eholk commented Nov 19, 2021

This PR addresses cases such as this one from #57478:

struct Foo;
impl !Send for Foo {}

let _: impl Send = || {
    let guard = Foo;
    drop(guard);
    yield;
};

Previously, the generator_interior pass would unnecessarily include the type Foo in the generator because it was not aware of the behavior of drop. We fix this issue by introducing a drop range analysis that finds portions of the code where a value is guaranteed to be dropped. If a value is dropped at all suspend points, then it is no longer included in the generator type. Note that we are using "dropped" in a generic sense to include any case in which a value has been moved. That is, we do not only look at calls to the drop function.

There are several phases to the drop tracking algorithm, and we'll go into more detail below.

  1. Use ExprUseVisitor to find values that are consumed and borrowed.
  2. DropRangeVisitor uses consume and borrow information to gather drop and reinitialization events, as well as build a control flow graph.
  3. We then propagate drop and reinitialization information through the CFG until we reach a fix point (see DropRanges::propagate_to_fixpoint).
  4. When recording a type (see InteriorVisitor::record), we check the computed drop ranges to see if that value is definitely dropped at the suspend point. If so, we skip including it in the type.

1. Use ExprUseVisitor to find values that are consumed and borrowed.

We use ExprUseVisitor to identify the places where values are consumed. We track both the hir_id of the value, and the hir_id of the expression that consumes it. For example, in the expression [Foo], the Foo is consumed by the array expression, so after the array expression we can consider the Foo temporary to be dropped.

In this process, we also collect values that are borrowed. The reason is that the MIR transform for generators conservatively assumes anything borrowed is live across a suspend point (see rustc_mir_transform::generator::locals_live_across_suspend_points). We match this behavior here as well.

2. Gather drop events, reinitialization events, and control flow graph

After finding the values of interest, we perform a post-order traversal over the HIR tree to find the points where these values are dropped or reinitialized. We use the post-order index of each event because this is how the existing generator interior analysis refers to the position of suspend points and the scopes of variables.

During this traversal, we also record branching and merging information to handle control flow constructs such as if, match, and loop. This is necessary because values may be dropped along some control flow paths but not others.

3. Iterate to fixed point

The previous pass found the interesting events and locations, but now we need to find the actual ranges where things are dropped. Upon entry, we have a list of nodes ordered by their position in the post-order traversal. Each node has a set of successors. For each node we additionally keep a bitfield with one bit per potentially consumed value. The bit is set if we the value is dropped along all paths entering this node.

To compute the drop information, we first reverse the successor edges to find each node's predecessors. Then we iterate through each node, and for each node we set its dropped value bitfield to the intersection of all incoming dropped value bitfields.

If any bitfield for any node changes, we re-run the propagation loop again.

4. Ignore dropped values across suspend points

At this point we have a data structure where we can ask whether a value is guaranteed to be dropped at any post order index for the HIR tree. We use this information in InteriorVisitor to check whether a value in question is dropped at a particular suspend point. If it is, we do not include that value's type in the generator type.

Note that we had to augment the region scope tree to include all yields in scope, rather than just the last one as we did before.

r? @nikomatsakis

@rust-highfive rust-highfive added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Nov 19, 2021
@eholk
Copy link
Contributor Author

eholk commented Nov 19, 2021

/cc @guswynn - You are probably interested in this too, since it should hopefully solve some of the must_not_suspend lint issues too.

@bors
Copy link
Contributor

bors commented Nov 20, 2021

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

@apiraino apiraino added the T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. label Nov 22, 2021
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.

Started reading. Here are a few comments. Will schedule some time to read more!

compiler/rustc_typeck/src/check/generator_interior.rs Outdated Show resolved Hide resolved
compiler/rustc_typeck/src/check/generator_interior.rs Outdated Show resolved Hide resolved
@rust-log-analyzer

This comment has been minimized.

@nikomatsakis
Copy link
Contributor

@eholk did a first review, this code looks nice, left some suggestions for refactorings, and I'll take a look after you're done!

@rust-log-analyzer

This comment has been minimized.

@eholk
Copy link
Contributor Author

eholk commented Dec 14, 2021

@rustbot ready

@bors
Copy link
Contributor

bors commented Dec 15, 2021

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

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.

Hi @eholk! More review thoughts. :)

self.drop_ranges.add_control_edge(fork, self.expr_count + 1);
self.visit_pat(pat);
match guard {
Some(Guard::If(expr)) => self.visit_expr(expr),
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't there a missing control edge here?

i.e., after we execute the if, we could go on to execute any future arm, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if it wouldn't be better to model this as

fork -> arm0
arm0 -> {guard0, arm1}
guard0 -> {arm-body0, arm1}
arm-body0 -> end
...

Copy link
Contributor

Choose a reason for hiding this comment

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

Example test case:

async fn main() {
    let x = vec![22_usize];
    std::mem::drop(x);
    match y() {
        true if {x = vec![]; false} => {}
        _ => { dummy().await }
    }
}

async fn dummy() { }
fn y() -> bool {true}

In this test, I imagine we might incorrectly conclude that x must be dropped and not re-initialized in this arm.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, good catch. I modified your example a little bit and am adding it as a test case. I like your idea of modeling match almost more like a chain of ifs instead, so I'll do that instead.

Copy link
Contributor

Choose a reason for hiding this comment

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

For patterns containing multiple alternatives the execution could also go from the guard back to the same arm.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, @tmiasko. Do you have an idea how we might observe this? I tried this:

let mut x = vec![22_usize];
std::mem::drop(x);
match false {
    true | false
        if {
            yield;
            x = vec![];
            false
        } => {}
    _ => {}
}

This doesn't work because I get an error on the yield saying that the borrow of false in the match is still active at the yield point. Does this mean we just can't yield in a match guard? It seems like elsewhere we can yield with borrows still active, so I'm not sure why this case doesn't work...

Here's the error message:

error[E0626]: borrow may still be in use when generator yields
  --> .\src\test\ui\generator\reinit-in-match-guard.rs:26:15
   |
26 |         match false {
   |               ^^^^^
...
29 |                     yield;
   |                     ----- possible yield occurs here

Copy link
Contributor

Choose a reason for hiding this comment

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

The example could be modified to use a wildcard pattern multiple times; it doesn't introduce any borrows.

src/test/ui/async-await/async-fn-nonsend.rs Outdated Show resolved Hide resolved
@eholk
Copy link
Contributor Author

eholk commented Dec 16, 2021

@nikomatsakis - thanks for the comments! I think I've addressed most of them now. I'm going to keep working this afternoon on tracking the potentially dirty sets in the CFG propagation and I'll push another patch up for that.

@eholk
Copy link
Contributor Author

eholk commented Dec 17, 2021

I just got the change tracking working, so is ready to take another look at.

@eholk
Copy link
Contributor Author

eholk commented Dec 20, 2021

@nikomatsakis and I just had a call to talk about this PR. We figured out that the PR's not handling partial drops correctly. Here's a test case that shows the issue:

#![feature(negative_impls)]

fn main() {
    gimme_send(foo());
}

fn gimme_send<T: Send>(t: T) {
    drop(t);
}

struct NotSend {}

impl Drop for NotSend {
    fn drop(&mut self) {}
}

impl !Send for NotSend {}

async fn foo() {
    let mut x = (NotSend {},);
    drop(x.0);
    x.0 = NotSend {};
    bar().await;
}

async fn bar() {}

We decided the way to work around this is to ignore partial drops, and count a partial initialization as an initialization of the whole variable. That's a safer, conservative approximation and if we want in the future we can look into handling partial drops too.

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.

Looking pretty good! Mostly small doc requests here, one meaningful-ish change, although I'm not sure if it's observable. I'd like to review the tests once more time.

DropRanges { tracked_value_map: drop_ranges.tracked_value_map, nodes: drop_ranges.nodes }
}

/// Applies `f` to consumable portion of a HIR node.
Copy link
Contributor

Choose a reason for hiding this comment

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

I would like to see more concrete documentation, with examples. What is place and node and how are they linked?

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 got rid of the node parameter and just passed hir instead so this function can call hir.find on its own, since that seems harder to get wrong. I think I wrote it this way originally because I was running into borrow checker errors where hir ended up borrowing self in the caller and f borrowed self mutably. Maybe I was passing a &Map instead of a Map...

I tried to clarify the documentation too.

let (guard_exit, arm_end_ids) = arms.iter().fold(
(self.expr_index, vec![]),
|(incoming_edge, mut arm_end_ids), hir::Arm { pat, body, guard, .. }| {
self.drop_ranges.add_control_edge(incoming_edge, self.expr_index + 1);
Copy link
Contributor

Choose a reason for hiding this comment

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

This code is kind of bending my mind! I think what would help is if you had an ascii art diagram (https://asciiflow.com/ ftw!) with labels on the various edges that get added, and then you could add a comment to each of the add_control_edge calls to indicate which edge it is adding.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea!

}

impl From<&PlaceWithHirId<'_>> for TrackedValue {
fn from(place_with_id: &PlaceWithHirId<'_>) -> Self {
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 an assertion that place_with_id.projections is empty?

Copy link
Contributor

Choose a reason for hiding this comment

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

Or implement TryFrom and use unwrap at the caller

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 decided to go with TryFrom.

}
}

fn reinit_expr(&mut self, expr: &hir::Expr<'_>) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be good to overapproximate what is initialized here. In other words, if see an assignment like a.b.c = 22, we can consider that a reinitialization of a.

We can then leave a comment that our analysis is always approximated towards more things being initialized than actually are.

It's true that this code doesn't compile today, but the way that this is setup, if we ever did make reinitialization compile, the following bit of code would go wrong I believe:

let pair: (String, String) = ...;
drop(pair);
pair.0 = ...;
pair.1 = ...;

Here, neither pair.0 nor pair.1 would be considered to reinitialize pair, but together they would do so.

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 match on expr.kind and recurse for Field expressions.

We might need to handle other expressions, including horrible things like let mut x = 5; *(loop { break &mut x; }) = 6;, but I think we're okay here for a couple of reasons:

  1. In order to do an assignment like this, we'll have to borrow a variable somewhere. This PR already ignore drops on variables that are borrowed.
  2. You can't borrow a variable that's been dropped.

/// We are interested in points where a variables is dropped or initialized, and the control flow
/// of the code. We identify locations in code by their post-order traversal index, so it is
/// important for this traversal to match that in `RegionResolutionVisitor` and `InteriorVisitor`.
struct DropRangeVisitor<'a, '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 think that we need a comment explaining what kind of approximations are made, particularly around partial paths. I believe the gist is:

  • Moving a counts as a move of a
  • Moving a partial path like a.b.c is ignored
  • Reinitializing a.b.c counts as a reinitialization of a

I would use some examples like this:

let mut a = (vec![0], vec![0]);
drop(a.0);
drop(a.1); 
// a still considered initialized

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup, those are the rules we want. I've tried to capture them in a comment.

assert_send(|| {
//~^ ERROR generator cannot be sent between threads safely
// FIXME: it would be nice to make this work.
let guard = Bar { foo: Foo, x: 42 };
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 make a test like

let guard = Bar { ... };
let Bar { foo, x } = guard;
drop(foo);

Does that work? I think ... maybe? It depends a bit on what events the EUV generates.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It looks like it does not currently work.

@eholk
Copy link
Contributor Author

eholk commented Jan 15, 2022

I just pushed up a new change that should address your comments. Thanks for the helpful review, as always!

@rust-log-analyzer

This comment has been minimized.

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.

r=me

// └─┘ ├─┴──►└─┴┐ │
// │ │ │
// } ▼ ▼ │
// ┌─┐◄───────────────────┘
Copy link
Contributor

Choose a reason for hiding this comment

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

+1

@nikomatsakis
Copy link
Contributor

@bors delegate

@nikomatsakis
Copy link
Contributor

@bors delegate+

@bors
Copy link
Contributor

bors commented Jan 18, 2022

✌️ @eholk can now approve this pull request

@eholk
Copy link
Contributor Author

eholk commented Jan 18, 2022

Thanks for the approval, @nikomatsakis!

r=me

@rust-log-analyzer

This comment has been minimized.

This changes drop range analysis to handle uninhabited return types such
as `!`. Since these calls to these functions do not return, we model
them as ending in an infinite loop.
The previous commit made the non_sync_with_method_call case pass due to
the await being unreachable. Unfortunately, this isn't actually the
behavior the test was verifying. This change lifts the panic into a
helper function so that the generator analysis still thinks the await
is reachable, and therefore we preserve the same testing behavior.
This makes it clearer what values we are tracking and why.
We previously weren't tracking partial re-inits while being too
aggressive around partial drops. With this change, we simply ignore
partial drops, which is the safer, more conservative choice.
@eholk
Copy link
Contributor Author

eholk commented Jan 19, 2022

@bors r=me

@nikomatsakis
Copy link
Contributor

@bors r+

@bors
Copy link
Contributor

bors commented Jan 20, 2022

📌 Commit 76f6b57 has been approved by nikomatsakis

@bors bors 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 Jan 20, 2022
bors added a commit to rust-lang-ci/rust that referenced this pull request Jan 21, 2022
…askrgr

Rollup of 17 pull requests

Successful merges:

 - rust-lang#91032 (Introduce drop range tracking to generator interior analysis)
 - rust-lang#92856 (Exclude "test" from doc_auto_cfg)
 - rust-lang#92860 (Fix errors on blanket impls by ignoring the children of generated impls)
 - rust-lang#93038 (Fix star handling in block doc comments)
 - rust-lang#93061 (Only suggest adding `!` to expressions that can be macro invocation)
 - rust-lang#93067 (rustdoc mobile: fix scroll offset when jumping to internal id)
 - rust-lang#93086 (Add tests to ensure that `let_chains` works with `if_let_guard`)
 - rust-lang#93087 (Fix src/test/run-make/raw-dylib-alt-calling-convention)
 - rust-lang#93091 (⬆ chalk to 0.76.0)
 - rust-lang#93094 (src/test/rustdoc-json: Check for `struct_field`s in `variant_tuple_struct.rs`)
 - rust-lang#93098 (Show a more informative panic message when `DefPathHash` does not exist)
 - rust-lang#93099 (rustdoc: auto create output directory when "--output-format json")
 - rust-lang#93102 (Pretty printer algorithm revamp step 3)
 - rust-lang#93104 (Support --bless for pp-exact pretty printer tests)
 - rust-lang#93114 (update comment for `ensure_monomorphic_enough`)
 - rust-lang#93128 (Add script to prevent point releases with same number as existing ones)
 - rust-lang#93136 (Backport the 1.58.1 release notes to master)

Failed merges:

r? `@ghost`
`@rustbot` modify labels: rollup
@bors bors merged commit 3d10c64 into rust-lang:master Jan 21, 2022
@rustbot rustbot added this to the 1.60.0 milestone Jan 21, 2022
@RalfJung
Copy link
Member

Is it possible that this causes the ICE in #93161 ?

(Btw this large PR should probably have been rollup=never)

@eholk
Copy link
Contributor Author

eholk commented Jan 21, 2022

@RalfJung - Yes, that looks like it.

I'll try and get a fix ready soon, but if you are blocked I can make a small patch that disables the drop tracking without having to untangle this PR from the ones it was rolled up with. There's basically one if that needs changed.

I'll remember rollup=never for the future, thanks for the suggestion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants