Skip to content
Merged
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b47f571
Document how closure capturing interacts with discriminant reads
meithecatte May 30, 2025
0d3942b
Document how range and slice patterns can constitute discriminant reads
meithecatte May 30, 2025
147fd57
Don't call things "discriminant reads" just because they behave like …
meithecatte Jul 11, 2025
69e5f80
Clarify what gets read by slice patterns
meithecatte Jul 23, 2025
7447e5a
Revise text about capture precision
traviscross Oct 13, 2025
31ce0a3
Revise intro on wildcard patterns and capturing
traviscross Oct 13, 2025
d7a5f9c
Add example of what's not captured w.r.t. destructuring
traviscross Oct 13, 2025
4e42d16
Make statement more self-standing
traviscross Oct 13, 2025
8f60bab
Add rule identifier to destructuring/capturing claim
traviscross Oct 13, 2025
02ca96f
Fix `non_exhaustive` rule identifier fragment
traviscross Oct 13, 2025
a7b12d4
Add forward reference for `non_exhaustive` exception
traviscross Oct 13, 2025
b8b4c94
Revise text about field matching and `..`
traviscross Oct 13, 2025
4e0e00a
Add identifier for wildcard fields capturing rule
traviscross Oct 13, 2025
1390c45
Remove example lead-in from array slice capturing rule
traviscross Oct 13, 2025
2d79a1e
Remove redundant example on structs and capturing
traviscross Oct 13, 2025
16c7d9e
Improve examples in wildcard capturing section
traviscross Oct 14, 2025
1eff192
Clarify main discriminant reads rule
traviscross Oct 14, 2025
64ac8db
Revise examples for main discriminant reads rule
traviscross Oct 14, 2025
c5f4a8a
Revise `...discriminants.single-variant` rule
traviscross Oct 14, 2025
aaf0e02
Revise `...discriminants.non_exhaustive` rule
traviscross Oct 14, 2025
7ad248f
Revise `...discriminants.uninhabited-variants` rule
traviscross Oct 14, 2025
a2877af
Revise `...discriminants.range-patterns` rule
traviscross Oct 14, 2025
e0c9612
Revise `...discriminants.slice-patterns-*` rules
traviscross Oct 14, 2025
69e7dfe
Separate sections for range/slice pattern capturing
traviscross Oct 14, 2025
c84f25f
Add admonition about the desugaring of destructuring
traviscross Oct 14, 2025
bc6c3c4
Add admonition about pointer vs pointee capturing
traviscross Oct 14, 2025
010f9a8
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.
Copy link
Member

Choose a reason for hiding this comment

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

Is the intention that these are the final semantics? I find it really strange to special-case single-variant enums.

Copy link
Member

@RalfJung RalfJung Dec 19, 2025

Choose a reason for hiding this comment

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

Ah, that's basically the same as the other thread above, which in turn is the same as 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.

The other thread is not the same as rust-lang/rust#147722. That thread concerns what is captured in a closure. Additionally, it involves a case where removing #[non_exhaustive] from an enum is a breaking change.

Copy link
Member

Choose a reason for hiding this comment

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

It is "the same" in the sense that it's all caused by special-casing single-variant enums except when they are foreign-crate non-exhaustive enums, isn't it?

Copy link
Contributor

Choose a reason for hiding this comment

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

Fair enough. They're "the same" in that sense.


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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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

Unfortunately, yes. If we have the following upstream library crate1:

#[non_exhaustive]
pub enum Meow {
    A(u32, String),
}

and a downstream crate with the following code

use crate1::Meow;

pub fn f(x: Meow) -> impl FnOnce() {
    || {
        match x {
            Meow::A(a, b) => {
                drop((a, b));
            }
            _ => unreachable!(),
        }
    }
}

then with rust-lang/rust#138961, removing #[non_exhaustive] breaks the downstream crate. This would be another argument for the change proposed in rust-lang/rust#147722, i.e. that regardless of #[non_exhaustive], there should be a read of the discriminant.

Copy link
Contributor Author

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?

I gave this part of your question some thought and I guess the closest you'd get to that is the potential for a difference in the size of some closures due to a different set of precise captures being computed...

Choose a reason for hiding this comment

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

I had trouble seeing what the error would be so I reproduced it (in Nightly)

error[E0373]: closure may outlive the current function, but it borrows `x.0`, which is owned by the current function
  --> src/main.rs:8:5
   |
 8 |     || {
   |     ^^ may outlive borrowed value `x.0`
 9 |         match x {
   |               - `x.0` is borrowed here
   |
note: closure is returned here
  --> src/main.rs:8:5
   |
 8 | /     || {
 9 | |         match x {
10 | |             Meow::A(a, b) => {
11 | |                 drop((a, b));
...  |
15 | |     }
   | |_____^
help: to force the closure to take ownership of `x.0` (and any other referenced variables), use the `move` keyword
   |
 8 |     move || {
   |     ++++

For more information about this error, try `rustc --explain E0373`.

(I filed a diagnostic papercut rust-lang/rust#150144)

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?

Turns out the answer is yes. Consider the following code:

dep/src/lib.rs:

#[non_exhaustive]
pub enum Thing {
    One(i32, LoudDrop),
}

pub struct LoudDrop(pub &'static str);
impl Drop for LoudDrop {
    fn drop(&mut self) {
        println!("dropping {}", self.0);
    }
}

src/main.rs:

use dep::{LoudDrop, Thing};

#[allow(unused)]
fn main() {
    let mut thing = Thing::One(0, LoudDrop("a"));
    let closure = move || match thing {
        Thing::One(x, _) => {}
        _ => unreachable!(),
    };
    println!("before assign");
    thing = Thing::One(1, LoudDrop("b"));
    println!("after assign");
}

Using nightly rust, this code prints:

before assign
after assign
dropping a
dropping b

However, if we delete the #[non_exhaustive], this code prints:

before assign
dropping a
after assign
dropping b

It seems that the LoudDrop is moved into the closure only if the enum is #[non_exhaustive]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, I was considering something like this but couldn't get it to a full example because I forgot about drop being observable 😅


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