From 1de30e8499d1e7395d3637ab48515783894203e2 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 01/27] 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 dfcb4fe44..a2515794e 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 462254d73244a83f50dd2381522a6d6736a2da1a 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 02/27] 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 a2515794e..0fbfbf4d8 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 28a209fee4036be3f856222374378701e9e17b1f 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 03/27] 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 0fbfbf4d8..645b4d212 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"), }; From 5785fd114c43ba448b99b4d07cac912284a2c3f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maja=20K=C4=85dzio=C5=82ka?= Date: Wed, 23 Jul 2025 17:46:20 +0200 Subject: [PATCH 04/27] Clarify what gets read by slice patterns --- src/types/closure.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/closure.md b/src/types/closure.md index 645b4d212..c0db9da99 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -340,7 +340,7 @@ c(); ``` r[type.closure.capture.precision.discriminants.slice-patterns] -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`. +Matching against a [slice pattern][patterns.slice] that needs to inspect the length of the scrutinee performs a read of the pointer value in order to fetch the length. The read will cause the closure to borrow the relevant place by `ImmBorrow`. ```rust,compile_fail,E0506 let x: &mut [i32] = &mut [1, 2, 3]; @@ -352,7 +352,7 @@ x[0] += 1; // ERROR: cannot assign to `x[_]` because it is borrowed c(); ``` -As such, matching against an array doesn't itself cause any borrows, as the lengthh is fixed and doesn't need to be read. +As such, matching against an array doesn't itself cause any borrows, as the lengthh is fixed and the pattern doesn't need to inspect it. ```rust let mut x: [i32; 3] = [1, 2, 3]; From 0673c0d5fa57fd54568a2d5a9b9efb890eacf33a Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Mon, 13 Oct 2025 21:21:40 +0000 Subject: [PATCH 05/27] Revise text about capture precision To the language about a capture path, this branch adds, "..., as well as any further projections performed by matching against patterns", and it links to the section on capturing and wildcard patterns. The link is a bit unobvious, since the section discusses what's not captured. The "as well as" phrasing is a bit awkward, and it's introduced after a comma. That indicates a non-restrictive clause, which this is not. The next rule talks about what place projections are. It seems that it might be better to instead extend that language than to try to fit this in here. We can say that destructuring can effect a place projection, and that seems right. Let's do that. --- src/types/closure.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/types/closure.md b/src/types/closure.md index c0db9da99..13cd146df 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -98,12 +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, as well as any [further projections performed by matching against patterns][pattern-wildcards]. - -[pattern-wildcards]: type.closure.capture.precision.wildcard +A *capture path* is a sequence starting with a variable from the environment followed by any number of place projections from that variable. 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. +A *place projection* is a [field access], [tuple index], [dereference] (and automatic dereferences), [array or slice index] expression, or [pattern destructuring] applied to a variable. r[type.closure.capture.precision.intro] The closure borrows or moves the capture path, which may be truncated based on the rules described below. @@ -126,6 +124,7 @@ Here the capture path is the local variable `s`, followed by a field access `.f1 This closure captures an immutable borrow of `s.f1.1`. [field access]: ../expressions/field-expr.md +[pattern destructuring]: patterns.destructure [tuple index]: ../expressions/tuple-expr.md#tuple-indexing-expressions [dereference]: ../expressions/operator-expr.md#the-dereference-operator [array or slice index]: ../expressions/array-expr.md#array-and-slice-indexing-expressions From 4bef10cfa7aab5cfb56f5e78bd13204dcf3d80c0 Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Mon, 13 Oct 2025 21:29:21 +0000 Subject: [PATCH 06/27] Revise intro on wildcard patterns and capturing Let's tighten up the wording a bit in the introduction to wildcard patterns and capturing to make it better match how we'll present things below. We'll remove the "for example..." ahead of the example, as we're not doing this below, and we'll separately extend the comments within the examples to include the relevant information. --- src/types/closure.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/types/closure.md b/src/types/closure.md index 13cd146df..0b686b363 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -186,9 +186,8 @@ If this were to capture `m`, then the closure would no longer outlive `'static`, r[type.closure.capture.precision.wildcard] ### Wildcard pattern bindings -Closures only capture data that needs to be read. -Binding a value with a [wildcard pattern] does not count as a read, and thus won't be captured. -For example, the following closures will not capture `x`: +r[type.closure.capture.precision.wildcard.reads] +Closures only capture data that needs to be read. Binding a value with a [wildcard pattern] does not read the value, so the place is not captured. ```rust let x = String::from("hello"); From c93b00518d69bb74739e9c516155e3272256c8b0 Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Mon, 13 Oct 2025 23:31:10 +0000 Subject: [PATCH 07/27] Add example of what's not captured w.r.t. destructuring In this branch, we're adding a statement in the wildcard section about how destructuring tuples, structs, and single-variant enums, by itself, does not constitute a read. Let's add some examples of this to demonstrate. --- src/types/closure.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/types/closure.md b/src/types/closure.md index 0b686b363..7b11f6796 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -203,6 +203,46 @@ c(); ``` This also includes destructuring of tuples, structs, and single-variant enums. + +```rust,no_run +struct S; // A non-`Copy` type. + +// Destructuring tuples does not cause a read or capture. +let x = (S,); +let c = || { + let (..) = x; // Does not capture `x`. +}; +x; // OK: `x` can be moved here. +c(); + +// Destructuring unit structs does not cause a read or capture. +let x = S; +let c = || { + let S = x; // Does not capture `x`. +}; +x; // OK: `x` can be moved here. +c(); + +// Destructuring structs does not cause a read or capture. +struct W(T); +let x = W(S); +let c = || { + let W(..) = x; // Does not capture `x`. +}; +x; // OK: `x` can be moved here. +c(); + +// Destructuring single-variant enums does not cause a read +// or capture. +enum E { V(T) } +let x = E::V(S); +let c = || { + let E::V(..) = x; // Does not capture `x`. +}; +x; // OK: `x` can be moved here. +c(); +``` + 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: From e811ba2448e7488ba580bbc5bca83a1d03d671d7 Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Mon, 13 Oct 2025 23:38:13 +0000 Subject: [PATCH 08/27] Make statement more self-standing Rather than saying "this also includes", let's make this statement stand better on its own. This will be helpful when giving it a rule identifier, and given how this statement is some distance, visually, from the other statements due to the examples, it adds clarity. --- src/types/closure.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/closure.md b/src/types/closure.md index 7b11f6796..9db4f6372 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -202,7 +202,7 @@ let c = || match x { // x is not captured c(); ``` -This also includes destructuring of tuples, structs, and single-variant enums. +Destructuring tuples, structs, and single-variant enums does not, by itself, cause a read or the place to be captured. ```rust,no_run struct S; // A non-`Copy` type. From a9a9c9c0144c50840fbae503d1f370376efb6262 Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Mon, 13 Oct 2025 23:40:06 +0000 Subject: [PATCH 09/27] Add rule identifier to destructuring/capturing claim Let's give the claim about the effect of destructuring with respect to reads and capturing a rule identifier. --- src/types/closure.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/closure.md b/src/types/closure.md index 9db4f6372..dae346766 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -202,6 +202,7 @@ let c = || match x { // x is not captured c(); ``` +r[type.closure.capture.precision.wildcard.destructuring] Destructuring tuples, structs, and single-variant enums does not, by itself, cause a read or the place to be captured. ```rust,no_run From 165ed0b81ad12b550b795211cc19605b44a08882 Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Mon, 13 Oct 2025 23:42:21 +0000 Subject: [PATCH 10/27] Fix `non_exhaustive` rule identifier fragment When a fragment of a rule identifier matches a Rust identifier, we use an underscore rather than a dash as a separator. Let's do that here. --- src/types/closure.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/closure.md b/src/types/closure.md index dae346766..8f0cc1048 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -342,7 +342,7 @@ x = Example::A(57); // `x` can be modified while the closure is live c(); ``` -r[type.closure.capture.precision.discriminants.non-exhaustive] +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 From 0f6c02d8844aaa9552d266697e11b0034ba9bde9 Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Mon, 13 Oct 2025 23:43:58 +0000 Subject: [PATCH 11/27] Add forward reference for `non_exhaustive` exception The normative rule about destructuring and capturing does not itself state the exception for `non_exhaustive` enums. That exception follows below. Let's add an admonition to link down to it so it's not missed. It's not great having this duplication; it'd be better if we weren't restating this fact twice. But it's not clear what to do about this. The first statement is making a broader statement about tuples, structs, and single-variant enums. The section that follows below is focused on discriminants. We'll think about this more later. --- src/types/closure.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/types/closure.md b/src/types/closure.md index 8f0cc1048..1cbbc4ee8 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -205,6 +205,9 @@ c(); r[type.closure.capture.precision.wildcard.destructuring] Destructuring tuples, structs, and single-variant enums does not, by itself, cause a read or the place to be captured. +> [!NOTE] +> Enums marked with [`#[non_exhaustive]`][attributes.type-system.non_exhaustive] from other crates are always treated as having multiple variants. See *[type.closure.capture.precision.discriminants.non_exhaustive]*. + ```rust,no_run struct S; // A non-`Copy` type. From eab4a34c9c34d2a240b6d1e1f355b0d8e4d1db3b Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Mon, 13 Oct 2025 23:48:24 +0000 Subject: [PATCH 12/27] Revise text about field matching and `..` Let's clean up the wording of this claim a bit. Let's put capturing in the present tense, rather than in the future tense, as that's how we're going to continue below. Let's take out, as well, the explicit lead-in to the example, as we're not generally doing that in these sections. --- src/types/closure.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/types/closure.md b/src/types/closure.md index 1cbbc4ee8..0a3cec1f6 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -247,8 +247,7 @@ x; // OK: `x` can be moved here. c(); ``` -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: +Fields matched against [RestPattern] (`..`) or [StructPatternEtCetera] (also `..`) are not read, and those fields are not captured. ```rust let x = (String::from("a"), String::from("b")); From 1b46086b6247711f26e1f83e51744bb3fb2c258a Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Mon, 13 Oct 2025 23:50:54 +0000 Subject: [PATCH 13/27] Add identifier for wildcard fields capturing rule Let's add a rule identifier for the rule about wildcards and the capturing of fields. --- src/types/closure.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/closure.md b/src/types/closure.md index 0a3cec1f6..b4ab39ad7 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -247,6 +247,7 @@ x; // OK: `x` can be moved here. c(); ``` +r[type.closure.capture.precision.wildcard.fields] Fields matched against [RestPattern] (`..`) or [StructPatternEtCetera] (also `..`) are not read, and those fields are not captured. ```rust From 54fde19776586e3ab9dbc7802d555998224eef4a Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Mon, 13 Oct 2025 23:53:09 +0000 Subject: [PATCH 14/27] Remove example lead-in from array slice capturing rule Let's remove the explicit lead-in to the example from the rule about (the lack of) partial captures of arrays and slices. --- src/types/closure.md | 1 - 1 file changed, 1 deletion(-) diff --git a/src/types/closure.md b/src/types/closure.md index b4ab39ad7..fc7c21ee1 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -282,7 +282,6 @@ c(); r[type.closure.capture.precision.wildcard.array-slice] Partial captures of arrays and slices are not supported; the entire slice or array is always captured even if used with wildcard pattern matching, indexing, or sub-slicing. -For example: ```rust,compile_fail,E0382 #[derive(Debug)] From d07e35b9ab145166ef36098c01a1b7cbf37537e5 Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Mon, 13 Oct 2025 23:58:24 +0000 Subject: [PATCH 15/27] Remove redundant example on structs and capturing What this example is showing is how one field of a struct can be captured while another is not (when using rest patterns). However, the example above demonstrates this with tuples, and there are no interesting differences between tuple fields and struct fields in this case. --- src/types/closure.md | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/types/closure.md b/src/types/closure.md index fc7c21ee1..881b9cf05 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -261,25 +261,6 @@ println!("{:?}", x.1); c(); ``` -```rust -struct Example { - f1: String, - f2: String, -} - -let e = Example { - f1: String::from("first"), - f2: String::from("second"), -}; -let c = || { - let Example { f2, .. } = e; // captures `e.f2` ByValue -}; -// Field f2 cannot be accessed since it is moved into the closure. -// Field f1 is still accessible. -println!("{:?}", e.f1); -c(); -``` - r[type.closure.capture.precision.wildcard.array-slice] Partial captures of arrays and slices are not supported; the entire slice or array is always captured even if used with wildcard pattern matching, indexing, or sub-slicing. From 8150ab9b36d61f1175b351d7a18d83addc483316 Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Tue, 14 Oct 2025 00:02:07 +0000 Subject: [PATCH 16/27] Improve examples in wildcard capturing section Some of the examples in this section did not really prove the claim they were making; let's adjust them to do this. Let's also simplify and minimize them to match the style of other recent examples we're adding and that of the ones that will follow in the section below. One particular stylistic conundrum is what to do about comments like this: // ERROR: Borrow of moved value. Normally, our stylistic convention would be to capitalize after the comma in this case and end with a period. That's what we'd do, e.g., in a similar case like this: // OK: The value can be moved here. But, of course, `rustc` doesn't capitalize and add a period to these kind of error messages, making it tempting to follow that lead. Since we already don't always use the same error messages that `rustc` does -- it's not a goal to match those -- it seems better to be internally consistent with our own documentation norms. Let's capitalize and add the period. We'll later add this to the style guide and work to align the document with this. --- src/types/closure.md | 41 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/src/types/closure.md b/src/types/closure.md index 881b9cf05..690dafd32 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -189,16 +189,16 @@ r[type.closure.capture.precision.wildcard] r[type.closure.capture.precision.wildcard.reads] Closures only capture data that needs to be read. Binding a value with a [wildcard pattern] does not read the value, so the place is not captured. -```rust -let x = String::from("hello"); +```rust,no_run +struct S; // A non-`Copy` type. +let x = S; let c = || { - let _ = x; // x is not captured + let _ = x; // Does not capture `x`. }; -c(); - -let c = || match x { // x is not captured - _ => println!("Hello World!") +let c = || match x { + _ => (), // Does not capture `x`. }; +x; // OK: `x` can be moved here. c(); ``` @@ -250,14 +250,14 @@ c(); r[type.closure.capture.precision.wildcard.fields] Fields matched against [RestPattern] (`..`) or [StructPatternEtCetera] (also `..`) are not read, and those fields are not captured. -```rust -let x = (String::from("a"), String::from("b")); +```rust,no_run +struct S; // A non-`Copy` type. +let x = (S, S); let c = || { - let (first, ..) = x; // captures `x.0` ByValue + let (x0, ..) = x; // Captures `x.0` by `ByValue`. }; -// The first tuple field has been moved into the closure. -// The second tuple field is still accessible. -println!("{:?}", x.1); +// Only the first tuple field was captured by the closure. +x.1; // OK: `x.1` can be moved here. c(); ``` @@ -265,24 +265,21 @@ r[type.closure.capture.precision.wildcard.array-slice] Partial captures of arrays and slices are not supported; the entire slice or array is always captured even if used with wildcard pattern matching, indexing, or sub-slicing. ```rust,compile_fail,E0382 -#[derive(Debug)] -struct Example; -let x = [Example, Example]; - +struct S; // A non-`Copy` type. +let mut x = [S, S]; let c = || { - let [first, _] = x; // captures all of `x` ByValue + let [x0, _] = x; // Captures all of `x` by `ByValue`. }; -c(); -println!("{:?}", x[1]); // ERROR: borrow of moved value: `x` +let _ = &mut x[1]; // ERROR: Borrow of moved value. ``` r[type.closure.capture.precision.wildcard.initialized] Values that are matched with wildcards must still be initialized. ```rust,compile_fail,E0381 -let x: i32; +let x: u8; let c = || { - let _ = x; // ERROR: used binding `x` isn't initialized + let _ = x; // ERROR: Binding `x` isn't initialized. }; ``` From efabfe2a074edb473ae4e9fd1ef986797894ee7c Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Tue, 14 Oct 2025 00:12:01 +0000 Subject: [PATCH 17/27] Clarify main discriminant reads rule The rule, > If pattern matching requires inspecting a discriminant, the relevant > place will get captured by `ImmBorrow`. is unclear in two ways. For one, when does pattern matching "require inspecting" the discriminant? Second, what is the "relevant" place? Let's break this up into two rules (and add rule identifiers). The first rule will state that reading the discriminant captures the place containing the discriminant (by `ImmBorrow`). The second rule will state explicitly the baseline rule for when such reads happen. --- src/types/closure.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/types/closure.md b/src/types/closure.md index 690dafd32..f6d4b4d94 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -288,7 +288,11 @@ let c = || { 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`. +r[type.closure.capture.precision.discriminants.reads] +If pattern matching reads a discriminant, the place containing that discriminant is captured by `ImmBorrow`. + +r[type.closure.capture.precision.discriminants.multiple-variant] +Matching against a variant of an enum that has more than one variant reads the discriminant, capturing the place by `ImmBorrow`. ```rust enum Example { From 3e96d82a506be2d3050c8654bfa96a3804da2fa7 Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Tue, 14 Oct 2025 00:20:15 +0000 Subject: [PATCH 18/27] Revise examples for main discriminant reads rule The example here showed what isn't captured, but it didn't demonstrate what is captured. Let's show both. We'll specifically use a non-`Copy` type for this because [Rust PR #138961] affects the behavior in the non-`Copy` case. [Rust PR #138961]: https://github.com/rust-lang/rust/pull/138961 --- src/types/closure.md | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/types/closure.md b/src/types/closure.md index f6d4b4d94..8d3dce0a5 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -294,19 +294,33 @@ If pattern matching reads a discriminant, the place containing that discriminant r[type.closure.capture.precision.discriminants.multiple-variant] Matching against a variant of an enum that has more than one variant reads the discriminant, capturing the place by `ImmBorrow`. -```rust -enum Example { - A(i32), - B(i32), -} - -let mut x = (Example::A(21), 37); +```rust,compile_fail,E0502 +struct S; // A non-`Copy` type. +let mut x = (Some(S), S); +let c = || match x { + (None, _) => (), +// ^^^^ +// This pattern requires reading the discriminant, which +// causes `x.0` to be captured by `ImmBorrow`. + _ => (), +}; +let _ = &mut x.0; // ERROR: Cannot borrow `x.0` as mutable. +// ^^^ +// The closure is still live, so `x.0` is still immutably +// borrowed here. +c(); +``` -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 +```rust,no_run +# struct S; // A non-`Copy` type. +# let x = (Some(S), S); +let c = || match x { // Captures `x.0` by `ImmBorrow`. + (None, _) => (), + _ => (), +}; +// Though `x.0` is captured due to the discriminant read, +// `x.1` is not captured. +x.1; // OK: `x.1` can be moved here. c(); ``` From a052ea52a5a80d99829e587fff6fceb31df940ed Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Tue, 14 Oct 2025 00:24:39 +0000 Subject: [PATCH 19/27] Revise `...discriminants.single-variant` rule Let's simplify the wording of the rule a bit, reiterate the effect of the rule (as we're doing elsewhere), and tighten up the example. --- src/types/closure.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/types/closure.md b/src/types/closure.md index 8d3dce0a5..626acc8f9 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -325,18 +325,15 @@ c(); ``` r[type.closure.capture.precision.discriminants.single-variant] -Matching against the only variant of an enum does not constitute a discriminant read. +Matching against the only variant of a single-variant enum does not read the discriminant and does not capture the place. -```rust -enum Example { - A(i32), -} - -let mut x = Example::A(42); +```rust,no_run +enum E { V(T) } // A single-variant enum. +let x = E::V(()); let c = || { - let Example::A(_) = x; // does not capture `x` + let E::V(_) = x; // Does not capture `x`. }; -x = Example::A(57); // `x` can be modified while the closure is live +x; // OK: `x` can be moved here. c(); ``` From c404d53a0cd889e51ee5c72394cb9c4729cadeb5 Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Tue, 14 Oct 2025 00:26:48 +0000 Subject: [PATCH 20/27] Revise `...discriminants.non_exhaustive` rule Let's clarify some wording in this rule, making it a bit more self-standing, and inline the link target as, when using the rule identifier for this, it's not meaningfully longer than doing it the other way. --- src/types/closure.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/types/closure.md b/src/types/closure.md index 626acc8f9..9004d5d6b 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -338,9 +338,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. - -[non_exhaustive]: attributes.type-system.non_exhaustive +If [`#[non_exhaustive]`][attributes.type-system.non_exhaustive] is applied to an enum defined in an external crate, the enum is treated as having multiple variants for the purpose of deciding whether a read occurs, even if it actually has only one variant. r[type.closure.capture.precision.discriminants.uninhabited-variant] Even if all other variants are uninhabited, the discriminant read still occurs. From 28ae30cca18f53c35405d9de841e50259637ea93 Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Tue, 14 Oct 2025 00:34:11 +0000 Subject: [PATCH 21/27] Revise `...discriminants.uninhabited-variants` rule Let's rename this rule identifier to the plural; we generally use the plural whenever it can make sense, and it does here. Let's make the wording of the rule more clear and self-standing. And let's tighten up the example. --- src/types/closure.md | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/types/closure.md b/src/types/closure.md index 9004d5d6b..90e127b1a 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -340,22 +340,16 @@ c(); r[type.closure.capture.precision.discriminants.non_exhaustive] If [`#[non_exhaustive]`][attributes.type-system.non_exhaustive] is applied to an enum defined in an external crate, the enum is treated as having multiple variants for the purpose of deciding whether a read occurs, even if it actually has only one variant. -r[type.closure.capture.precision.discriminants.uninhabited-variant] -Even if all other variants are uninhabited, the discriminant read still occurs. +r[type.closure.capture.precision.discriminants.uninhabited-variants] +Even if all variants but the one being matched against are uninhabited, making the pattern [irrefutable][patterns.refutable], the discriminant is still read if it otherwise would be. -```rust,compile_fail,E0506 -enum Void {} - -enum Example { - A(i32), - B(Void), -} - -let mut x = Example::A(42); +```rust,compile_fail,E0502 +enum Empty {} +let mut x = Ok::<_, Empty>(42); let c = || { - let Example::A(_) = x; // captures `x` by ImmBorrow + let Ok(_) = x; // Captures `x` by `ImmBorrow`. }; -x = Example::A(57); // ERROR: cannot assign to `x` because it is borrowed +let _ = &mut x; // ERROR: Cannot borrow `x` as mutable. c(); ``` From 78de4d7c4526135a640df58478635452c75cb1e8 Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Tue, 14 Oct 2025 00:56:11 +0000 Subject: [PATCH 22/27] Revise `...discriminants.range-patterns` rule Let's tighten up the text for this rule and condense it down into one sentence. It's a bit awkward to have another sentence just to say, "this is true even if...". Let's clean up the example as well. --- src/types/closure.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/types/closure.md b/src/types/closure.md index 90e127b1a..41bf238b9 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -354,14 +354,14 @@ c(); ``` r[type.closure.capture.precision.discriminants.range-patterns] -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. +Matching against a [range pattern][patterns.range] reads the place being matched, even if the range includes all possible values of the type, and captures the place by `ImmBorrow`. -```rust,compile_fail,E0506 -let mut x = 7_u8; +```rust,compile_fail,E0502 +let mut x = 0u8; let c = || { - let 0..=u8::MAX = x; // captures `x` by ImmBorrow + let 0..=u8::MAX = x; // Captures `x` by `ImmBorrow`. }; -x += 1; // ERROR: cannot assign to `x` because it is borrowed +let _ = &mut x; // ERROR: Cannot borrow `x` as mutable. c(); ``` From 8ec79cbfcf37d19d24859315e322733c3e489cc3 Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Tue, 14 Oct 2025 01:00:27 +0000 Subject: [PATCH 23/27] Revise `...discriminants.slice-patterns-*` rules The section here stated: > Matching against a slice pattern that needs to inspect the length of > the scrutinee performs a read of the pointer value in order to fetch > the length. The read will cause the closure to borrow the relevant > place by `ImmBorrow`. It then goes on to state exceptions for arrays matched against slice patterns and slice patterns containing only a rest pattern. As we saw in an earlier commit, it's better to not lean on the reader to infer the "relevant place" or when the length needs to be inspected. Let's elaborate those details and state the full rule in one go, upfront, and then state a separate guarantee that matching an array against a slice pattern does not do a read. We'll also fix a typo, add rule identifiers, and tighten up the examples. --- src/types/closure.md | 46 ++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/src/types/closure.md b/src/types/closure.md index 41bf238b9..0340c5a0d 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -365,38 +365,42 @@ let _ = &mut x; // ERROR: Cannot borrow `x` as mutable. c(); ``` -r[type.closure.capture.precision.discriminants.slice-patterns] -Matching against a [slice pattern][patterns.slice] that needs to inspect the length of the scrutinee performs a read of the pointer value in order to fetch the length. The read will cause the closure to borrow the relevant place by `ImmBorrow`. +r[type.closure.capture.precision.discriminants.slice-patterns-slices] +Matching a slice against a [slice pattern][patterns.slice] other than one with only a single [rest pattern][patterns.rest] (i.e. `[..]`) reads the length from the wide pointer and captures the pointed-to place by `ImmBorrow`. -```rust,compile_fail,E0506 -let x: &mut [i32] = &mut [1, 2, 3]; -let c = || match x { // captures `*x` by ImmBorrow - [_, _, _] => println!("three elements"), - _ => println!("something else"), +```rust,compile_fail,E0502 +let x: &mut [u8] = &mut []; +let c = || match x { // Captures `*x` by `ImmBorrow`. + [] => (), +// ^^ +// This matches a slice of exactly zero elements. To know whether the +// scrutinee matches, the length must be read from the wide pointer, +// causing the pointee to be captured. + _ => (), }; -x[0] += 1; // ERROR: cannot assign to `x[_]` because it is borrowed +let _ = &mut *x; // ERROR: Cannot borrow `*x` as mutable. c(); ``` -As such, matching against an array doesn't itself cause any borrows, as the lengthh is fixed and the pattern doesn't need to inspect it. - -```rust -let mut x: [i32; 3] = [1, 2, 3]; -let c = || match x { // does not capture `x` - [_, _, _] => println!("three elements, obviously"), +```rust,no_run +let x: &mut [u8] = &mut []; +let c = || match x { // Does not capture `*x`. + [..] => (), +// ^^ Rest pattern. }; -x[0] += 1; // `x` can be modified while the closure is live +let _ = &mut *x; // OK: `*x` can be borrow here. c(); ``` -Likewise, a slice pattern that matches slices of all possible lengths does not constitute a read. +r[type.closure.capture.precision.discriminants.slice-patterns-arrays] +As the length of an array is fixed by its type, matching an array against a slice pattern does not by itself capture the place. -```rust -let x: &mut [i32] = &mut [1, 2, 3]; -let c = || match x { // does not capture `x` - [..] => println!("always matches"), +```rust,no_run +let x: [u8; 1] = [0]; +let c = || match x { // Does not capture `x`. + [_] => (), // Length is fixed. }; -x[0] += 1; // `x` can be modified while the closure is live +x; // OK: `x` can be moved here. c(); ``` From 4bb220f9725508033ee23a5b2aa04dbb6e5da1ea Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Tue, 14 Oct 2025 22:05:43 +0000 Subject: [PATCH 24/27] Separate sections for range/slice pattern capturing The rules for the capturing behavior related to range patterns and slice patterns were comingled with the rules for capturing and discriminant reads. While these are spiritually related, they are distinct, so let's break these apart into separate sections. --- src/types/closure.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/types/closure.md b/src/types/closure.md index 0340c5a0d..b48213e18 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -353,7 +353,11 @@ let _ = &mut x; // ERROR: Cannot borrow `x` as mutable. c(); ``` -r[type.closure.capture.precision.discriminants.range-patterns] + +r[type.closure.capture.precision.range-patterns] +### Capturing and range patterns + +r[type.closure.capture.precision.range-patterns.reads] Matching against a [range pattern][patterns.range] reads the place being matched, even if the range includes all possible values of the type, and captures the place by `ImmBorrow`. ```rust,compile_fail,E0502 @@ -365,7 +369,10 @@ let _ = &mut x; // ERROR: Cannot borrow `x` as mutable. c(); ``` -r[type.closure.capture.precision.discriminants.slice-patterns-slices] +r[type.closure.capture.precision.slice-patterns] +### Capturing and slice patterns + +r[type.closure.capture.precision.slice-patterns.slices] Matching a slice against a [slice pattern][patterns.slice] other than one with only a single [rest pattern][patterns.rest] (i.e. `[..]`) reads the length from the wide pointer and captures the pointed-to place by `ImmBorrow`. ```rust,compile_fail,E0502 @@ -392,7 +399,7 @@ let _ = &mut *x; // OK: `*x` can be borrow here. c(); ``` -r[type.closure.capture.precision.discriminants.slice-patterns-arrays] +r[type.closure.capture.precision.slice-patterns.arrays] As the length of an array is fixed by its type, matching an array against a slice pattern does not by itself capture the place. ```rust,no_run From 1c9c88bdb76f6084b6a5a4e935fd14e80774dc3a Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Tue, 14 Oct 2025 22:57:50 +0000 Subject: [PATCH 25/27] Add admonition about the desugaring of destructuring For our purposes in describing capture paths, we define place projections. For the text that follows in the sections below to hold, we need destructuring to be treated as a place projection for this purpose. However, it might seem surprising for destructuring to be in this list. While it is believed to have the same effect as a projection expression, we might not consider an instance of pattern destructuring to be a projection expression exactly. To better contextualize this, let's add an admonition that mentions that pattern destructuring desugars into the kind of field accesses that we would more likely think about as projection operations. --- src/types/closure.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/types/closure.md b/src/types/closure.md index b48213e18..90f9c7c5a 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -103,6 +103,9 @@ A *capture path* is a sequence starting with a variable from the environment fol r[type.closure.capture.precision.place-projection] A *place projection* is a [field access], [tuple index], [dereference] (and automatic dereferences), [array or slice index] expression, or [pattern destructuring] applied to a variable. +> [!NOTE] +> In `rustc`, pattern destructuring desugars into a series of dereferences and field or element accesses. + r[type.closure.capture.precision.intro] The closure borrows or moves the capture path, which may be truncated based on the rules described below. From 618eb3284972e88c12976f4a2496d89a25939c9b Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Tue, 14 Oct 2025 23:39:41 +0000 Subject: [PATCH 26/27] Add admonition about pointer vs pointee capturing There's some surprising nuance to what gets captured when reading the length of a slice with a slice pattern. Let's add an admonition about this. --- src/types/closure.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/types/closure.md b/src/types/closure.md index 90f9c7c5a..ae1b84b5c 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -378,6 +378,11 @@ r[type.closure.capture.precision.slice-patterns] r[type.closure.capture.precision.slice-patterns.slices] Matching a slice against a [slice pattern][patterns.slice] other than one with only a single [rest pattern][patterns.rest] (i.e. `[..]`) reads the length from the wide pointer and captures the pointed-to place by `ImmBorrow`. +> [!NOTE] +> Perhaps surprisingly, even though we read the length from the (wide) *pointer*, it is currently the place of the *pointee* that is captured. However, due to an optimization that reduces the precision of captures, often in practice `rustc` does capture the pointer instead. +> +> For details, see the doc comment on [`truncate_capture_for_optimization`](https://doc.rust-lang.org/1.90.0/nightly-rustc/rustc_hir_typeck/upvar/fn.truncate_capture_for_optimization.html). + ```rust,compile_fail,E0502 let x: &mut [u8] = &mut []; let c = || match x { // Captures `*x` by `ImmBorrow`. From 83396827618f29dee9ac46d2f2e43f76c0f20316 Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Tue, 14 Oct 2025 23:42:44 +0000 Subject: [PATCH 27/27] Fix preexisting typo on "ancestors" While we're here, let's fix a typo on the word "ancestors" in this section. --- src/types/closure.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/closure.md b/src/types/closure.md index ae1b84b5c..40964f36d 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -135,7 +135,7 @@ This closure captures an immutable borrow of `s.f1.1`. r[type.closure.capture.precision.shared-prefix] ### Shared prefix -In the case where a capture path and one of the ancestor’s of that path are both captured by a closure, the ancestor path is captured with the highest capture mode among the two captures, `CaptureMode = max(AncestorCaptureMode, DescendantCaptureMode)`, using the strict weak ordering: +In the case where a capture path and one of the ancestors of that path are both captured by a closure, the ancestor path is captured with the highest capture mode among the two captures, `CaptureMode = max(AncestorCaptureMode, DescendantCaptureMode)`, using the strict weak ordering: `ImmBorrow < UniqueImmBorrow < MutBorrow < ByValue`