From b47f571fa6fcab241e5967af4be315de175e7822 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 dfcb4fe442..a2515794e5 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 0d3942bc5e29033aabe8bdfed7ad4147792dea96 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 a2515794e5..0fbfbf4d88 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 147fd57c6b164ac3bacf81bf1b37c1ce2627eff4 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 0fbfbf4d88..645b4d212f 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 69e5f803157d992885b96c578dff634d9d6b2ac0 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 645b4d212f..c0db9da992 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 7447e5a5c1bc5b61874d612b1e5a7c923e02df34 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 c0db9da992..1c3ad75eb5 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 zero or more 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 31ce0a305dede412f8fb1ec442dd9ab4024f6e37 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 1c3ad75eb5..b25428a195 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 d7a5f9c62a9891000f9c2eef3f82d8a0396edfff 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 b25428a195..a8e769b585 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 4e42d16671877d8c09bd7591e51a366d5dfb45fc 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 a8e769b585..2259ab9dc0 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 8f60bab84f485a1ccc4a3f147aa859f9ceddd121 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 2259ab9dc0..309b13b497 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 02ca96feaac93b5c32889ab671ee7b94ddfd3434 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 309b13b497..572deb71d4 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 a7b12d48290024f57835dc61e4f049560aafcc78 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 572deb71d4..f38f0d8f61 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 b8b4c94e41959a06f7028091c5ab33372d6bc59d 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 f38f0d8f61..47cebecce8 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 4e0e00a6005309924a05070c7907ab6b3c5159fa 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 47cebecce8..64f7702150 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 1390c45057912dd0a0080247081a5799badb59f0 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 64f7702150..db95c1bce4 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 2d79a1ef2fcf9b9334dc3b1a74343f70d362f06e 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 db95c1bce4..064089c3a3 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 16c7d9e4fd0bae34151ad2f7b2c1b8488fbb0a37 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 064089c3a3..5756885326 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 1eff192279f4811f15087eda25328daa6b0f193a 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 5756885326..244049fb58 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 64ac8dbf5ce7ae77adaf463e13a3e8abadfc74bb 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 244049fb58..49b3ed6a3a 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 c5f4a8a1a4a177f5fc6f3889d922f0ada4aab6b8 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 49b3ed6a3a..8350e2be34 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 aaf0e02b7e2de9113b777aef3e7aa1cd203d8c79 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 8350e2be34..5084fce3b0 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 7ad248fdb9781df4e64381595608f0f45b153998 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 5084fce3b0..b3707d91f6 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 a2877af7316dd2b3344ce3440349587731e0eba0 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 b3707d91f6..acced3f8e2 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 e0c96122040b9eb85590029fee04caefd7a72da2 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. In saying that the pointer value is read, this might suggest that the pointer is captured. Actually, though, it's the slice (the pointee) that is captured. Let's fix this. Beyond that, 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 acced3f8e2..ec953fa586 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. `[..]`) is treated as a read of the length from the slice and captures the slice 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`. + &mut [] => (), +// ^^ +// This matches a slice of exactly zero elements. To know whether the +// scrutinee matches, the length must be read, causing the slice 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 69e7dfecd533cf5412f9ae7783dbde7e63ab1c15 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 ec953fa586..fde54cb575 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. `[..]`) is treated as a read of the length from the slice and captures the slice 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 c84f25f9bafa30188bdae369edb33cc2219f1f16 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 fde54cb575..77e5c79412 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 bc6c3c4026b4129d8006c33bc50fc2be3eb1094f 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 | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/types/closure.md b/src/types/closure.md index 77e5c79412..48af435b05 100644 --- a/src/types/closure.md +++ b/src/types/closure.md @@ -402,6 +402,34 @@ let _ = &mut *x; // OK: `*x` can be borrow here. c(); ``` +> [!NOTE] +> Perhaps surprisingly, even though the length is contained in the (wide) *pointer* to the slice, it is the place of the *pointee* (the slice) that is treated as read and is captured. +> +> ```rust,no_run +> fn f<'l: 's, 's>(x: &'s mut &'l [u8]) -> impl Fn() + 'l { +> // The closure outlives `'l` because it captures `**x`. If +> // instead it captured `*x`, it would not live long enough +> // to satisfy the `impl Fn() + 'l` bound. +> || match *x { // Captures `**x` by `ImmBorrow`. +> &[] => (), +> _ => (), +> } +> } +> ``` +> +> In this way, the behavior is consistent with dereferencing to the slice in the scrutinee. +> +> ```rust,no_run +> fn f<'l: 's, 's>(x: &'s mut &'l [u8]) -> impl Fn() + 'l { +> || match **x { // Captures `**x` by `ImmBorrow`. +> [] => (), +> _ => (), +> } +> } +> ``` +> +> For details, see [Rust PR #138961](https://github.com/rust-lang/rust/pull/138961). + 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. From 010f9a8f5de5e13c5fb78f7ddfc0db34c6ad6a35 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 48af435b05..33fb09e92b 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`