Skip to content

Document how closure capturing interacts with discriminant reads #1837

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 117 additions & 2 deletions src/types/closure.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -264,6 +267,118 @@ 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`.
Copy link
Member

@Nadrieril Nadrieril May 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless we mutably access the contents, right? E.g.

let c = || match x {
    (Example::A(ref mut _y), _) => println!("variant A"),
    (Example::B(_), _) => println!("variant B"),
};

Is that already implied by the other rules?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok yep, that's the "Shared prefix" thing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup. Do you think this deserves a clarification? (if so, how would you word it)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, I think it's clear in the context


```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.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

Expand Down
Loading