From ab722efbd466104bd70faab5107da38863dc72ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maja=20K=C4=85dzio=C5=82ka?= Date: Fri, 30 May 2025 23:09:53 +0200 Subject: [PATCH 1/3] Document how closure capturing interacts with discriminant reads This is the behavior after the bugfixes in rustc PR 138961. --- src/types/closure.md | 70 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/src/types/closure.md b/src/types/closure.md index bcb82e98f..dc3927f32 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -98,7 +98,10 @@ Async closures always capture all input arguments, regardless of whether or not ## Capture Precision r[type.closure.capture.precision.capture-path] -A *capture path* is a sequence starting with a variable from the environment followed by zero or more place projections that were applied to that variable. +A *capture path* is a sequence starting with a variable from the environment followed by zero or more place projections that were applied to that variable, as well as +any [further projections performed by matching against patterns][pattern-wildcards]. + +[pattern-wildcards]: type.closure.capture.precision.wildcard r[type.closure.capture.precision.place-projection] A *place projection* is a [field access], [tuple index], [dereference] (and automatic dereferences), or [array or slice index] expression applied to a variable. @@ -202,7 +205,7 @@ let c = || match x { // x is not captured c(); ``` -This also includes destructuring of tuples, structs, and enums. +This also includes destructuring of tuples, structs, and single-variant enums. Fields matched with the [RestPattern] or [StructPatternEtCetera] are also not considered as read, and thus those fields will not be captured. The following illustrates some of these: @@ -264,6 +267,69 @@ let c = || { [wildcard pattern]: ../patterns.md#wildcard-pattern +r[type.closure.capture.precision.discriminants] +### Capturing for discriminant reads + +If pattern matching requires inspecting a discriminant, the relevant place will get captured by `ImmBorrow`. + +```rust +enum Example { + A(i32), + B(i32), +} + +let mut x = (Example::A(21), 37); + +let c = || match x { // captures `x.0` by ImmBorrow + (Example::A(_), _) => println!("variant A"), + (Example::B(_), _) => println!("variant B"), +}; +x.1 += 1; // x.1 can still be modified +c(); +``` + +r[type.closure.capture.precision.discriminants.single-variant] +Matching against the only variant of an enum does not constitute a discriminant read. + +```rust +enum Example { + A(i32), +} + +let mut x = Example::A(42); +let c = || { + let Example::A(_) = x; // does not capture `x` +}; +x = Example::A(57); // x can be modified while the closure is live +c(); +``` + +r[type.closure.capture.precision.discriminants.non-exhaustive] +If [the `#[non_exhaustive]` attribute][non_exhaustive] is applied to an enum +defined in an external crate, it is considered to have multiple variants, +even if only one variant is actually present. + +[non_exhaustive]: attributes.type-system.non_exhaustive + +r[type.closure.capture.precision.discriminants.uninhabited-variant] +Even if all other variants are uninhabited, the discriminant read still occurs. + +```rust,compile_fail,E0506 +enum Void {} + +enum Example { + A(i32), + B(Void), +} + +let mut x = Example::A(42); +let c = || { + let Example::A(_) = x; // captures `x` by ImmBorrow +}; +x = Example::A(57); // ERROR: cannot assign to `x` because it is borrowed +c(); +``` + r[type.closure.capture.precision.move-dereference] ### Capturing references in move contexts From 6ed91821eebdb4b65771ad8768726cdaadf2bd6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maja=20K=C4=85dzio=C5=82ka?= Date: Fri, 30 May 2025 23:51:14 +0200 Subject: [PATCH 2/3] Document how range and slice patterns can constitute discriminant reads --- src/types/closure.md | 51 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/types/closure.md b/src/types/closure.md index dc3927f32..061756f24 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -300,7 +300,7 @@ let mut x = Example::A(42); let c = || { let Example::A(_) = x; // does not capture `x` }; -x = Example::A(57); // x can be modified while the closure is live +x = Example::A(57); // `x` can be modified while the closure is live c(); ``` @@ -330,6 +330,55 @@ x = Example::A(57); // ERROR: cannot assign to `x` because it is borrowed c(); ``` +r[type.closure.capture.precision.discriminants.range-patterns] +Matching against a [range pattern][patterns.range] constitutes a discriminant read, even if +the range matches all possible values. + +```rust,compile_fail,E0506 +let mut x = 7_u8; +let c = || { + let 0..=u8::MAX = x; // captures `x` by ImmBorrow +}; +x += 1; // ERROR: cannot assign to `x` because it is borrowed +c(); +``` + +r[type.closure.capture.precision.discriminants.slice-patterns] +Matching against a [slice pattern][patterns.slice] constitutes a discriminant read if +the slice pattern needs to inspect the length of the scrutinee. + +```rust,compile_fail,E0506 +let mut x: &mut [i32] = &mut [1, 2, 3]; +let c = || match x { // captures `*x` by ImmBorrow + [_, _, _] => println!("three elements"), + _ => println!("something else"), +}; +x[0] += 1; // ERROR: cannot assign to `x[_]` because it is borrowed +c(); +``` + +Thus, matching against an array doesn't constitute a discriminant read, as the length is fixed. + +```rust +let mut x: [i32; 3] = [1, 2, 3]; +let c = || match x { // does not capture `x` + [_, _, _] => println!("three elements, obviously"), +}; +x[0] += 1; // `x` can be modified while the closure is live +c(); +``` + +Likewise, a slice pattern that matches slices of all possible lengths does not constitute a discriminant read. + +```rust +let mut x: &mut [i32] = &mut [1, 2, 3]; +let c = || match x { // does not capture `x` + [..] => println!("always matches"), +}; +x[0] += 1; // `x` can be modified while the closure is live +c(); +``` + r[type.closure.capture.precision.move-dereference] ### Capturing references in move contexts From dff4a644a176a87ba396a918a0d63bf584128cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maja=20K=C4=85dzio=C5=82ka?= Date: Fri, 11 Jul 2025 18:22:44 +0200 Subject: [PATCH 3/3] Don't call things "discriminant reads" just because they behave like ones --- src/types/closure.md | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/types/closure.md b/src/types/closure.md index 061756f24..0c9598dd3 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -98,8 +98,7 @@ Async closures always capture all input arguments, regardless of whether or not ## Capture Precision r[type.closure.capture.precision.capture-path] -A *capture path* is a sequence starting with a variable from the environment followed by zero or more place projections that were applied to that variable, as well as -any [further projections performed by matching against patterns][pattern-wildcards]. +A *capture path* is a sequence starting with a variable from the environment followed by zero or more place projections that were applied to that variable, as well as any [further projections performed by matching against patterns][pattern-wildcards]. [pattern-wildcards]: type.closure.capture.precision.wildcard @@ -305,9 +304,7 @@ c(); ``` r[type.closure.capture.precision.discriminants.non-exhaustive] -If [the `#[non_exhaustive]` attribute][non_exhaustive] is applied to an enum -defined in an external crate, it is considered to have multiple variants, -even if only one variant is actually present. +If [the `#[non_exhaustive]` attribute][non_exhaustive] is applied to an enum defined in an external crate, it is considered to have multiple variants, even if only one variant is actually present. [non_exhaustive]: attributes.type-system.non_exhaustive @@ -331,8 +328,7 @@ c(); ``` r[type.closure.capture.precision.discriminants.range-patterns] -Matching against a [range pattern][patterns.range] constitutes a discriminant read, even if -the range matches all possible values. +Matching against a [range pattern][patterns.range] performs a read of the place being matched, causing the closure to borrow it by `ImmBorrow`. This is the case even if the range matches all possible values. ```rust,compile_fail,E0506 let mut x = 7_u8; @@ -344,11 +340,10 @@ c(); ``` r[type.closure.capture.precision.discriminants.slice-patterns] -Matching against a [slice pattern][patterns.slice] constitutes a discriminant read if -the slice pattern needs to inspect the length of the scrutinee. +Matching against a [slice pattern][patterns.slice] performs a read if the slice pattern needs to inspect the length of the scrutinee. The read will cause the closure to borrow the relevant place by `ImmBorrow`. ```rust,compile_fail,E0506 -let mut x: &mut [i32] = &mut [1, 2, 3]; +let x: &mut [i32] = &mut [1, 2, 3]; let c = || match x { // captures `*x` by ImmBorrow [_, _, _] => println!("three elements"), _ => println!("something else"), @@ -357,7 +352,7 @@ x[0] += 1; // ERROR: cannot assign to `x[_]` because it is borrowed c(); ``` -Thus, matching against an array doesn't constitute a discriminant read, as the length is fixed. +As such, matching against an array doesn't itself cause any borrows, as the lengthh is fixed and doesn't need to be read. ```rust let mut x: [i32; 3] = [1, 2, 3]; @@ -368,10 +363,10 @@ x[0] += 1; // `x` can be modified while the closure is live c(); ``` -Likewise, a slice pattern that matches slices of all possible lengths does not constitute a discriminant read. +Likewise, a slice pattern that matches slices of all possible lengths does not constitute a read. ```rust -let mut x: &mut [i32] = &mut [1, 2, 3]; +let x: &mut [i32] = &mut [1, 2, 3]; let c = || match x { // does not capture `x` [..] => println!("always matches"), };