-
Notifications
You must be signed in to change notification settings - Fork 564
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
Changes from all commits
b47f571
0d3942b
147fd57
69e5f80
7447e5a
31ce0a3
d7a5f9c
4e42d16
8f60bab
02ca96f
a7b12d4
b8b4c94
4e0e00a
1390c45
2d79a1e
16c7d9e
1eff192
64ac8db
c5f4a8a
aaf0e02
7ad248f
a2877af
e0c9612
69e7dfe
c84f25f
bc6c3c4
010f9a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
|
@@ -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` | ||
|
|
||
|
|
@@ -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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. More discussion is happening in rust-lang/rust#147722.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can adding/removing Can removing
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Unfortunately, yes. If we have the following upstream library #[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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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... There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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: However, if we delete the It seems that the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
||
|
|
||
There was a problem hiding this comment.
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.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.