1
- //! A MIR pass which duplicates a coroutine's body and removes any derefs which
2
- //! would be present for upvars that are taken by-ref. The result of which will
3
- //! be a coroutine body that takes all of its upvars by-move, and which we stash
4
- //! into the `CoroutineInfo` for all coroutines returned by coroutine-closures.
1
+ //! This pass constructs a second coroutine body sufficient for return from
2
+ //! `FnOnce`/`AsyncFnOnce` implementations for coroutine-closures (e.g. async closures).
3
+ //!
4
+ //! Consider an async closure like:
5
+ //! ```rust
6
+ //! #![feature(async_closure)]
7
+ //!
8
+ //! let x = vec![1, 2, 3];
9
+ //!
10
+ //! let closure = async move || {
11
+ //! println!("{x:#?}");
12
+ //! };
13
+ //! ```
14
+ //!
15
+ //! This desugars to something like:
16
+ //! ```rust,ignore (invalid-borrowck)
17
+ //! let x = vec![1, 2, 3];
18
+ //!
19
+ //! let closure = move || {
20
+ //! async {
21
+ //! println!("{x:#?}");
22
+ //! }
23
+ //! };
24
+ //! ```
25
+ //!
26
+ //! Important to note here is that while the outer closure *moves* `x: Vec<i32>`
27
+ //! into its upvars, the inner `async` coroutine simply captures a ref of `x`.
28
+ //! This is the "magic" of async closures -- the futures that they return are
29
+ //! allowed to borrow from their parent closure's upvars.
30
+ //!
31
+ //! However, what happens when we call `closure` with `AsyncFnOnce` (or `FnOnce`,
32
+ //! since all async closures implement that too)? Well, recall the signature:
33
+ //! ```
34
+ //! use std::future::Future;
35
+ //! pub trait AsyncFnOnce<Args>
36
+ //! {
37
+ //! type CallOnceFuture: Future<Output = Self::Output>;
38
+ //! type Output;
39
+ //! fn async_call_once(
40
+ //! self,
41
+ //! args: Args
42
+ //! ) -> Self::CallOnceFuture;
43
+ //! }
44
+ //! ```
45
+ //!
46
+ //! This signature *consumes* the async closure (`self`) and returns a `CallOnceFuture`.
47
+ //! How do we deal with the fact that the coroutine is supposed to take a reference
48
+ //! to the captured `x` from the parent closure, when that parent closure has been
49
+ //! destroyed?
50
+ //!
51
+ //! This is the second piece of magic of async closures. We can simply create a
52
+ //! *second* `async` coroutine body where that `x` that was previously captured
53
+ //! by reference is now captured by value. This means that we consume the outer
54
+ //! closure and return a new coroutine that will hold onto all of these captures,
55
+ //! and drop them when it is finished (i.e. after it has been `.await`ed).
56
+ //!
57
+ //! We do this with the analysis below, which detects the captures that come from
58
+ //! borrowing from the outer closure, and we simply peel off a `deref` projection
59
+ //! from them. This second body is stored alongside the first body, and optimized
60
+ //! with it in lockstep. When we need to resolve a body for `FnOnce` or `AsyncFnOnce`,
61
+ //! we use this "by move" body instead.
5
62
6
63
use itertools:: Itertools ;
7
64
@@ -16,6 +73,8 @@ pub struct ByMoveBody;
16
73
17
74
impl < ' tcx > MirPass < ' tcx > for ByMoveBody {
18
75
fn run_pass ( & self , tcx : TyCtxt < ' tcx > , body : & mut mir:: Body < ' tcx > ) {
76
+ // We only need to generate by-move coroutine bodies for coroutines that come
77
+ // from coroutine-closures.
19
78
let Some ( coroutine_def_id) = body. source . def_id ( ) . as_local ( ) else {
20
79
return ;
21
80
} ;
@@ -24,15 +83,19 @@ impl<'tcx> MirPass<'tcx> for ByMoveBody {
24
83
else {
25
84
return ;
26
85
} ;
86
+
87
+ // Also, let's skip processing any bodies with errors, since there's no guarantee
88
+ // the MIR body will be constructed well.
27
89
let coroutine_ty = body. local_decls [ ty:: CAPTURE_STRUCT_LOCAL ] . ty ;
28
90
if coroutine_ty. references_error ( ) {
29
91
return ;
30
92
}
31
93
32
- let ty:: Coroutine ( _, args) = * coroutine_ty. kind ( ) else { bug ! ( "{body:#?}" ) } ;
33
- let args = args. as_coroutine ( ) ;
34
-
35
- let coroutine_kind = args. kind_ty ( ) . to_opt_closure_kind ( ) . unwrap ( ) ;
94
+ let ty:: Coroutine ( _, coroutine_args) = * coroutine_ty. kind ( ) else { bug ! ( "{body:#?}" ) } ;
95
+ // We don't need to generate a by-move coroutine if the kind of the coroutine is
96
+ // already `FnOnce` -- that means that any upvars that the closure consumes have
97
+ // already been taken by-value.
98
+ let coroutine_kind = coroutine_args. as_coroutine ( ) . kind_ty ( ) . to_opt_closure_kind ( ) . unwrap ( ) ;
36
99
if coroutine_kind == ty:: ClosureKind :: FnOnce {
37
100
return ;
38
101
}
@@ -43,12 +106,13 @@ impl<'tcx> MirPass<'tcx> for ByMoveBody {
43
106
else {
44
107
bug ! ( ) ;
45
108
} ;
46
- let parent_args = parent_args. as_coroutine_closure ( ) ;
47
- let parent_upvars_ty = parent_args. tupled_upvars_ty ( ) ;
48
- let tupled_inputs_ty = tcx. instantiate_bound_regions_with_erased (
49
- parent_args. coroutine_closure_sig ( ) . map_bound ( |sig| sig. tupled_inputs_ty ) ,
50
- ) ;
51
- let num_args = tupled_inputs_ty. tuple_fields ( ) . len ( ) ;
109
+ let parent_closure_args = parent_args. as_coroutine_closure ( ) ;
110
+ let num_args = parent_closure_args
111
+ . coroutine_closure_sig ( )
112
+ . skip_binder ( )
113
+ . tupled_inputs_ty
114
+ . tuple_fields ( )
115
+ . len ( ) ;
52
116
53
117
let mut by_ref_fields = UnordSet :: default ( ) ;
54
118
for ( idx, ( coroutine_capture, parent_capture) ) in tcx
@@ -59,41 +123,30 @@ impl<'tcx> MirPass<'tcx> for ByMoveBody {
59
123
. zip_eq ( tcx. closure_captures ( parent_def_id) )
60
124
. enumerate ( )
61
125
{
62
- // This argument is captured by-move from the parent closure, but by-ref
126
+ // This upvar is captured by-move from the parent closure, but by-ref
63
127
// from the inner async block. That means that it's being borrowed from
64
- // the closure body -- we need to change the coroutine take it by move.
128
+ // the outer closure body -- we need to change the coroutine to take the
129
+ // upvar by value.
65
130
if coroutine_capture. is_by_ref ( ) && !parent_capture. is_by_ref ( ) {
66
131
by_ref_fields. insert ( FieldIdx :: from_usize ( num_args + idx) ) ;
67
132
}
68
133
69
134
// Make sure we're actually talking about the same capture.
135
+ // FIXME(async_closures): We could look at the `hir::Upvar` instead?
70
136
assert_eq ! ( coroutine_capture. place. ty( ) , parent_capture. place. ty( ) ) ;
71
137
}
72
138
73
- let by_move_coroutine_ty = Ty :: new_coroutine (
74
- tcx,
75
- coroutine_def_id. to_def_id ( ) ,
76
- ty:: CoroutineArgs :: new (
139
+ let by_move_coroutine_ty = tcx
140
+ . instantiate_bound_regions_with_erased ( parent_closure_args. coroutine_closure_sig ( ) )
141
+ . to_coroutine_given_kind_and_upvars (
77
142
tcx,
78
- ty:: CoroutineArgsParts {
79
- parent_args : args. parent_args ( ) ,
80
- kind_ty : Ty :: from_closure_kind ( tcx, ty:: ClosureKind :: FnOnce ) ,
81
- resume_ty : args. resume_ty ( ) ,
82
- yield_ty : args. yield_ty ( ) ,
83
- return_ty : args. return_ty ( ) ,
84
- witness : args. witness ( ) ,
85
- // Concatenate the args + closure's captures (since they're all by move).
86
- tupled_upvars_ty : Ty :: new_tup_from_iter (
87
- tcx,
88
- tupled_inputs_ty
89
- . tuple_fields ( )
90
- . iter ( )
91
- . chain ( parent_upvars_ty. tuple_fields ( ) ) ,
92
- ) ,
93
- } ,
94
- )
95
- . args ,
96
- ) ;
143
+ parent_closure_args. parent_args ( ) ,
144
+ coroutine_def_id. to_def_id ( ) ,
145
+ ty:: ClosureKind :: FnOnce ,
146
+ tcx. lifetimes . re_erased ,
147
+ parent_closure_args. tupled_upvars_ty ( ) ,
148
+ parent_closure_args. coroutine_captures_by_ref_ty ( ) ,
149
+ ) ;
97
150
98
151
let mut by_move_body = body. clone ( ) ;
99
152
MakeByMoveBody { tcx, by_ref_fields, by_move_coroutine_ty } . visit_body ( & mut by_move_body) ;
0 commit comments