-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
stabilize disjoint capture in closures (RFC 2229) #88126
Comments
cc @rust-lang/lang @rust-lang/wg-rfc-2229 |
@rfcbot fcp merge Per discussion in the @rust-lang/lang meeting today, I propose to merge this. We are trying to keep the wheels moving here to try and meet the 1.56 branch deadline for Rust 2021. |
Team member @nikomatsakis has proposed to merge this. The next step is review by the rest of the tagged team members: No concerns currently listed. Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! See this document for info about what commands tagged team members can give me. |
Congrats to @arora-aman, @roxelo, and others who worked on this. Great work. |
Found an interesting interaction between disjoint captures and The following program compiles on 2018 but fails on 2021: use std::panic::AssertUnwindSafe;
struct UnwindUnsafe<'a>(&'a mut u32);
fn main() {
let mut x = 0;
let uu = UnwindUnsafe(&mut x);
let aus = AssertUnwindSafe(uu);
let _ = std::panic::catch_unwind(move || {
let AssertUnwindSafe(uu) = aus;
drop(uu)
});
} The issue here is that the closure doesn't actually capture the AssertUnwindSafe wrapper, so it becomes The example comes from a real-world code in rust-lang/rust-analyzer#9961 |
Thank you for bring this up. We are aware of this and this case gets handled by the 2229 migration. Not sure if these needs to get documented in the report but here's the issue for reference: rust-lang/project-rfc-2229#29 |
This FCP needs to start today to make it in time for 1.56. |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
@cramertj Here's your free emoji: |
The final comment period, with a disposition to merge, as per the review above, is now complete. As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed. The RFC will be merged soon. |
What's the status of the second blocker mentioned in this issue?
|
That's been fixed by #88280 |
I ticked the checkbox for this then |
Since this is now stabilized, should this issue be closed? |
Yes. |
…ler-errors Remove the `capture_disjoint_fields` feature As best I can tell, this was stabilized for Edition 2021 in rust-lang#88126 but the feature was never removed.
…ler-errors Remove the `capture_disjoint_fields` feature As best I can tell, this was stabilized for Edition 2021 in rust-lang#88126 but the feature was never removed.
…ler-errors Remove the `capture_disjoint_fields` feature As best I can tell, this was stabilized for Edition 2021 in rust-lang#88126 but the feature was never removed.
…ler-errors Remove the `capture_disjoint_fields` feature As best I can tell, this was stabilized for Edition 2021 in rust-lang#88126 but the feature was never removed.
RFC 2229 Stabilization Report
Links
Impl blockers
#[nonexhaustive]
captures across cratesSummary
This feature adjusts the rules for closure captures starting in Rust 2021. In Rust 2018 and before, closures always captured entire local variables. Under these new rules, they capture more precise paths. These paths are always some prefix of the precise paths that are references within the closure body.
As a simple example, in Rust 2018,
|| println!("{}", a.y)
captures a reference toa
and not justa.y
. Capturinga
in its entirety prevents mutation or moves from other fields ofa
, so that code like this does not compile:In Rust 2021, however, this closure would only capture a reference to
a.y
, and hence the code above would compile.Disjoint capture was proposed as part of RFC 2229 and the RFC contains details about the motivation.
Capture algorithm, guide description
Ref closures
Ref closures generally capture the precise paths that you use within the closure, subject to a few limitations:
(*b).f
, whereb: Box<SomeType>
, we would capture all ofb
. This ensures that closures never capture deceptively large amounts of data that were intentionally being passed via a box.Move closures
Move closures generally try to take ownership of all their captures. However, in the event that the capture they are taking ownership of is borrowed data, they may borrow the data instead:
x.0.split(0)
wherex: &([u32], String)
will capturex.0
by reference, even though this is a move closure (note that in Rust 2018, we would have captured the referencex
itself, which is ery similar).(*b).f
, whereb: Box<SomeType>
, we would capture all ofb
. This ensures that closures never capture deceptively large amounts of data that were intentionally being passed via a box.Optimizations
In addition to the rules above, the compiler also performs some optimizations to reduce closure size, but these do not affect which programs compile or do not compile, or the order of destructor execution, so they are not relevant to understanding how the program works.
Examples
Capture algorithm, precise description
The precise capture algorithm is described here. The high-level idea is as follows:
(*a).b.c
move
,ref
, orref mut
|| x.0.truncate(0)
, wherex: &mut (String, String)
, would capture the place(*x).0
with the moderef mut
.x.0.truncate(0)
is shorthand forstr::truncate(&mut (*x).0, 0)
move
closure or notunsafe_check(P, M) -> (P, M)
truncates the place P so that any unsafe operations occur in the closure, not the creator(*x.f).m
, ifx.f: *mut T
, we would just capturex.f
ref_opt(P, M) -> (P, M)
truncates dereferences of&T
references so that we capture the entire&T
instead of capturing some subfield. This is an optimization that minimizes the size of closures without affecting the set of things that compile.x: &(String, String)
and|| foo(&x.0, &x.1)
, we would minimize the places(*x).0
and(*x).1
to justx
, thus capturing a single reference instead of tworef_xform
: If this is a "by-value" mode capture, then truncate to the first pointer dereferenceb: Box<[u8; 1024]>
and a by-value capture of*b
, truncate atb
so that we capture the box, not its contentsmove_xform
, which performs 3 truncations:ref mut
mode capture, and the place contains a deref of an&mut
reference, then leave it unchanged.ref
mode capture, and the place contains a deref of an&
reference, then leave it unchanged.by-value
mode capture, then truncate to the first pointer dereference|| { &x; &mut x.y; }
we capturex
(the prefix ofx
andx.y
) but with the moderef mut
(the greater of the two modes).What data does a closure use?
Closures are considered to reference data under the following conditions:
&a.b.c
) is aref
mode access to the placea.b.c
; shared borrows are often introduced via calls to methods likea.b.c.split(0)
&mut a.b.c
) is aref mut
mode access to the placea.b.c
; mutable borrows are often introduced via calls to methods likea.b.c.truncate(0)
drop(x)
) ofCopy
type are considered moderef
of the placex
drop(x)
) not ofCopy
type are considered amove
mode access to the placex
Enum::Variant
is considered a read of the enum if:#[non_exhaustive]
and is defined in another crate (FIXME)Implementation strategy
The implementation strategy is to modify the desugaring of closures. Whereas Rust 2018 closures like
|| foo(&mut a.b, &mut c.d, &mut c.e)
desugars to creating an anonymous struct with two fields, one fora
and one forc
:The Rust 2021 struct would contain a field for each unique capture:
This change is focused on MIR construction and does not affect the borrow checker in any significant way (there were changes to the diagnostics code).
Size measurements
We have measured the size of closures on the compiler and other code bases. A broader crater run is in progress. Preliminary measurements suggest that closures are slightly larger in Rust 2021, but not substantially: the average is that closures grow by less than 1 byte. This table breaks down measurements by the size of the original closure:
How the RFC's unresolved questions were resolved
The RFC posed the following unresolved questions:
We use a naive algorithm apart from a single optimization which captures an
&
reference instead of creating shared references to the individual fields. Our measurements found that this optimization is sufficient to ensure that closures are generally the same size or smaller in Rust 2018 and avoided any complex changes to the borrow checker.We do not signal that functions are pure, which means that overloaded derefs and deref-mut implementations capture the base pointer. The one exception is
Box
, as described in the next question.Yes, we treat
Box
specially. This is consistent with the borrow checker which genreally treatsBox
as special. This could in the future be generalized with some sort of "deref pure" trait but we left that for future work.This was resolved via edition migrations. Migratons were also used to address some other cases that arose, such as the fact that closures may not implement different auto traits as a result of capturing different things.
Test cases and testing
#[rustc_capture_analysis]
annotation causes rustc to dump debugging output about what is captured and why, allowing us to test the model directly.&T
capture optimization, and the stderr file identifies the precise path that is captured.The text was updated successfully, but these errors were encountered: