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
+ //! pub trait AsyncFnOnce<Args>
35
+ //! {
36
+ //! type CallOnceFuture: Future<Output = Self::Output>;
37
+ //! type Output;
38
+ //! fn async_call_once(
39
+ //! self,
40
+ //! args: Args
41
+ //! ) -> Self::CallOnceFuture;
42
+ //! }
43
+ //! ```
44
+ //!
45
+ //! This signature *consumes* the async closure (`self`) and returns a `CallOnceFuture`.
46
+ //! How do we deal with the fact that the coroutine is supposed to take a reference
47
+ //! to the captured `x` from the parent closure, when that parent closure has been
48
+ //! destroyed?
49
+ //!
50
+ //! This is the second piece of magic of async closures. We can simply create a
51
+ //! *second* `async` coroutine body where that `x` that was previously captured
52
+ //! by reference is now captured by value. This means that we consume the outer
53
+ //! closure and return a new coroutine that will hold onto all of these captures,
54
+ //! and drop them when it is finished (i.e. after it has been `.await`ed).
55
+ //!
56
+ //! We do this with the analysis below, which detects the captures that come from
57
+ //! borrowing from the outer closure, and we simply peel off a `deref` projection
58
+ //! from them. This second body is stored alongside the first body, and optimized
59
+ //! with it in lockstep. When we need to resolve a body for `FnOnce` or `AsyncFnOnce`,
60
+ //! we use this "by move" body instead.
5
61
6
62
use itertools:: Itertools ;
7
63
@@ -16,6 +72,8 @@ pub struct ByMoveBody;
16
72
17
73
impl < ' tcx > MirPass < ' tcx > for ByMoveBody {
18
74
fn run_pass ( & self , tcx : TyCtxt < ' tcx > , body : & mut mir:: Body < ' tcx > ) {
75
+ // We only need to generate by-move coroutine bodies for coroutines that come
76
+ // from coroutine-closures.
19
77
let Some ( coroutine_def_id) = body. source . def_id ( ) . as_local ( ) else {
20
78
return ;
21
79
} ;
@@ -24,15 +82,19 @@ impl<'tcx> MirPass<'tcx> for ByMoveBody {
24
82
else {
25
83
return ;
26
84
} ;
85
+
86
+ // Also, let's skip processing any bodies with errors, since there's no guarantee
87
+ // the MIR body will be constructed well.
27
88
let coroutine_ty = body. local_decls [ ty:: CAPTURE_STRUCT_LOCAL ] . ty ;
28
89
if coroutine_ty. references_error ( ) {
29
90
return ;
30
91
}
31
92
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 ( ) ;
93
+ let ty:: Coroutine ( _, coroutine_args) = * coroutine_ty. kind ( ) else { bug ! ( "{body:#?}" ) } ;
94
+ // We don't need to generate a by-move coroutine if the kind of the coroutine is
95
+ // already `FnOnce` -- that means that any upvars that the closure consumes have
96
+ // already been taken by-value.
97
+ let coroutine_kind = coroutine_args. as_coroutine ( ) . kind_ty ( ) . to_opt_closure_kind ( ) . unwrap ( ) ;
36
98
if coroutine_kind == ty:: ClosureKind :: FnOnce {
37
99
return ;
38
100
}
@@ -43,12 +105,13 @@ impl<'tcx> MirPass<'tcx> for ByMoveBody {
43
105
else {
44
106
bug ! ( ) ;
45
107
} ;
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 ( ) ;
108
+ let parent_closure_args = parent_args. as_coroutine_closure ( ) ;
109
+ let num_args = parent_closure_args
110
+ . coroutine_closure_sig ( )
111
+ . skip_binder ( )
112
+ . tupled_inputs_ty
113
+ . tuple_fields ( )
114
+ . len ( ) ;
52
115
53
116
let mut by_ref_fields = FxIndexSet :: default ( ) ;
54
117
for ( idx, ( coroutine_capture, parent_capture) ) in tcx
@@ -59,41 +122,30 @@ impl<'tcx> MirPass<'tcx> for ByMoveBody {
59
122
. zip_eq ( tcx. closure_captures ( parent_def_id) )
60
123
. enumerate ( )
61
124
{
62
- // This argument is captured by-move from the parent closure, but by-ref
125
+ // This upvar is captured by-move from the parent closure, but by-ref
63
126
// 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.
127
+ // the outer closure body -- we need to change the coroutine to take the
128
+ // upvar by value.
65
129
if coroutine_capture. is_by_ref ( ) && !parent_capture. is_by_ref ( ) {
66
130
by_ref_fields. insert ( FieldIdx :: from_usize ( num_args + idx) ) ;
67
131
}
68
132
69
133
// Make sure we're actually talking about the same capture.
134
+ // FIXME(async_closures): We could look at the `hir::Upvar` instead?
70
135
assert_eq ! ( coroutine_capture. place. ty( ) , parent_capture. place. ty( ) ) ;
71
136
}
72
137
73
- let by_move_coroutine_ty = Ty :: new_coroutine (
74
- tcx,
75
- coroutine_def_id. to_def_id ( ) ,
76
- ty:: CoroutineArgs :: new (
138
+ let by_move_coroutine_ty = tcx
139
+ . instantiate_bound_regions_with_erased ( parent_closure_args. coroutine_closure_sig ( ) )
140
+ . to_coroutine_given_kind_and_upvars (
77
141
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
- ) ;
142
+ parent_closure_args. parent_args ( ) ,
143
+ coroutine_def_id. to_def_id ( ) ,
144
+ ty:: ClosureKind :: FnOnce ,
145
+ tcx. lifetimes . re_erased ,
146
+ parent_closure_args. tupled_upvars_ty ( ) ,
147
+ parent_closure_args. coroutine_captures_by_ref_ty ( ) ,
148
+ ) ;
97
149
98
150
let mut by_move_body = body. clone ( ) ;
99
151
MakeByMoveBody { tcx, by_ref_fields, by_move_coroutine_ty } . visit_body ( & mut by_move_body) ;
0 commit comments