Skip to content

Commit 4c89b5e

Browse files
authored
Rollup merge of rust-lang#138961 - meithecatte:expr-use-visitor, r=Nadrieril,traviscross
Make closure capturing have consistent and correct behaviour around patterns Reference PR: - rust-lang/reference#1837 This PR has two goals: - firstly, it fixes rust-lang#137467. In order to do so, it needs to introduce a small breaking change surrounding the interaction of closure captures with matching against enums with uninhabited variants. Yes – to fix an ICE! - this also fixes rust-lang#138973, a slightly different case with the same root cause. - likewise, fixes rust-lang#140011. - secondly, it fixes rust-lang#137553, making the closure capturing rules consistent between `let` patterns and `match` patterns. This is new insta-stable behavior. ## Background This change concerns how precise closure captures interact with patterns. As a little known feature, patterns that require inspecting only part of a value will only cause that part of the value to get captured: ```rust fn main() { let mut a = (21, 37); // only captures a.0, writing to a.1 does not invalidate the closure let mut f = || { let (ref mut x, _) = a; *x = 42; }; a.1 = 69; f(); } ``` I was not able to find any discussion of this behavior being introduced, or discussion of its edge-cases, but it is [documented in the Rust reference](https://doc.rust-lang.org/reference/types/closure.html#r-type.closure.capture.precision.wildcard). The currently stable behavior is as follows: - if any pattern contains a binding, the place it binds gets captured (implemented in current `walk_pat`) - patterns in refutable positions (`match`, `if let`, `let ... else`, but not destructuring `let` or destructuring function parameters) get processed as follows (`maybe_read_scrutinee`): - if matching against the pattern will at any point require inspecting a discriminant, or it includes a variable binding not followed by an ``@`-pattern,` capture *the entire scrutinee* by reference You will note that this behavior is quite weird and it's hard to imagine a sensible rationale for at least some of its aspects. It has the following issues: - firstly, it assumes that matching against an irrefutable pattern cannot possibly require inspecting any discriminants. With or-patterns, this isn't true, and it is the cause of the rust-lang#137467 ICE. - secondly, the presence of an ``@`-pattern` doesn't really have any semantics by itself. This is the weird behavior tracked as rust-lang#137553. - thirdly, the behavior is different between pattern-matching done through `let` and pattern-matching done through `match` – which is a superficial syntactic difference This PR aims to address all of the above issues. The new behavior is as follows: - like before, if a pattern contains a binding, the place it binds gets captured as required by the binding mode - if matching against the pattern requires inspecting a disciminant, the place whose discriminant needs to be inspected gets captured by reference "requires inspecting a discriminant" is also used here to mean "compare something with a constant" and other such decisions. For types other than ADTs, the details are not interesting and aren't changing. ## The breaking change During closure capture analysis, matching an `enum` against a constructor is considered to require inspecting a discriminant if the `enum` has more than one variant. Notably, this is the case even if all the other variants happen to be uninhabited. This is motivated by implementation difficulties involved in querying whether types are inhabited before we're done with type inference – without moving mountains to make it happen, you hit this assert: https://github.com/rust-lang/rust/blob/43f0014ef0f242418674f49052ed39b70f73bc1c/compiler/rustc_middle/src/ty/inhabitedness/mod.rs#L121 Now, because the previous implementation did not concern itself with capturing the discriminants for irrefutable patterns at all, this is a breaking change – the following example, adapted from the testsuite, compiles on current stable, but will not compile with this PR: ```rust #[derive(Clone, Copy, PartialEq, Eq, Debug)] enum Void {} pub fn main() { let mut r = Result::<Void, (u32, u32)>::Err((0, 0)); let mut f = || { let Err((ref mut a, _)) = r; *a = 1; }; let mut g = || { //~^ ERROR: cannot borrow `r` as mutable more than once at a time let Err((_, ref mut b)) = r; *b = 2; }; f(); g(); assert_eq!(r, Err((1, 2))); } ``` ## Is the breaking change necessary? One other option would be to double down, and introduce a set of syntactic rules for determining whether a sub-pattern is in an irrefutable position, instead of querying the types and checking how many variants there are. **This would not eliminate the breaking change,** but it would limit it to more contrived examples, such as ```rust let ((true, Err((ref mut a, _, _))) | (false, Err((_, ref mut a, _)))) = x; ``` In this example, the `Err`s would not be considered in an irrefutable position, because they are part of an or-pattern. However, current stable would treat this just like a tuple `(bool, (T, U, _))`. While introducing such a distinction would limit the impact, I would say that the added complexity would not be commensurate with the benefit it introduces. ## The new insta-stable behavior If a pattern in a `match` expression or similar has parts it will never read, this part will not be captured anymore: ```rust fn main() { let mut a = (21, 37); // now only captures a.0, instead of the whole a let mut f = || { match a { (ref mut x, _) => *x = 42, } }; a.1 = 69; f(); } ``` Note that this behavior was pretty much already present, but only accessible with this One Weird Trick™: ```rust fn main() { let mut a = (21, 37); // both stable and this PR only capture a.0, because of the no-op `@-pattern` let mut f = || { match a { (ref mut x @ _, _) => *x = 42, } }; a.1 = 69; f(); } ``` ## The second, more practically-relevant breaking change After running crater, we have discovered that the aforementioned insta-stable behavior, where sometimes closures will now capture less, can also manifest as a breaking change. This is because it is possible that previously a closure would capture an entire struct by-move, and now it'll start capturing only part of it – some by move, and some by reference. This then causes the closure to have a more restrictive lifetime than it did previously. See: - rust-lang#138961 (comment) - EC-labs/cec-assignment#1 - tryandromeda/andromeda#43 ## Implementation notes The PR has two main commits: - "ExprUseVisitor: properly report discriminant reads" makes `walk_pat` perform all necessary capturing. This is the part that fixes rust-lang#137467. - "ExprUseVisitor: remove maybe_read_scrutinee" removes the unnecessary "capture the entire scrutinee" behavior, fixing rust-lang#137553. The new logic stops making the distinction between one particular example that used to work, and another ICE, tracked as rust-lang#119786. As this requires an unstable feature, I am leaving this as future work.
2 parents c4dc70e + 9d031bc commit 4c89b5e

40 files changed

+801
-342
lines changed

compiler/rustc_hir_typeck/src/expr_use_visitor.rs

Lines changed: 118 additions & 158 deletions
Large diffs are not rendered by default.

compiler/rustc_hir_typeck/src/upvar.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
761761
/// ],
762762
/// }
763763
/// ```
764+
#[instrument(level = "debug", skip(self))]
764765
fn compute_min_captures(
765766
&self,
766767
closure_def_id: LocalDefId,
@@ -2029,6 +2030,7 @@ struct InferBorrowKind<'tcx> {
20292030
}
20302031

20312032
impl<'tcx> euv::Delegate<'tcx> for InferBorrowKind<'tcx> {
2033+
#[instrument(skip(self), level = "debug")]
20322034
fn fake_read(
20332035
&mut self,
20342036
place_with_id: &PlaceWithHirId<'tcx>,
@@ -2119,6 +2121,7 @@ impl<'tcx> euv::Delegate<'tcx> for InferBorrowKind<'tcx> {
21192121
}
21202122

21212123
/// Rust doesn't permit moving fields out of a type that implements drop
2124+
#[instrument(skip(fcx), ret, level = "debug")]
21222125
fn restrict_precision_for_drop_types<'a, 'tcx>(
21232126
fcx: &'a FnCtxt<'a, 'tcx>,
21242127
mut place: Place<'tcx>,
@@ -2179,6 +2182,7 @@ fn restrict_precision_for_unsafe(
21792182
/// - No unsafe block is required to capture `place`.
21802183
///
21812184
/// Returns the truncated place and updated capture mode.
2185+
#[instrument(ret, level = "debug")]
21822186
fn restrict_capture_precision(
21832187
place: Place<'_>,
21842188
curr_mode: ty::UpvarCapture,
@@ -2208,6 +2212,7 @@ fn restrict_capture_precision(
22082212
}
22092213

22102214
/// Truncate deref of any reference.
2215+
#[instrument(ret, level = "debug")]
22112216
fn adjust_for_move_closure(
22122217
mut place: Place<'_>,
22132218
mut kind: ty::UpvarCapture,
@@ -2222,6 +2227,7 @@ fn adjust_for_move_closure(
22222227
}
22232228

22242229
/// Truncate deref of any reference.
2230+
#[instrument(ret, level = "debug")]
22252231
fn adjust_for_use_closure(
22262232
mut place: Place<'_>,
22272233
mut kind: ty::UpvarCapture,
@@ -2237,6 +2243,7 @@ fn adjust_for_use_closure(
22372243

22382244
/// Adjust closure capture just that if taking ownership of data, only move data
22392245
/// from enclosing stack frame.
2246+
#[instrument(ret, level = "debug")]
22402247
fn adjust_for_non_move_closure(
22412248
mut place: Place<'_>,
22422249
mut kind: ty::UpvarCapture,
@@ -2559,6 +2566,7 @@ fn determine_place_ancestry_relation<'tcx>(
25592566
/// // it is constrained to `'a`
25602567
/// }
25612568
/// ```
2569+
#[instrument(ret, level = "debug")]
25622570
fn truncate_capture_for_optimization(
25632571
mut place: Place<'_>,
25642572
mut curr_mode: ty::UpvarCapture,

compiler/rustc_mir_build/src/builder/matches/match_pair.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,12 @@ impl<'tcx> MatchPairTree<'tcx> {
339339

340340
if let Some(test_case) = test_case {
341341
// This pattern is refutable, so push a new match-pair node.
342+
//
343+
// Note: unless test_case is TestCase::Or, place must not be None.
344+
// This means that the closure capture analysis in
345+
// rustc_hir_typeck::upvar, and in particular the pattern handling
346+
// code of ExprUseVisitor, must capture all of the places we'll use.
347+
// Make sure to keep these two parts in sync!
342348
match_pairs.push(MatchPairTree {
343349
place,
344350
test_case,

src/tools/clippy/clippy_lints/src/methods/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -888,7 +888,7 @@ declare_clippy_lint! {
888888
/// ```
889889
#[clippy::version = "pre 1.29.0"]
890890
pub SEARCH_IS_SOME,
891-
complexity,
891+
nursery,
892892
"using an iterator or string search followed by `is_some()` or `is_none()`, which is more succinctly expressed as a call to `any()` or `contains()` (with negation in case of `is_none()`)"
893893
}
894894

src/tools/clippy/tests/ui/crashes/ice-9041.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#![warn(clippy::search_is_some)]
12
pub struct Thing;
23
//@no-rustfix
34
pub fn has_thing(things: &[Thing]) -> bool {

src/tools/clippy/tests/ui/crashes/ice-9041.stderr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
error: called `is_some()` after searching an `Iterator` with `find`
2-
--> tests/ui/crashes/ice-9041.rs:5:19
2+
--> tests/ui/crashes/ice-9041.rs:6:19
33
|
44
LL | things.iter().find(|p| is_thing_ready(p)).is_some()
55
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider using: `any(|p| is_thing_ready(&p))`

src/tools/clippy/tests/ui/search_is_some_fixable_some.fixed

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -311,19 +311,23 @@ mod issue9120 {
311311
}
312312
}
313313

314+
// skip this test due to rust-lang/rust-clippy#16086
315+
/*
314316
#[allow(clippy::match_like_matches_macro)]
315317
fn issue15102() {
316318
let values = [None, Some(3)];
317-
let has_even = values.iter().any(|v| matches!(&v, Some(x) if x % 2 == 0));
318-
//~^ search_is_some
319+
let has_even = values.iter().find(|v| matches!(v, Some(x) if x % 2 == 0)).is_some();
320+
~^ search_is_some
319321
println!("{has_even}");
320322
321323
let has_even = values
322324
.iter()
323-
.any(|v| match &v {
324-
//~^ search_is_some
325+
.find(|v| match v {
326+
~^ search_is_some
325327
Some(x) if x % 2 == 0 => true,
326328
_ => false,
327-
});
329+
})
330+
.is_some();
328331
println!("{has_even}");
329332
}
333+
*/

src/tools/clippy/tests/ui/search_is_some_fixable_some.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,20 +322,23 @@ mod issue9120 {
322322
}
323323
}
324324

325+
// skip this test due to rust-lang/rust-clippy#16086
326+
/*
325327
#[allow(clippy::match_like_matches_macro)]
326328
fn issue15102() {
327329
let values = [None, Some(3)];
328330
let has_even = values.iter().find(|v| matches!(v, Some(x) if x % 2 == 0)).is_some();
329-
//~^ search_is_some
331+
~^ search_is_some
330332
println!("{has_even}");
331333
332334
let has_even = values
333335
.iter()
334336
.find(|v| match v {
335-
//~^ search_is_some
337+
~^ search_is_some
336338
Some(x) if x % 2 == 0 => true,
337339
_ => false,
338340
})
339341
.is_some();
340342
println!("{has_even}");
341343
}
344+
*/

src/tools/clippy/tests/ui/search_is_some_fixable_some.stderr

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -346,32 +346,5 @@ error: called `is_some()` after searching an `Iterator` with `find`
346346
LL | let _ = v.iter().find(|x: &&u32| (*arg_no_deref_dyn)(x)).is_some();
347347
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider using: `any(|x: &u32| (*arg_no_deref_dyn)(&x))`
348348

349-
error: called `is_some()` after searching an `Iterator` with `find`
350-
--> tests/ui/search_is_some_fixable_some.rs:328:34
351-
|
352-
LL | let has_even = values.iter().find(|v| matches!(v, Some(x) if x % 2 == 0)).is_some();
353-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider using: `any(|v| matches!(&v, Some(x) if x % 2 == 0))`
354-
355-
error: called `is_some()` after searching an `Iterator` with `find`
356-
--> tests/ui/search_is_some_fixable_some.rs:334:10
357-
|
358-
LL | .find(|v| match v {
359-
| __________^
360-
LL | |
361-
LL | | Some(x) if x % 2 == 0 => true,
362-
LL | | _ => false,
363-
LL | | })
364-
LL | | .is_some();
365-
| |__________________^
366-
|
367-
help: consider using
368-
|
369-
LL ~ .any(|v| match &v {
370-
LL +
371-
LL + Some(x) if x % 2 == 0 => true,
372-
LL + _ => false,
373-
LL ~ });
374-
|
375-
376-
error: aborting due to 51 previous errors
349+
error: aborting due to 49 previous errors
377350

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// This test serves to document the change in semantics introduced by
2+
// rust-lang/rust#138961.
3+
//
4+
// A corollary of partial-pattern.rs: while the tuple access testcase makes
5+
// it clear why these semantics are useful, it is actually the dereference
6+
// being performed by the pattern that matters.
7+
8+
fn main() {
9+
// the inner reference is dangling
10+
let x: &&u32 = unsafe {
11+
let x: u32 = 42;
12+
&&* &raw const x
13+
};
14+
15+
let _ = || { //~ ERROR: encountered a dangling reference
16+
match x {
17+
&&_y => {},
18+
}
19+
};
20+
}

0 commit comments

Comments
 (0)