-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
Conversation
/cc @guswynn - You are probably interested in this too, since it should hopefully solve some of the |
☔ The latest upstream changes (presumably #91080) made this pull request unmergeable. Please resolve the merge conflicts. |
a2c78ba
to
4cba899
Compare
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.
Started reading. Here are a few comments. Will schedule some time to read more!
This comment has been minimized.
This comment has been minimized.
0e050ec
to
5381442
Compare
compiler/rustc_typeck/src/check/generator_interior/drop_ranges.rs
Outdated
Show resolved
Hide resolved
compiler/rustc_typeck/src/check/generator_interior/drop_ranges.rs
Outdated
Show resolved
Hide resolved
compiler/rustc_typeck/src/check/generator_interior/drop_ranges.rs
Outdated
Show resolved
Hide resolved
compiler/rustc_typeck/src/check/generator_interior/drop_ranges.rs
Outdated
Show resolved
Hide resolved
compiler/rustc_typeck/src/check/generator_interior/drop_ranges.rs
Outdated
Show resolved
Hide resolved
compiler/rustc_typeck/src/check/generator_interior/drop_ranges.rs
Outdated
Show resolved
Hide resolved
compiler/rustc_typeck/src/check/generator_interior/drop_ranges.rs
Outdated
Show resolved
Hide resolved
@eholk did a first review, this code looks nice, left some suggestions for refactorings, and I'll take a look after you're done! |
This comment has been minimized.
This comment has been minimized.
@rustbot ready |
☔ The latest upstream changes (presumably #91945) made this pull request unmergeable. Please resolve the merge conflicts. |
c36695e
to
1dc57eb
Compare
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.
Hi @eholk! More review thoughts. :)
compiler/rustc_typeck/src/check/generator_interior/drop_ranges.rs
Outdated
Show resolved
Hide resolved
compiler/rustc_typeck/src/check/generator_interior/drop_ranges.rs
Outdated
Show resolved
Hide resolved
compiler/rustc_typeck/src/check/generator_interior/drop_ranges/cfg_propagate.rs
Show resolved
Hide resolved
compiler/rustc_typeck/src/check/generator_interior/drop_ranges/cfg_build.rs
Outdated
Show resolved
Hide resolved
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), |
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.
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?
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 wonder if it wouldn't be better to model this as
fork -> arm0
arm0 -> {guard0, arm1}
guard0 -> {arm-body0, arm1}
arm-body0 -> end
...
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.
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.
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, 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 if
s instead, so I'll do that instead.
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.
For patterns containing multiple alternatives the execution could also go from the guard back to the same arm.
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.
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
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 example could be modified to use a wildcard pattern multiple times; it doesn't introduce any borrows.
compiler/rustc_typeck/src/check/generator_interior/drop_ranges/cfg_propagate.rs
Show resolved
Hide resolved
compiler/rustc_typeck/src/check/generator_interior/drop_ranges/record_consumed_borrow.rs
Show resolved
Hide resolved
compiler/rustc_typeck/src/check/generator_interior/drop_ranges.rs
Outdated
Show resolved
Hide resolved
@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. |
I just got the change tracking working, so is ready to take another look at. |
@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. |
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.
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. |
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 would like to see more concrete documentation, with examples. What is place
and node
and how are they linked?
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 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); |
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 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.
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.
Good idea!
} | ||
|
||
impl From<&PlaceWithHirId<'_>> for TrackedValue { | ||
fn from(place_with_id: &PlaceWithHirId<'_>) -> Self { |
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.
Can you add an assertion that place_with_id.projections
is empty?
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 implement TryFrom
and use unwrap
at the caller
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 decided to go with TryFrom
.
} | ||
} | ||
|
||
fn reinit_expr(&mut self, expr: &hir::Expr<'_>) { |
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 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.
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 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:
- 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.
- 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> { |
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 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 ofa
- Moving a partial path like
a.b.c
is ignored - Reinitializing
a.b.c
counts as a reinitialization ofa
I would use some examples like this:
let mut a = (vec![0], vec![0]);
drop(a.0);
drop(a.1);
// a still considered initialized
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.
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 }; |
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.
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.
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.
It looks like it does not currently work.
I just pushed up a new change that should address your comments. Thanks for the helpful review, as always! |
This comment has been minimized.
This comment has been minimized.
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.
r=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.
+1
@bors delegate |
@bors delegate+ |
✌️ @eholk can now approve this pull request |
Thanks for the approval, @nikomatsakis! r=me |
This comment has been minimized.
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.
53e729b
to
76f6b57
Compare
@bors r=me |
@bors r+ |
📌 Commit 76f6b57 has been approved by |
…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
Is it possible that this causes the ICE in #93161 ? (Btw this large PR should probably have been |
@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 I'll remember |
This PR addresses cases such as this one from #57478:
Previously, the
generator_interior
pass would unnecessarily include the typeFoo
in the generator because it was not aware of the behavior ofdrop
. 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 thedrop
function.There are several phases to the drop tracking algorithm, and we'll go into more detail below.
ExprUseVisitor
to find values that are consumed and borrowed.DropRangeVisitor
uses consume and borrow information to gather drop and reinitialization events, as well as build a control flow graph.DropRanges::propagate_to_fixpoint
).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 thehir_id
of the value, and thehir_id
of the expression that consumes it. For example, in the expression[Foo]
, theFoo
is consumed by the array expression, so after the array expression we can consider theFoo
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
, andloop
. 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