Skip to content
Open
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1de30e8
Document how closure capturing interacts with discriminant reads
meithecatte May 30, 2025
462254d
Document how range and slice patterns can constitute discriminant reads
meithecatte May 30, 2025
28a209f
Don't call things "discriminant reads" just because they behave like …
meithecatte Jul 11, 2025
5785fd1
Clarify what gets read by slice patterns
meithecatte Jul 23, 2025
ba36ec9
Revise text about capture precision
traviscross Oct 13, 2025
c9483b0
Revise intro on wildcard patterns and capturing
traviscross Oct 13, 2025
a294512
Add example of what's not captured w.r.t. destructuring
traviscross Oct 13, 2025
851d723
Make statement more self-standing
traviscross Oct 13, 2025
c1ecf00
Add rule identifier to destructuring/capturing claim
traviscross Oct 13, 2025
e5f4c8b
Fix `non_exhaustive` rule identifier fragment
traviscross Oct 13, 2025
a5eb504
Add forward reference for `non_exhaustive` exception
traviscross Oct 13, 2025
6337498
Revise text about field matching and `..`
traviscross Oct 13, 2025
8ce5539
Add identifier for wildcard fields capturing rule
traviscross Oct 13, 2025
2a42ae1
Remove example lead-in from array slice capturing rule
traviscross Oct 13, 2025
79cab22
Remove redundant example on structs and capturing
traviscross Oct 13, 2025
211d2a4
Improve examples in wildcard capturing section
traviscross Oct 14, 2025
15302fa
Clarify main discriminant reads rule
traviscross Oct 14, 2025
bf143cf
Revise examples for main discriminant reads rule
traviscross Oct 14, 2025
2e84d05
Revise `...discriminants.single-variant` rule
traviscross Oct 14, 2025
680f474
Revise `...discriminants.non_exhaustive` rule
traviscross Oct 14, 2025
49f64ad
Revise `...discriminants.uninhabited-variants` rule
traviscross Oct 14, 2025
a0e3a3c
Revise `...discriminants.range-patterns` rule
traviscross Oct 14, 2025
508f005
Revise `...discriminants.slice-patterns-*` rules
traviscross Oct 14, 2025
962e72e
Separate sections for range/slice pattern capturing
traviscross Oct 14, 2025
d2db8ef
Add admonition about the desugaring of destructuring
traviscross Oct 14, 2025
cb3a364
Add admonition about pointer vs pointee capturing
traviscross Oct 14, 2025
db2a9f8
Fix preexisting typo on "ancestors"
traviscross Oct 14, 2025
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
266 changes: 222 additions & 44 deletions src/types/closure.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,13 @@ 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 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.

> [!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.
Expand All @@ -124,14 +127,15 @@ 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

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`

Expand Down Expand Up @@ -185,85 +189,259 @@ 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");
```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`.
};
let c = || match x {
_ => (), // Does not capture `x`.
};
x; // OK: `x` can be moved here.
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.

let c = || match x { // x is not captured
_ => println!("Hello World!")
> [!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.

// 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();
```

This also includes destructuring of tuples, structs, and 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:
// 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();

```rust
let x = (String::from("a"), String::from("b"));
// Destructuring structs does not cause a read or capture.
struct W<T>(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<T> { V(T) }
let x = E::V(S);
let c = || {
let (first, ..) = x; // captures `x.0` ByValue
let E::V(..) = x; // Does not capture `x`.
};
// The first tuple field has been moved into the closure.
// The second tuple field is still accessible.
println!("{:?}", x.1);
x; // OK: `x` can be moved here.
c();
```

```rust
struct Example {
f1: String,
f2: String,
}
r[type.closure.capture.precision.wildcard.fields]
Fields matched against [RestPattern] (`..`) or [StructPatternEtCetera] (also `..`) are not read, and those fields are not captured.

let e = Example {
f1: String::from("first"),
f2: String::from("second"),
};
```rust,no_run
struct S; // A non-`Copy` type.
let x = (S, S);
let c = || {
let Example { f2, .. } = e; // captures `e.f2` ByValue
let (x0, ..) = x; // Captures `x.0` by `ByValue`.
};
// Field f2 cannot be accessed since it is moved into the closure.
// Field f1 is still accessible.
println!("{:?}", e.f1);
// Only the first tuple field was captured by the closure.
x.1; // OK: `x.1` can be moved here.
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)]
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.
};
```

[wildcard pattern]: ../patterns.md#wildcard-pattern

r[type.closure.capture.precision.discriminants]
### Capturing for discriminant reads

r[type.closure.capture.precision.discriminants.reads]
If pattern matching reads a discriminant, the place containing that discriminant is captured by `ImmBorrow`.
Comment on lines +294 to +295
Copy link
Member

@Nadrieril Nadrieril Oct 20, 2025

Choose a reason for hiding this comment

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

Question: shouldn't this bit be enough for the section, and shouldn't the "which discriminants are read" be moved to a more appropriate place, like wherever we describe pattern-matching?

Copy link
Member

Choose a reason for hiding this comment

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

Even if we do that I'd keep the examples, as they're a really good way to observe what exactly is being read.

Copy link
Member

Choose a reason for hiding this comment

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

Having read things in more detail there's actually an open question here: we might want the static semantics and runtime semantics to differ. The case I have in mind is #[non_exhaustive] enums: for closure capture we should definitely pretend they have multiple variants, but I'm not sure we should emit the discriminant read in MIR (this recently tripped someone up).

Copy link
Member

Choose a reason for hiding this comment

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

More discussion is happening in rust-lang/rust#147722.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I'd had the same thoughts. I agree that it makes more sense, high level, for the dynamic semantics of patterns with respect to reads to be in the patterns chapter. At the same time, I think there'd be some risk of the rules then that affect the static semantics with respect to captures being somewhat spread out.

Given how tied in these reads are to the question of the static semantics described here, and given that for better or worse the Reference is more organized by static semantics currently, I'm OK with leaving this reorganization to future work.

Copy link
Member

Choose a reason for hiding this comment

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

Sounds good. I recall that the dynamic semantics of patterns needs quite some work anyway, that'll be its own endeavor I imagine.


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,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();
```

```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();
```

r[type.closure.capture.precision.discriminants.single-variant]
Matching against the only variant of a single-variant enum does not read the discriminant and does not capture the place.

```rust,no_run
enum E<T> { V(T) } // A single-variant enum.
let x = E::V(());
let c = || {
let E::V(_) = x; // Does not capture `x`.
};
x; // OK: `x` can be moved here.
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.
Comment on lines +343 to +344
Copy link
Contributor

Choose a reason for hiding this comment

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

Can adding/removing #[non_exhaustive] affect run time behavior in closure captures? See also rust-lang/rust#147722

Can removing #[non_exhaustive] cause code in another crate to stop compiling?


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,E0502
enum Empty {}
let mut x = Ok::<_, Empty>(42);
let c = || {
let Ok(_) = x; // Captures `x` by `ImmBorrow`.
};
let _ = &mut x; // ERROR: Cannot borrow `x` as mutable.
c();
```


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
let mut x = 0u8;
let c = || {
let 0..=u8::MAX = x; // Captures `x` by `ImmBorrow`.
};
let _ = &mut x; // ERROR: Cannot borrow `x` as mutable.
c();
```

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
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.
_ => (),
};
let _ = &mut *x; // ERROR: Cannot borrow `*x` as mutable.
c();
```

```rust,no_run
let x: &mut [u8] = &mut [];
let c = || match x { // Does not capture `*x`.
[..] => (),
// ^^ Rest pattern.
};
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.

```rust,no_run
let x: [u8; 1] = [0];
let c = || match x { // Does not capture `x`.
[_] => (), // Length is fixed.
};
x; // OK: `x` can be moved here.
c();
```

r[type.closure.capture.precision.move-dereference]
### Capturing references in move contexts

Expand Down
Loading