From 2bb92c7a0f29bb7010ad407de58a860b967ebefc Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Wed, 10 Jun 2026 04:01:26 +0900 Subject: [PATCH 01/13] lower move expressions in coroutine closures --- .../rustc_ast_lowering/src/diagnostics.rs | 2 +- compiler/rustc_ast_lowering/src/expr.rs | 15 +++- .../rustc_ast_lowering/src/expr/closure.rs | 71 +++++++++++-------- compiler/rustc_hir_analysis/src/collect.rs | 10 ++- 4 files changed, 67 insertions(+), 31 deletions(-) diff --git a/compiler/rustc_ast_lowering/src/diagnostics.rs b/compiler/rustc_ast_lowering/src/diagnostics.rs index b0fada9d3cd9e..aa8550f9b99ed 100644 --- a/compiler/rustc_ast_lowering/src/diagnostics.rs +++ b/compiler/rustc_ast_lowering/src/diagnostics.rs @@ -148,7 +148,7 @@ pub(crate) struct ClosureCannotBeStatic { } #[derive(Diagnostic)] -#[diag("`move(expr)` is only supported in plain closures")] +#[diag("`move(expr)` is only supported in closures")] pub(crate) struct MoveExprOnlyInPlainClosures { #[primary_span] pub span: Span, diff --git a/compiler/rustc_ast_lowering/src/expr.rs b/compiler/rustc_ast_lowering/src/expr.rs index 0a4a2ae7145e3..2d5f760114ec5 100644 --- a/compiler/rustc_ast_lowering/src/expr.rs +++ b/compiler/rustc_ast_lowering/src/expr.rs @@ -865,6 +865,19 @@ impl<'hir> LoweringContext<'_, 'hir> { (params, res) }); + let explicit_captures: &'hir [hir::ExplicitCapture] = + if let Some(move_expr_state) = self.move_expr_bindings.last().and_then(Option::as_ref) { + self.arena.alloc_from_iter(move_expr_state.occurrences.iter().filter_map( + |occurrence| { + occurrence + .explicit_capture + .then_some(hir::ExplicitCapture { var_hir_id: occurrence.binding }) + }, + )) + } else { + &[] + }; + // `static |<_task_context?>| -> { }`: hir::ExprKind::Closure(self.arena.alloc(hir::Closure { def_id: closure_def_id, @@ -877,7 +890,7 @@ impl<'hir> LoweringContext<'_, 'hir> { fn_arg_span: None, kind: hir::ClosureKind::Coroutine(coroutine_kind), constness: hir::Constness::NotConst, - explicit_captures: &[], + explicit_captures, })) } diff --git a/compiler/rustc_ast_lowering/src/expr/closure.rs b/compiler/rustc_ast_lowering/src/expr/closure.rs index 2831fb4fa8352..95f54ba281e17 100644 --- a/compiler/rustc_ast_lowering/src/expr/closure.rs +++ b/compiler/rustc_ast_lowering/src/expr/closure.rs @@ -12,8 +12,8 @@ use crate::diagnostics::{ClosureCannotBeStatic, CoroutineTooManyParameters}; impl<'hir> LoweringContext<'_, 'hir> { // Entry point for `ExprKind::Closure`. Plain closures go through // `lower_expr_plain_closure_with_move_exprs`, which can wrap the lowered - // closure in `let` initializers for `move(...)`. Coroutine closures keep the - // existing coroutine-specific path and reject `move(...)` for now. + // closure in `let` initializers for `move(...)`. Coroutine closures use the + // same wrapper after building their coroutine-specific body shape. pub(super) fn lower_expr_closure_expr( &mut self, e: &Expr, @@ -23,8 +23,6 @@ impl<'hir> LoweringContext<'_, 'hir> { let attrs = self.lower_attrs(expr_hir_id, &e.attrs, e.span, Target::from_expr(e)); match closure.coroutine_marker { - // FIXME(TaKO8Ki): Support `move(expr)` in coroutine closures too. - // For the first step, we only support plain closures. Some(coroutine_marker) => hir::Expr { hir_id: expr_hir_id, kind: self.lower_expr_coroutine_closure( @@ -117,12 +115,24 @@ impl<'hir> LoweringContext<'_, 'hir> { fn_arg_span, ); + let closure_expr = hir::Expr { + hir_id: expr_hir_id, + kind: closure_kind, + span: self.lower_span(whole_span), + }; + + self.lower_expr_with_move_exprs(closure_expr, move_expr_state, body, whole_span) + } + + fn lower_expr_with_move_exprs( + &mut self, + expr: hir::Expr<'hir>, + move_expr_state: MoveExprState<'hir>, + body: &Expr, + whole_span: Span, + ) -> hir::Expr<'hir> { if move_expr_state.occurrences.is_empty() { - return hir::Expr { - hir_id: expr_hir_id, - kind: closure_kind, - span: self.lower_span(whole_span), - }; + return expr; } let initializers = MoveExprInitializerFinder::collect(body) @@ -162,14 +172,8 @@ impl<'hir> LoweringContext<'_, 'hir> { initializer_bindings.insert(occurrence.id, (occurrence.ident, occurrence.binding)); } - let closure_expr = self.arena.alloc(hir::Expr { - hir_id: expr_hir_id, - kind: closure_kind, - span: self.lower_span(whole_span), - }); - let stmts = self.arena.alloc_from_iter(stmts); - let block = self.block_all(whole_span, stmts, Some(closure_expr)); + let block = self.block_all(whole_span, stmts, Some(self.arena.alloc(expr))); self.expr(whole_span, hir::ExprKind::Block(block, None)) } @@ -294,9 +298,9 @@ impl<'hir> LoweringContext<'_, 'hir> { } // Coroutine closures are lowered separately because they build a different - // body shape. This path pushes `None` for `move_expr_bindings`, so any - // `move(...)` in the coroutine body gets a targeted unsupported-position - // error instead of being collected like a plain closure occurrence. + // body shape. The source body is still lowered with `MoveExprState` active, + // so `move(...)` occurrences are collected and then hoisted to the outer + // closure body, immediately before the generated coroutine is created. fn lower_expr_coroutine_closure( &mut self, binder: &ClosureBinder, @@ -332,16 +336,27 @@ impl<'hir> LoweringContext<'_, 'hir> { // Transform `async |x: u8| -> X { ... }` into // `|x: u8| || -> X { ... }`. let body_id = this.lower_body(|this| { - let ((parameters, expr), _) = this.with_move_expr_bindings(None, |this| { - this.lower_coroutine_body_with_moved_arguments( - &inner_decl, - |this| this.with_new_scopes(fn_decl_span, |this| this.lower_expr_mut(body)), + let ((parameters, expr), move_expr_state) = + this.with_move_expr_bindings(Some(MoveExprState::default()), |this| { + this.lower_coroutine_body_with_moved_arguments( + &inner_decl, + |this| { + this.with_new_scopes(fn_decl_span, |this| this.lower_expr_mut(body)) + }, + fn_decl_span, + body.span, + coroutine_marker, + hir::CoroutineSource::Closure, + ) + }); + let Some(move_expr_state) = move_expr_state else { + span_bug!( fn_decl_span, - body.span, - coroutine_marker, - hir::CoroutineSource::Closure, - ) - }); + "coroutine closure lowering did not return `move(...)` state" + ); + }; + + let expr = this.lower_expr_with_move_exprs(expr, move_expr_state, body, body.span); this.maybe_forward_track_caller(closure_hir_id, expr.hir_id); diff --git a/compiler/rustc_hir_analysis/src/collect.rs b/compiler/rustc_hir_analysis/src/collect.rs index 2e4da8d948f07..2f61ed9be30b8 100644 --- a/compiler/rustc_hir_analysis/src/collect.rs +++ b/compiler/rustc_hir_analysis/src/collect.rs @@ -1709,6 +1709,14 @@ fn coroutine_for_closure(tcx: TyCtxt<'_>, def_id: LocalDefId) -> DefId { bug!() }; + let body = tcx.hir_body(body).value; + // `move(...)` in coroutine closures wraps the generated coroutine in an + // outer block of synthetic initializer lets. + let body = match body.kind { + hir::ExprKind::Block(block, None) if let Some(tail) = block.expr => tail, + _ => body, + }; + let &hir::Expr { kind: hir::ExprKind::Closure(&rustc_hir::Closure { @@ -1717,7 +1725,7 @@ fn coroutine_for_closure(tcx: TyCtxt<'_>, def_id: LocalDefId) -> DefId { .. }), .. - } = tcx.hir_body(body).value + } = body else { bug!() }; From ee0ee6b66fcc2a3172ad4fb4e084639cf513649f Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 11 Jun 2026 13:07:15 +0900 Subject: [PATCH 02/13] handle move-expression captures in coroutine closures --- compiler/rustc_hir_typeck/src/upvar.rs | 78 ++++++++++++------- compiler/rustc_middle/src/ty/closure.rs | 18 ++--- compiler/rustc_middle/src/ty/mod.rs | 2 +- .../src/coroutine/by_move_body.rs | 12 ++- 4 files changed, 66 insertions(+), 44 deletions(-) diff --git a/compiler/rustc_hir_typeck/src/upvar.rs b/compiler/rustc_hir_typeck/src/upvar.rs index 38839c598f913..89b91094f50d2 100644 --- a/compiler/rustc_hir_typeck/src/upvar.rs +++ b/compiler/rustc_hir_typeck/src/upvar.rs @@ -504,44 +504,64 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> { .tupled_inputs_ty .tuple_fields() .len(); + let coroutine_def_id = + self.tcx.coroutine_for_closure(closure_def_id).expect_local(); + let explicit_captures = self + .tcx + .hir_node_by_def_id(coroutine_def_id) + .expect_closure() + .explicit_captures; let typeck_results = self.typeck_results.borrow(); + let parent_captures = typeck_results + .closure_min_captures_flattened(closure_def_id) + .collect::>(); let tupled_upvars_ty_for_borrow = Ty::new_tup_from_iter( self.tcx, - ty::analyze_coroutine_closure_captures( - typeck_results.closure_min_captures_flattened(closure_def_id), - typeck_results - .closure_min_captures_flattened( - self.tcx.coroutine_for_closure(closure_def_id).expect_local(), - ) - // Skip the captures that are just moving the closure's args - // into the coroutine. These are always by move, and we append - // those later in the `CoroutineClosureSignature` helper functions. - .skip(num_args), - |(_, parent_capture), (_, child_capture)| { - // This is subtle. See documentation on function. - let needs_ref = should_reborrow_from_env_of_parent_coroutine_closure( - parent_capture, - child_capture, - ); - + typeck_results + .closure_min_captures_flattened(coroutine_def_id) + // Skip the captures that are just moving the closure's args + // into the coroutine. These are always by move, and we append + // those later in the `CoroutineClosureSignature` helper functions. + .skip(num_args) + .map(|child_capture| { let upvar_ty = child_capture.place.ty(); let capture = child_capture.info.capture_kind; - // Not all upvars are captured by ref, so use - // `apply_capture_kind_on_capture_ty` to ensure that we - // compute the right captured type. - apply_capture_kind_on_capture_ty( - self.tcx, - upvar_ty, - capture, - if needs_ref { + let region = if explicit_captures.iter().any(|explicit| { + explicit.var_hir_id == child_capture.get_root_variable() + }) { + // Synthetic move-expression locals are captured by + // value into the generated coroutine. They do not + // reborrow from the parent coroutine-closure env. + self.tcx.lifetimes.re_erased + } else { + let Some(parent_capture) = parent_captures.iter().copied().find( + |parent_capture| { + ty::child_prefix_matches_parent_projections( + parent_capture, + child_capture, + ) + }, + ) else { + bug!("child capture did not match a parent coroutine capture"); + }; + + // This is subtle. See documentation on function. + if should_reborrow_from_env_of_parent_coroutine_closure( + parent_capture, + child_capture, + ) { closure_env_region } else { self.tcx.lifetimes.re_erased - }, - ) - }, - ), + } + }; + + // Not all upvars are captured by ref, so use + // `apply_capture_kind_on_capture_ty` to ensure that we + // compute the right captured type. + apply_capture_kind_on_capture_ty(self.tcx, upvar_ty, capture, region) + }), ); let coroutine_captures_by_ref_ty = Ty::new_fn_ptr( self.tcx, diff --git a/compiler/rustc_middle/src/ty/closure.rs b/compiler/rustc_middle/src/ty/closure.rs index c6cdb9b9e2b96..7bf143f625405 100644 --- a/compiler/rustc_middle/src/ty/closure.rs +++ b/compiler/rustc_middle/src/ty/closure.rs @@ -434,11 +434,11 @@ pub fn analyze_coroutine_closure_captures<'a, 'tcx: 'a, T>( // refining the set of captures via edition-2021 precise captures. We want to // match up any number of child captures with one parent capture, so we keep // peeking off this `Peekable` until the child doesn't match anymore. + // + // Do not require every parent capture to match a child capture. A parent + // capture may be used only while evaluating a coroutine-closure + // `move(expr)` initializer, before the child coroutine is created. for (parent_field_idx, parent_capture) in parent_captures.into_iter().enumerate() { - // Make sure we use every field at least once, b/c why are we capturing something - // if it's not used in the inner coroutine. - let mut field_used_at_least_once = false; - // A parent matches a child if they share the same prefix of projections. // The child may have more, if it is capturing sub-fields out of // something that is captured by-move in the parent closure. @@ -458,21 +458,13 @@ pub fn analyze_coroutine_closure_captures<'a, 'tcx: 'a, T>( (parent_field_idx, parent_capture), (child_field_idx, child_capture), ); - - field_used_at_least_once = true; } - - // Make sure the field was used at least once. - assert!( - field_used_at_least_once, - "we captured {parent_capture:#?} but it was not used in the child coroutine?" - ); } assert_eq!(child_captures.next(), None, "leftover child captures?"); } } -fn child_prefix_matches_parent_projections( +pub fn child_prefix_matches_parent_projections( parent_capture: &ty::CapturedPlace<'_>, child_capture: &ty::CapturedPlace<'_>, ) -> bool { diff --git a/compiler/rustc_middle/src/ty/mod.rs b/compiler/rustc_middle/src/ty/mod.rs index cc6a8619e1e74..3506b771eabf4 100644 --- a/compiler/rustc_middle/src/ty/mod.rs +++ b/compiler/rustc_middle/src/ty/mod.rs @@ -65,7 +65,7 @@ pub use self::closure::{ BorrowKind, CAPTURE_STRUCT_LOCAL, CaptureInfo, CapturedPlace, ClosureTypeInfo, MinCaptureInformationMap, MinCaptureList, RootVariableMinCaptureList, UpvarCapture, UpvarId, UpvarPath, analyze_coroutine_closure_captures, is_ancestor_or_same_capture, - place_to_string_for_capture, + child_prefix_matches_parent_projections, place_to_string_for_capture, }; pub use self::consts::{ AliasConst, AliasConstKind, AtomicOrdering, Const, ConstInt, ConstKind, ConstToValTreeResult, diff --git a/compiler/rustc_mir_transform/src/coroutine/by_move_body.rs b/compiler/rustc_mir_transform/src/coroutine/by_move_body.rs index 88ffe5861a697..6a66b9c1f9478 100644 --- a/compiler/rustc_mir_transform/src/coroutine/by_move_body.rs +++ b/compiler/rustc_mir_transform/src/coroutine/by_move_body.rs @@ -125,10 +125,20 @@ pub(crate) fn coroutine_by_move_body_def_id<'tcx>( .tupled_inputs_ty .tuple_fields() .len(); + let explicit_captures = + tcx.hir_node_by_def_id(coroutine_def_id).expect_closure().explicit_captures; let field_remapping: UnordMap<_, _> = ty::analyze_coroutine_closure_captures( tcx.closure_captures(parent_def_id).iter().copied(), - tcx.closure_captures(coroutine_def_id).iter().skip(num_args).copied(), + tcx.closure_captures(coroutine_def_id) + .iter() + .skip(num_args) + .filter(|capture| { + !explicit_captures + .iter() + .any(|explicit| explicit.var_hir_id == capture.get_root_variable()) + }) + .copied(), |(parent_field_idx, parent_capture), (child_field_idx, child_capture)| { // Store this set of additional projections (fields and derefs). // We need to re-apply them later. From 79bad7dbba961066cb8d372a8fb8e564f5523337 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 11 Jun 2026 13:07:29 +0900 Subject: [PATCH 03/13] update move-expression coroutine closure tests --- tests/ui/move-expr/async-closures.rs | 30 ++++++++++++++++--- tests/ui/move-expr/async-closures.stderr | 8 ----- tests/ui/move-expr/outside-plain-closure.rs | 2 +- .../ui/move-expr/outside-plain-closure.stderr | 2 +- tests/ui/move-expr/parse-ambiguity-errors.rs | 2 +- .../move-expr/parse-ambiguity-errors.stderr | 2 +- 6 files changed, 30 insertions(+), 16 deletions(-) delete mode 100644 tests/ui/move-expr/async-closures.stderr diff --git a/tests/ui/move-expr/async-closures.rs b/tests/ui/move-expr/async-closures.rs index eea93f02b807a..5f93915a9f877 100644 --- a/tests/ui/move-expr/async-closures.rs +++ b/tests/ui/move-expr/async-closures.rs @@ -1,11 +1,33 @@ //@ edition: 2021 +//@ run-pass #![allow(incomplete_features)] #![feature(move_expr)] +use std::cell::Cell; +use std::sync::Arc; + fn main() { - let s = String::from("hello"); - let _ = async || { - move(s); - //~^ ERROR `move(expr)` is only supported in plain closures + let created = Cell::new(0); + let c = async || { + let n = move({ + created.set(created.get() + 1); + created.get() + }); + n }; + assert_eq!(created.get(), 0); + let fut = c(); + assert_eq!(created.get(), 1); + drop(fut); + + let x = Arc::new(String::from("hello")); + assert_eq!(Arc::strong_count(&x), 1); + + let c = async || move(x.clone()); + assert_eq!(Arc::strong_count(&x), 1); + let fut = c(); + assert_eq!(Arc::strong_count(&x), 2); + drop(fut); + assert_eq!(Arc::strong_count(&x), 1); + assert_eq!(Arc::strong_count(&x), 1); } diff --git a/tests/ui/move-expr/async-closures.stderr b/tests/ui/move-expr/async-closures.stderr deleted file mode 100644 index d0fd5c8ee7df0..0000000000000 --- a/tests/ui/move-expr/async-closures.stderr +++ /dev/null @@ -1,8 +0,0 @@ -error: `move(expr)` is only supported in plain closures - --> $DIR/async-closures.rs:8:9 - | -LL | move(s); - | ^^^^ - -error: aborting due to 1 previous error - diff --git a/tests/ui/move-expr/outside-plain-closure.rs b/tests/ui/move-expr/outside-plain-closure.rs index c4aa6551119fe..64bf8374d92d0 100644 --- a/tests/ui/move-expr/outside-plain-closure.rs +++ b/tests/ui/move-expr/outside-plain-closure.rs @@ -3,5 +3,5 @@ fn main() { let _ = move(String::from("nope")); - //~^ ERROR `move(expr)` is only supported in plain closures + //~^ ERROR `move(expr)` is only supported in closures } diff --git a/tests/ui/move-expr/outside-plain-closure.stderr b/tests/ui/move-expr/outside-plain-closure.stderr index 68c4223641304..c84d71a3579c3 100644 --- a/tests/ui/move-expr/outside-plain-closure.stderr +++ b/tests/ui/move-expr/outside-plain-closure.stderr @@ -1,4 +1,4 @@ -error: `move(expr)` is only supported in plain closures +error: `move(expr)` is only supported in closures --> $DIR/outside-plain-closure.rs:5:13 | LL | let _ = move(String::from("nope")); diff --git a/tests/ui/move-expr/parse-ambiguity-errors.rs b/tests/ui/move-expr/parse-ambiguity-errors.rs index c2927373cb8a7..68b64d1582ff8 100644 --- a/tests/ui/move-expr/parse-ambiguity-errors.rs +++ b/tests/ui/move-expr/parse-ambiguity-errors.rs @@ -5,7 +5,7 @@ fn main() { let x: bool = true; let y: bool = true; let _ = move(x) || y; - //~^ ERROR `move(expr)` is only supported in plain closures + //~^ ERROR `move(expr)` is only supported in closures let x: bool = true; let y: bool = true; diff --git a/tests/ui/move-expr/parse-ambiguity-errors.stderr b/tests/ui/move-expr/parse-ambiguity-errors.stderr index c4dc929eac36c..5990bf861f609 100644 --- a/tests/ui/move-expr/parse-ambiguity-errors.stderr +++ b/tests/ui/move-expr/parse-ambiguity-errors.stderr @@ -4,7 +4,7 @@ error: expected one of `async`, `|`, or `||`, found `[` LL | let _ = move[x] || y; | ^ expected one of `async`, `|`, or `||` -error: `move(expr)` is only supported in plain closures +error: `move(expr)` is only supported in closures --> $DIR/parse-ambiguity-errors.rs:7:13 | LL | let _ = move(x) || y; From 24590c86f619e90146b80ade7fe5d4bd0fe81a93 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 11 Jun 2026 13:11:44 +0900 Subject: [PATCH 04/13] rustfmt --- compiler/rustc_ast_lowering/src/expr.rs | 25 +++++++++++++------------ compiler/rustc_hir_typeck/src/upvar.rs | 8 ++++---- compiler/rustc_middle/src/ty/mod.rs | 4 ++-- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/compiler/rustc_ast_lowering/src/expr.rs b/compiler/rustc_ast_lowering/src/expr.rs index 2d5f760114ec5..d849dc87275be 100644 --- a/compiler/rustc_ast_lowering/src/expr.rs +++ b/compiler/rustc_ast_lowering/src/expr.rs @@ -865,18 +865,19 @@ impl<'hir> LoweringContext<'_, 'hir> { (params, res) }); - let explicit_captures: &'hir [hir::ExplicitCapture] = - if let Some(move_expr_state) = self.move_expr_bindings.last().and_then(Option::as_ref) { - self.arena.alloc_from_iter(move_expr_state.occurrences.iter().filter_map( - |occurrence| { - occurrence - .explicit_capture - .then_some(hir::ExplicitCapture { var_hir_id: occurrence.binding }) - }, - )) - } else { - &[] - }; + let explicit_captures: &'hir [hir::ExplicitCapture] = if let Some(move_expr_state) = + self.move_expr_bindings.last().and_then(Option::as_ref) + { + self.arena.alloc_from_iter(move_expr_state.occurrences.iter().filter_map( + |occurrence| { + occurrence + .explicit_capture + .then_some(hir::ExplicitCapture { var_hir_id: occurrence.binding }) + }, + )) + } else { + &[] + }; // `static |<_task_context?>| -> { }`: hir::ExprKind::Closure(self.arena.alloc(hir::Closure { diff --git a/compiler/rustc_hir_typeck/src/upvar.rs b/compiler/rustc_hir_typeck/src/upvar.rs index 89b91094f50d2..c9dc0f33da8e2 100644 --- a/compiler/rustc_hir_typeck/src/upvar.rs +++ b/compiler/rustc_hir_typeck/src/upvar.rs @@ -535,14 +535,14 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> { // reborrow from the parent coroutine-closure env. self.tcx.lifetimes.re_erased } else { - let Some(parent_capture) = parent_captures.iter().copied().find( - |parent_capture| { + let Some(parent_capture) = + parent_captures.iter().copied().find(|parent_capture| { ty::child_prefix_matches_parent_projections( parent_capture, child_capture, ) - }, - ) else { + }) + else { bug!("child capture did not match a parent coroutine capture"); }; diff --git a/compiler/rustc_middle/src/ty/mod.rs b/compiler/rustc_middle/src/ty/mod.rs index 3506b771eabf4..9328bf44ee04f 100644 --- a/compiler/rustc_middle/src/ty/mod.rs +++ b/compiler/rustc_middle/src/ty/mod.rs @@ -64,8 +64,8 @@ pub use vtable::*; pub use self::closure::{ BorrowKind, CAPTURE_STRUCT_LOCAL, CaptureInfo, CapturedPlace, ClosureTypeInfo, MinCaptureInformationMap, MinCaptureList, RootVariableMinCaptureList, UpvarCapture, UpvarId, - UpvarPath, analyze_coroutine_closure_captures, is_ancestor_or_same_capture, - child_prefix_matches_parent_projections, place_to_string_for_capture, + UpvarPath, analyze_coroutine_closure_captures, child_prefix_matches_parent_projections, + is_ancestor_or_same_capture, place_to_string_for_capture, }; pub use self::consts::{ AliasConst, AliasConstKind, AtomicOrdering, Const, ConstInt, ConstKind, ConstToValTreeResult, From db7693a6ccf247fc288b0fbe7e55a3b2e1702651 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Tue, 23 Jun 2026 02:24:44 +0900 Subject: [PATCH 05/13] refactor move expr initializer wrapping --- compiler/rustc_ast_lowering/src/expr.rs | 134 +++++++++++++++--- .../rustc_ast_lowering/src/expr/closure.rs | 56 +------- 2 files changed, 115 insertions(+), 75 deletions(-) diff --git a/compiler/rustc_ast_lowering/src/expr.rs b/compiler/rustc_ast_lowering/src/expr.rs index d849dc87275be..2bf239545bd81 100644 --- a/compiler/rustc_ast_lowering/src/expr.rs +++ b/compiler/rustc_ast_lowering/src/expr.rs @@ -21,7 +21,7 @@ mod closure; use crate::diagnostics::{ AsyncCoroutinesNotSupported, AwaitOnlyInAsyncFnAndBlocks, FunctionalRecordUpdateDestructuringAssignment, InclusiveRangeWithNoEnd, - InvalidLegacyConstGenericArg, MatchArmWithNoBody, MoveExprOnlyInPlainClosures, + InvalidLegacyConstGenericArg, MatchArmWithNoBody, MoveExprOnlyInSupportedContexts, NeverPatternWithBody, NeverPatternWithGuard, UnderscoreExprLhsAssign, UseConstGenericArg, YieldInClosure, }; @@ -42,7 +42,7 @@ struct MoveExprInitializer<'a> { expr: &'a Expr, } -/// State for `move(...)` expressions found while lowering one plain closure body. +/// State for `move(...)` expressions found while lowering one closure-like body. pub(super) struct MoveExprState<'hir> { pub(super) bindings: NodeMap<(Ident, HirId)>, pub(super) occurrences: Vec>, @@ -73,6 +73,12 @@ impl<'a> MoveExprInitializerFinder<'a> { this.visit_expr(expr); this.initializers } + + fn collect_block(block: &'a Block) -> Vec> { + let mut this = Self { initializers: Vec::new() }; + this.visit_block(block); + this.initializers + } } impl<'a> Visitor<'a> for MoveExprInitializerFinder<'a> { @@ -145,13 +151,88 @@ impl<'hir> LoweringContext<'_, 'hir> { let (pat, binding) = self.pat_ident(inner.span, ident); let Some(state) = self.move_expr_bindings.last_mut().and_then(|state| state.as_mut()) else { - span_bug!(move_kw_span, "`move(...)` lowered without a plain closure body state"); + span_bug!(move_kw_span, "`move(...)` lowered without a closure-like body state"); }; state.bindings.insert(id, (ident, binding)); state.occurrences.push(MoveExprOccurrence { id, ident, pat, binding, explicit_capture }); (ident, binding) } + fn lower_expr_with_move_exprs( + &mut self, + expr: hir::Expr<'hir>, + move_expr_state: MoveExprState<'hir>, + body: &Expr, + whole_span: Span, + ) -> hir::Expr<'hir> { + let initializers = MoveExprInitializerFinder::collect(body); + self.lower_expr_with_move_expr_initializers(expr, move_expr_state, initializers, whole_span) + } + + fn lower_expr_with_move_exprs_in_block( + &mut self, + expr: hir::Expr<'hir>, + move_expr_state: MoveExprState<'hir>, + body: &Block, + whole_span: Span, + ) -> hir::Expr<'hir> { + let initializers = MoveExprInitializerFinder::collect_block(body); + self.lower_expr_with_move_expr_initializers(expr, move_expr_state, initializers, whole_span) + } + + fn lower_expr_with_move_expr_initializers( + &mut self, + expr: hir::Expr<'hir>, + move_expr_state: MoveExprState<'hir>, + initializers: Vec>, + whole_span: Span, + ) -> hir::Expr<'hir> { + if move_expr_state.occurrences.is_empty() { + return expr; + } + + let initializers = initializers + .into_iter() + .map(|initializer| (initializer.id, initializer.expr)) + .collect::>(); + let mut stmts = Vec::with_capacity(move_expr_state.occurrences.len()); + let mut initializer_bindings = NodeMap::default(); + for occurrence in &move_expr_state.occurrences { + // Evaluate the expression inside `move(...)` before creating the + // closure/coroutine and store it in a synthetic local: + // `|| move(foo).bar` becomes roughly + // `let __move_expr_0 = foo; || __move_expr_0.bar`. + let expr = initializers[&occurrence.id]; + let init = if initializer_bindings.is_empty() { + self.lower_expr(expr) + } else { + // Earlier entries cover nested `move(...)` expressions that + // appear inside this initializer, as in + // `move(move(foo.clone()))`. + let (init, _) = self.with_move_expr_bindings( + Some(MoveExprState { + bindings: initializer_bindings.clone(), + occurrences: Vec::new(), + }), + |this| this.lower_expr(expr), + ); + init + }; + stmts.push(self.stmt_let_pat( + None, + expr.span, + Some(init), + occurrence.pat, + hir::LocalSource::Normal, + )); + initializer_bindings.insert(occurrence.id, (occurrence.ident, occurrence.binding)); + } + + let stmts = self.arena.alloc_from_iter(stmts); + let block = self.block_all(whole_span, stmts, Some(self.arena.alloc(expr))); + self.expr(whole_span, hir::ExprKind::Block(block, None)) + } + fn lower_exprs(&mut self, exprs: &[Box]) -> &'hir [hir::Expr<'hir>] { self.arena.alloc_from_iter(exprs.iter().map(|x| self.lower_expr_mut(x))) } @@ -334,8 +415,9 @@ impl<'hir> LoweringContext<'_, 'hir> { }), )) } else { - let guar = - self.dcx().emit_err(MoveExprOnlyInPlainClosures { span: *move_kw_span }); + let guar = self + .dcx() + .emit_err(MoveExprOnlyInSupportedContexts { span: *move_kw_span }); hir::ExprKind::Err(guar) } } @@ -346,22 +428,34 @@ impl<'hir> LoweringContext<'_, 'hir> { CoroutineKind::Gen => hir::CoroutineDesugaring::Gen, CoroutineKind::AsyncGen => hir::CoroutineDesugaring::AsyncGen, }; - self.make_desugared_coroutine_expr( - *capture_clause, - e.id, - None, - *decl_span, + let (kind, move_expr_state) = + self.with_move_expr_bindings(Some(MoveExprState::default()), |this| { + this.make_desugared_coroutine_expr( + *capture_clause, + e.id, + None, + *decl_span, + e.span, + desugaring_kind, + hir::CoroutineSource::Block, + |this| { + this.with_new_scopes(e.span, |this| this.lower_block_expr(block)) + }, + ) + }); + let Some(move_expr_state) = move_expr_state else { + span_bug!( + *decl_span, + "coroutine block lowering did not return `move(...)` state" + ); + }; + let expr = hir::Expr { hir_id: expr_hir_id, kind, span }; + return self.lower_expr_with_move_exprs_in_block( + expr, + move_expr_state, + block, e.span, - desugaring_kind, - hir::CoroutineSource::Block, - |this| { - this.with_new_scopes(e.span, |this| { - let (expr, _) = this - .with_move_expr_bindings(None, |this| this.lower_block_expr(block)); - expr - }) - }, - ) + ); } ExprKind::Block(blk, opt_label) => { // Different from loops, label of block resolves to block id rather than diff --git a/compiler/rustc_ast_lowering/src/expr/closure.rs b/compiler/rustc_ast_lowering/src/expr/closure.rs index 95f54ba281e17..a929ebad3302f 100644 --- a/compiler/rustc_ast_lowering/src/expr/closure.rs +++ b/compiler/rustc_ast_lowering/src/expr/closure.rs @@ -1,11 +1,10 @@ -use rustc_ast::node_id::NodeMap; use rustc_ast::*; use rustc_hir as hir; use rustc_hir::{HirId, Target, find_attr}; use rustc_middle::span_bug; use rustc_span::Span; -use super::{LoweringContext, MoveExprInitializerFinder, MoveExprState}; +use super::{LoweringContext, MoveExprState}; use crate::FnDeclKind; use crate::diagnostics::{ClosureCannotBeStatic, CoroutineTooManyParameters}; @@ -124,59 +123,6 @@ impl<'hir> LoweringContext<'_, 'hir> { self.lower_expr_with_move_exprs(closure_expr, move_expr_state, body, whole_span) } - fn lower_expr_with_move_exprs( - &mut self, - expr: hir::Expr<'hir>, - move_expr_state: MoveExprState<'hir>, - body: &Expr, - whole_span: Span, - ) -> hir::Expr<'hir> { - if move_expr_state.occurrences.is_empty() { - return expr; - } - - let initializers = MoveExprInitializerFinder::collect(body) - .into_iter() - .map(|initializer| (initializer.id, initializer.expr)) - .collect::>(); - let mut stmts = Vec::with_capacity(move_expr_state.occurrences.len()); - let mut initializer_bindings = NodeMap::default(); - for occurrence in &move_expr_state.occurrences { - // Evaluate the expression inside `move(...)` before creating the - // closure and store it in a synthetic local: - // `|| move(foo).bar` becomes roughly - // `let __move_expr_0 = foo; || __move_expr_0.bar`. - let expr = initializers[&occurrence.id]; - let init = if initializer_bindings.is_empty() { - self.lower_expr(expr) - } else { - // Earlier entries cover nested `move(...)` expressions that - // appear inside this initializer, as in - // `move(move(foo.clone()))`. - let (init, _) = self.with_move_expr_bindings( - Some(MoveExprState { - bindings: initializer_bindings.clone(), - occurrences: Vec::new(), - }), - |this| this.lower_expr(expr), - ); - init - }; - stmts.push(self.stmt_let_pat( - None, - expr.span, - Some(init), - occurrence.pat, - hir::LocalSource::Normal, - )); - initializer_bindings.insert(occurrence.id, (occurrence.ident, occurrence.binding)); - } - - let stmts = self.arena.alloc_from_iter(stmts); - let block = self.block_all(whole_span, stmts, Some(self.arena.alloc(expr))); - self.expr(whole_span, hir::ExprKind::Block(block, None)) - } - // Lowers the actual plain closure node and body. The body is lowered while a // `MoveExprState` is active, so `move(...)` occurrences become synthetic // local uses and the caller can later add the matching initializers. From f5c36e81abbcc9b7fb2faf0465908d1cad3ba0be Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Tue, 23 Jun 2026 02:25:28 +0900 Subject: [PATCH 06/13] support move expr in coroutine blocks --- compiler/rustc_ast_lowering/src/diagnostics.rs | 4 ++-- tests/ui/move-expr/outside-plain-closure.rs | 2 +- tests/ui/move-expr/outside-plain-closure.stderr | 2 +- tests/ui/move-expr/parse-ambiguity-errors.rs | 2 +- tests/ui/move-expr/parse-ambiguity-errors.stderr | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/compiler/rustc_ast_lowering/src/diagnostics.rs b/compiler/rustc_ast_lowering/src/diagnostics.rs index aa8550f9b99ed..b18359d9b14da 100644 --- a/compiler/rustc_ast_lowering/src/diagnostics.rs +++ b/compiler/rustc_ast_lowering/src/diagnostics.rs @@ -148,8 +148,8 @@ pub(crate) struct ClosureCannotBeStatic { } #[derive(Diagnostic)] -#[diag("`move(expr)` is only supported in closures")] -pub(crate) struct MoveExprOnlyInPlainClosures { +#[diag("`move(expr)` is only supported in closures, `async`, `gen`, and `async gen` blocks")] +pub(crate) struct MoveExprOnlyInSupportedContexts { #[primary_span] pub span: Span, } diff --git a/tests/ui/move-expr/outside-plain-closure.rs b/tests/ui/move-expr/outside-plain-closure.rs index 64bf8374d92d0..881c00d32aa11 100644 --- a/tests/ui/move-expr/outside-plain-closure.rs +++ b/tests/ui/move-expr/outside-plain-closure.rs @@ -3,5 +3,5 @@ fn main() { let _ = move(String::from("nope")); - //~^ ERROR `move(expr)` is only supported in closures + //~^ ERROR `move(expr)` is only supported in closures, `async`, `gen`, and `async gen` blocks } diff --git a/tests/ui/move-expr/outside-plain-closure.stderr b/tests/ui/move-expr/outside-plain-closure.stderr index c84d71a3579c3..8654f52bf4ac4 100644 --- a/tests/ui/move-expr/outside-plain-closure.stderr +++ b/tests/ui/move-expr/outside-plain-closure.stderr @@ -1,4 +1,4 @@ -error: `move(expr)` is only supported in closures +error: `move(expr)` is only supported in closures, `async`, `gen`, and `async gen` blocks --> $DIR/outside-plain-closure.rs:5:13 | LL | let _ = move(String::from("nope")); diff --git a/tests/ui/move-expr/parse-ambiguity-errors.rs b/tests/ui/move-expr/parse-ambiguity-errors.rs index 68b64d1582ff8..c9428770538f7 100644 --- a/tests/ui/move-expr/parse-ambiguity-errors.rs +++ b/tests/ui/move-expr/parse-ambiguity-errors.rs @@ -5,7 +5,7 @@ fn main() { let x: bool = true; let y: bool = true; let _ = move(x) || y; - //~^ ERROR `move(expr)` is only supported in closures + //~^ ERROR `move(expr)` is only supported in closures, `async`, `gen`, and `async gen` blocks let x: bool = true; let y: bool = true; diff --git a/tests/ui/move-expr/parse-ambiguity-errors.stderr b/tests/ui/move-expr/parse-ambiguity-errors.stderr index 5990bf861f609..17a397cc26900 100644 --- a/tests/ui/move-expr/parse-ambiguity-errors.stderr +++ b/tests/ui/move-expr/parse-ambiguity-errors.stderr @@ -4,7 +4,7 @@ error: expected one of `async`, `|`, or `||`, found `[` LL | let _ = move[x] || y; | ^ expected one of `async`, `|`, or `||` -error: `move(expr)` is only supported in closures +error: `move(expr)` is only supported in closures, `async`, `gen`, and `async gen` blocks --> $DIR/parse-ambiguity-errors.rs:7:13 | LL | let _ = move(x) || y; From 7541f068053593aaca20ae0f02f5d7a2972a0dd0 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Tue, 23 Jun 2026 02:25:54 +0900 Subject: [PATCH 07/13] add coroutine block move expr tests --- tests/ui/move-expr/async-blocks.rs | 41 ++++++++++ tests/ui/move-expr/async-gen-blocks.rs | 100 +++++++++++++++++++++++++ tests/ui/move-expr/gen-blocks.rs | 58 ++++++++++++++ 3 files changed, 199 insertions(+) create mode 100644 tests/ui/move-expr/async-blocks.rs create mode 100644 tests/ui/move-expr/async-gen-blocks.rs create mode 100644 tests/ui/move-expr/gen-blocks.rs diff --git a/tests/ui/move-expr/async-blocks.rs b/tests/ui/move-expr/async-blocks.rs new file mode 100644 index 0000000000000..e6bee3b3c6c06 --- /dev/null +++ b/tests/ui/move-expr/async-blocks.rs @@ -0,0 +1,41 @@ +//@ edition: 2021 +//@ run-pass +#![allow(incomplete_features)] +#![feature(move_expr)] + +use std::cell::Cell; +use std::sync::Arc; + +fn main() { + let created = Cell::new(0); + let fut = async { + let n = move({ + created.set(created.get() + 1); + created.get() + }); + n + }; + assert_eq!(created.get(), 1); + drop(fut); + + let x = Arc::new(String::from("hello")); + assert_eq!(Arc::strong_count(&x), 1); + let fut = async { move(x.clone()) }; + assert_eq!(Arc::strong_count(&x), 2); + drop(fut); + assert_eq!(Arc::strong_count(&x), 1); + + let y = Arc::new(String::from("nested")); + assert_eq!(Arc::strong_count(&y), 1); + let fut = async { move(move(y.clone())) }; + assert_eq!(Arc::strong_count(&y), 2); + drop(fut); + assert_eq!(Arc::strong_count(&y), 1); + + let z = Arc::new(String::from("async move")); + assert_eq!(Arc::strong_count(&z), 1); + let fut = async move { move(z.clone()) }; + assert_eq!(Arc::strong_count(&z), 2); + drop(fut); + assert_eq!(Arc::strong_count(&z), 1); +} diff --git a/tests/ui/move-expr/async-gen-blocks.rs b/tests/ui/move-expr/async-gen-blocks.rs new file mode 100644 index 0000000000000..4037b276ebcb7 --- /dev/null +++ b/tests/ui/move-expr/async-gen-blocks.rs @@ -0,0 +1,100 @@ +//@ edition: 2024 +//@ run-pass +#![allow(incomplete_features)] +#![feature(async_iterator, gen_blocks, move_expr)] + +use std::async_iter::AsyncIterator; +use std::cell::Cell; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll, Waker}; + +struct PendingOnce { + pending: bool, +} + +impl PendingOnce { + fn new() -> Self { + Self { pending: true } + } +} + +impl Future for PendingOnce { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { + if self.pending { + self.pending = false; + cx.waker().wake_by_ref(); + Poll::Pending + } else { + Poll::Ready(()) + } + } +} + +fn poll_next(iter: Pin<&mut I>) -> Poll> { + let cx = &mut Context::from_waker(Waker::noop()); + AsyncIterator::poll_next(iter, cx) +} + +fn ready_next(iter: Pin<&mut I>) -> Option { + match poll_next(iter) { + Poll::Ready(item) => item, + Poll::Pending => panic!("async iterator unexpectedly returned pending"), + } +} + +fn main() { + let created = Cell::new(0); + let mut iter = Box::pin(async gen { + let n = move({ + created.set(created.get() + 1); + created.get() + }); + yield n; + yield n + 1; + }); + assert_eq!(created.get(), 1); + assert_eq!(ready_next(iter.as_mut()), Some(1)); + assert_eq!(ready_next(iter.as_mut()), Some(2)); + assert_eq!(ready_next(iter.as_mut()), None); + + let x = Arc::new(String::from("hello")); + assert_eq!(Arc::strong_count(&x), 1); + let mut iter = Box::pin(async gen { + let value = move(x.clone()); + yield Arc::strong_count(&value); + PendingOnce::new().await; + yield Arc::strong_count(&value); + }); + assert_eq!(Arc::strong_count(&x), 2); + assert_eq!(ready_next(iter.as_mut()), Some(2)); + assert!(matches!(poll_next(iter.as_mut()), Poll::Pending)); + assert_eq!(ready_next(iter.as_mut()), Some(2)); + drop(iter); + assert_eq!(Arc::strong_count(&x), 1); + + let y = Arc::new(String::from("nested")); + assert_eq!(Arc::strong_count(&y), 1); + let mut iter = Box::pin(async gen { + let value = move(move(y.clone())); + yield Arc::strong_count(&value); + }); + assert_eq!(Arc::strong_count(&y), 2); + assert_eq!(ready_next(iter.as_mut()), Some(2)); + drop(iter); + assert_eq!(Arc::strong_count(&y), 1); + + let z = Arc::new(String::from("async gen move")); + assert_eq!(Arc::strong_count(&z), 1); + let mut iter = Box::pin(async gen move { + let value = move(z.clone()); + yield Arc::strong_count(&value); + }); + assert_eq!(Arc::strong_count(&z), 2); + assert_eq!(ready_next(iter.as_mut()), Some(2)); + drop(iter); + assert_eq!(Arc::strong_count(&z), 1); +} diff --git a/tests/ui/move-expr/gen-blocks.rs b/tests/ui/move-expr/gen-blocks.rs new file mode 100644 index 0000000000000..c6cec6491cc6e --- /dev/null +++ b/tests/ui/move-expr/gen-blocks.rs @@ -0,0 +1,58 @@ +//@ edition: 2024 +//@ run-pass +#![allow(incomplete_features)] +#![feature(gen_blocks, move_expr)] + +use std::cell::Cell; +use std::sync::Arc; + +fn main() { + let created = Cell::new(0); + let mut iter = gen { + let n = move({ + created.set(created.get() + 1); + created.get() + }); + yield n; + yield n + 1; + }; + assert_eq!(created.get(), 1); + assert_eq!(iter.next(), Some(1)); + assert_eq!(iter.next(), Some(2)); + assert_eq!(iter.next(), None); + + let x = Arc::new(String::from("hello")); + assert_eq!(Arc::strong_count(&x), 1); + let mut iter = gen { + let value = move(x.clone()); + yield Arc::strong_count(&value); + yield Arc::strong_count(&value); + }; + assert_eq!(Arc::strong_count(&x), 2); + assert_eq!(iter.next(), Some(2)); + assert_eq!(iter.next(), Some(2)); + drop(iter); + assert_eq!(Arc::strong_count(&x), 1); + + let y = Arc::new(String::from("nested")); + assert_eq!(Arc::strong_count(&y), 1); + let mut iter = gen { + let value = move(move(y.clone())); + yield Arc::strong_count(&value); + }; + assert_eq!(Arc::strong_count(&y), 2); + assert_eq!(iter.next(), Some(2)); + drop(iter); + assert_eq!(Arc::strong_count(&y), 1); + + let z = Arc::new(String::from("gen move")); + assert_eq!(Arc::strong_count(&z), 1); + let mut iter = gen move { + let value = move(z.clone()); + yield Arc::strong_count(&value); + }; + assert_eq!(Arc::strong_count(&z), 2); + assert_eq!(iter.next(), Some(2)); + drop(iter); + assert_eq!(Arc::strong_count(&z), 1); +} From e831c5324f3047afc34a35e3093419b1f96e4192 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Wed, 12 Aug 2026 18:56:06 +0900 Subject: [PATCH 08/13] fix nested move expression lowering across capture contexts --- compiler/rustc_ast_lowering/src/expr.rs | 82 +++---------- .../rustc_ast_lowering/src/expr/closure.rs | 113 ++++++++++++------ compiler/rustc_ast_lowering/src/lib.rs | 2 +- 3 files changed, 93 insertions(+), 104 deletions(-) diff --git a/compiler/rustc_ast_lowering/src/expr.rs b/compiler/rustc_ast_lowering/src/expr.rs index 2bf239545bd81..faae4f98ae854 100644 --- a/compiler/rustc_ast_lowering/src/expr.rs +++ b/compiler/rustc_ast_lowering/src/expr.rs @@ -36,30 +36,20 @@ pub(super) struct WillCreateDefIdsVisitor; struct MoveExprInitializer<'a> { /// The `NodeId` of the outer `move(...)` expression. id: NodeId, - /// Span of the `move` token, used for the generated binding name. - move_kw_span: Span, /// The expression inside `move(...)`; e.g. `foo.bar` in `move(foo.bar)`. expr: &'a Expr, } /// State for `move(...)` expressions found while lowering one closure-like body. +#[derive(Default)] pub(super) struct MoveExprState<'hir> { - pub(super) bindings: NodeMap<(Ident, HirId)>, pub(super) occurrences: Vec>, } -impl<'hir> Default for MoveExprState<'hir> { - fn default() -> Self { - Self { bindings: NodeMap::default(), occurrences: Vec::new() } - } -} - pub(super) struct MoveExprOccurrence<'hir> { id: NodeId, - ident: Ident, pat: &'hir hir::Pat<'hir>, binding: HirId, - explicit_capture: bool, } /// Looks up the initializer expression for each `move(...)` occurrence. @@ -84,15 +74,11 @@ impl<'a> MoveExprInitializerFinder<'a> { impl<'a> Visitor<'a> for MoveExprInitializerFinder<'a> { fn visit_expr(&mut self, expr: &'a Expr) { match &expr.kind { - ExprKind::Move(inner, move_kw_span) => { + ExprKind::Move(inner, _) => { self.visit_expr(inner); - self.initializers.push(MoveExprInitializer { - id: expr.id, - move_kw_span: *move_kw_span, - expr: inner, - }); + self.initializers.push(MoveExprInitializer { id: expr.id, expr: inner }); } - ExprKind::Closure(..) | ExprKind::Gen(..) | ExprKind::ConstBlock(..) => {} + ExprKind::ConstBlock(..) => {} _ => walk_expr(self, expr), } } @@ -135,13 +121,7 @@ impl<'hir> LoweringContext<'_, 'hir> { (result, state) } - fn record_move_expr( - &mut self, - id: NodeId, - inner: &Expr, - move_kw_span: Span, - explicit_capture: bool, - ) -> (Ident, HirId) { + fn record_move_expr(&mut self, id: NodeId, inner: &Expr, move_kw_span: Span) -> (Ident, HirId) { let index = self .move_expr_bindings .last() @@ -153,8 +133,7 @@ impl<'hir> LoweringContext<'_, 'hir> { else { span_bug!(move_kw_span, "`move(...)` lowered without a closure-like body state"); }; - state.bindings.insert(id, (ident, binding)); - state.occurrences.push(MoveExprOccurrence { id, ident, pat, binding, explicit_capture }); + state.occurrences.push(MoveExprOccurrence { id, pat, binding }); (ident, binding) } @@ -196,28 +175,16 @@ impl<'hir> LoweringContext<'_, 'hir> { .map(|initializer| (initializer.id, initializer.expr)) .collect::>(); let mut stmts = Vec::with_capacity(move_expr_state.occurrences.len()); - let mut initializer_bindings = NodeMap::default(); for occurrence in &move_expr_state.occurrences { // Evaluate the expression inside `move(...)` before creating the // closure/coroutine and store it in a synthetic local: // `|| move(foo).bar` becomes roughly // `let __move_expr_0 = foo; || __move_expr_0.bar`. let expr = initializers[&occurrence.id]; - let init = if initializer_bindings.is_empty() { - self.lower_expr(expr) - } else { - // Earlier entries cover nested `move(...)` expressions that - // appear inside this initializer, as in - // `move(move(foo.clone()))`. - let (init, _) = self.with_move_expr_bindings( - Some(MoveExprState { - bindings: initializer_bindings.clone(), - occurrences: Vec::new(), - }), - |this| this.lower_expr(expr), - ); - init - }; + // This state has already been popped, so a nested `move(...)` in + // the initializer is recorded by the immediately enclosing + // closure-like body instead of this one. + let init = self.lower_expr(expr); stmts.push(self.stmt_let_pat( None, expr.span, @@ -225,7 +192,6 @@ impl<'hir> LoweringContext<'_, 'hir> { occurrence.pat, hir::LocalSource::Normal, )); - initializer_bindings.insert(occurrence.id, (occurrence.ident, occurrence.binding)); } let stmts = self.arena.alloc_from_iter(stmts); @@ -386,19 +352,8 @@ impl<'hir> LoweringContext<'_, 'hir> { if !self.tcx.features().move_expr() { return self.expr_err(*move_kw_span, self.dcx().has_errors().unwrap()); } - if let Some(state) = self.move_expr_bindings.last().and_then(Option::as_ref) { - let existing = state.bindings.get(&e.id).copied(); - let (ident, binding) = existing.unwrap_or_else(|| { - for nested in MoveExprInitializerFinder::collect(inner) { - self.record_move_expr( - nested.id, - nested.expr, - nested.move_kw_span, - false, - ); - } - self.record_move_expr(e.id, inner, *move_kw_span, true) - }); + if self.move_expr_bindings.last().is_some_and(Option::is_some) { + let (ident, binding) = self.record_move_expr(e.id, inner, *move_kw_span); hir::ExprKind::Path(hir::QPath::Resolved( None, self.arena.alloc(hir::Path { @@ -962,13 +917,12 @@ impl<'hir> LoweringContext<'_, 'hir> { let explicit_captures: &'hir [hir::ExplicitCapture] = if let Some(move_expr_state) = self.move_expr_bindings.last().and_then(Option::as_ref) { - self.arena.alloc_from_iter(move_expr_state.occurrences.iter().filter_map( - |occurrence| { - occurrence - .explicit_capture - .then_some(hir::ExplicitCapture { var_hir_id: occurrence.binding }) - }, - )) + self.arena.alloc_from_iter( + move_expr_state + .occurrences + .iter() + .map(|occurrence| hir::ExplicitCapture { var_hir_id: occurrence.binding }), + ) } else { &[] }; diff --git a/compiler/rustc_ast_lowering/src/expr/closure.rs b/compiler/rustc_ast_lowering/src/expr/closure.rs index a929ebad3302f..3d28b5e92ad3f 100644 --- a/compiler/rustc_ast_lowering/src/expr/closure.rs +++ b/compiler/rustc_ast_lowering/src/expr/closure.rs @@ -22,23 +22,20 @@ impl<'hir> LoweringContext<'_, 'hir> { let attrs = self.lower_attrs(expr_hir_id, &e.attrs, e.span, Target::from_expr(e)); match closure.coroutine_marker { - Some(coroutine_marker) => hir::Expr { - hir_id: expr_hir_id, - kind: self.lower_expr_coroutine_closure( - &closure.binder, - closure.capture_clause, - e.id, - expr_hir_id, - coroutine_marker, - closure.constness, - &closure.fn_decl, - &closure.body, - closure.fn_decl_span, - closure.fn_arg_span, - attrs, - ), - span: self.lower_span(e.span), - }, + Some(coroutine_marker) => self.lower_expr_coroutine_closure_with_move_exprs( + expr_hir_id, + attrs, + &closure.binder, + closure.capture_clause, + e.id, + coroutine_marker, + closure.constness, + &closure.fn_decl, + &closure.body, + closure.fn_decl_span, + closure.fn_arg_span, + e.span, + ), None => self.lower_expr_plain_closure_with_move_exprs( expr_hir_id, attrs, @@ -56,6 +53,46 @@ impl<'hir> LoweringContext<'_, 'hir> { } } + fn lower_expr_coroutine_closure_with_move_exprs( + &mut self, + expr_hir_id: HirId, + attrs: &[hir::Attribute], + binder: &ClosureBinder, + capture_clause: CaptureBy, + closure_id: NodeId, + coroutine_marker: CoroutineMarker, + constness: Const, + decl: &FnDecl, + body: &Expr, + fn_decl_span: Span, + fn_arg_span: Span, + whole_span: Span, + ) -> hir::Expr<'hir> { + let (kind, move_expr_state) = + self.with_move_expr_bindings(Some(MoveExprState::default()), |this| { + this.lower_expr_coroutine_closure( + binder, + capture_clause, + closure_id, + expr_hir_id, + coroutine_marker, + constness, + decl, + body, + fn_decl_span, + fn_arg_span, + attrs, + ) + }); + let Some(move_expr_state) = move_expr_state else { + span_bug!(fn_decl_span, "coroutine closure lowering did not return `move(...)` state"); + }; + let closure_expr = + hir::Expr { hir_id: expr_hir_id, kind, span: self.lower_span(whole_span) }; + + self.lower_expr_with_move_exprs(closure_expr, move_expr_state, body, whole_span) + } + /// Lowers a plain closure expression and wraps it in an outer block if the /// closure body used `move(...)`. /// @@ -64,28 +101,18 @@ impl<'hir> LoweringContext<'_, 'hir> { /// lower each `move(...)` occurrence as a use of the synthetic local that /// will be introduced by that outer block. For example: /// - /// ```ignore (illustrative) - /// || (move(move(foo.clone()))).len() - /// ``` - /// - /// first lowers the closure body roughly as `|| __move_expr_1.len()` while - /// recording two occurrences: - /// - /// ```ignore (illustrative) - /// move(foo.clone()) -> __move_expr_0 - /// move(move(foo.clone())) -> __move_expr_1 - /// ``` - /// - /// This method then lowers the recorded initializers in order and builds the - /// surrounding block: + /// For example, `|| move(foo.clone()).len()` becomes roughly: /// /// ```ignore (illustrative) /// { /// let __move_expr_0 = foo.clone(); - /// let __move_expr_1 = __move_expr_0; - /// || __move_expr_1.len() + /// || __move_expr_0.len() /// } /// ``` + /// + /// If the initializer contains another `move(...)`, it is lowered after + /// this closure's state is popped and therefore belongs to the immediately + /// enclosing closure-like body. fn lower_expr_plain_closure_with_move_exprs( &mut self, expr_hir_id: HirId, @@ -170,11 +197,10 @@ impl<'hir> LoweringContext<'_, 'hir> { span_bug!(fn_decl_span, "plain closure lowering did not return `move(...)` state"); }; let explicit_captures: &'hir [hir::ExplicitCapture] = self.arena.alloc_from_iter( - move_expr_state.occurrences.iter().filter_map(|occurrence| { - occurrence - .explicit_capture - .then_some(hir::ExplicitCapture { var_hir_id: occurrence.binding }) - }), + move_expr_state + .occurrences + .iter() + .map(|occurrence| hir::ExplicitCapture { var_hir_id: occurrence.binding }), ); let bound_generic_params = self.lower_lifetime_binder(closure_id, generic_params); @@ -322,6 +348,15 @@ impl<'hir> LoweringContext<'_, 'hir> { self.dcx().span_err(span, "const coroutines are not supported"); } + let explicit_captures: &'hir [hir::ExplicitCapture] = self.arena.alloc_from_iter( + self.move_expr_bindings + .last() + .and_then(Option::as_ref) + .into_iter() + .flat_map(|state| &state.occurrences) + .map(|occurrence| hir::ExplicitCapture { var_hir_id: occurrence.binding }), + ); + let c = self.arena.alloc(hir::Closure { def_id: closure_def_id, binder: binder_clause, @@ -336,7 +371,7 @@ impl<'hir> LoweringContext<'_, 'hir> { // "coroutine that returns &str", rather than directly returning a `&str`. kind: hir::ClosureKind::CoroutineClosure(coroutine_desugaring), constness: self.lower_constness(attrs, constness), - explicit_captures: &[], + explicit_captures, }); hir::ExprKind::Closure(c) } diff --git a/compiler/rustc_ast_lowering/src/lib.rs b/compiler/rustc_ast_lowering/src/lib.rs index ef6995d9c11d6..f5dc9d5203c13 100644 --- a/compiler/rustc_ast_lowering/src/lib.rs +++ b/compiler/rustc_ast_lowering/src/lib.rs @@ -320,7 +320,7 @@ struct LoweringContext<'a, 'hir> { allow_for_await: Arc<[Symbol]>, allow_async_fn_traits: Arc<[Symbol]>, - /// Stack of `move(...)` collection states. A plain closure body pushes + /// Stack of `move(...)` collection states. A closure-like body pushes /// `Some`, so `move(...)` expressions can record the generated locals they /// should lower to. Nested bodies that cannot use `move(...)` push `None`. move_expr_bindings: Vec>>, From b3d7ec7492f1aa8916328b4f7a37d5315d3cf6f8 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Wed, 12 Aug 2026 18:57:03 +0900 Subject: [PATCH 09/13] improve diagnostics for exhausted nested move expressions --- .../rustc_ast_lowering/src/diagnostics.rs | 9 +++++++++ compiler/rustc_ast_lowering/src/expr.rs | 20 ++++++++++++++++--- compiler/rustc_ast_lowering/src/lib.rs | 4 ++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/compiler/rustc_ast_lowering/src/diagnostics.rs b/compiler/rustc_ast_lowering/src/diagnostics.rs index b18359d9b14da..2a468cb60546d 100644 --- a/compiler/rustc_ast_lowering/src/diagnostics.rs +++ b/compiler/rustc_ast_lowering/src/diagnostics.rs @@ -154,6 +154,15 @@ pub(crate) struct MoveExprOnlyInSupportedContexts { pub span: Span, } +#[derive(Diagnostic)] +#[diag( + "nested `move(expr)` requires another enclosing closure, `async`, `gen`, or `async gen` block" +)] +pub(crate) struct NestedMoveExprWithoutEnclosingContext { + #[primary_span] + pub span: Span, +} + #[derive(Diagnostic)] #[diag("functional record updates are not allowed in destructuring assignments")] pub(crate) struct FunctionalRecordUpdateDestructuringAssignment { diff --git a/compiler/rustc_ast_lowering/src/expr.rs b/compiler/rustc_ast_lowering/src/expr.rs index faae4f98ae854..00176fa9886ee 100644 --- a/compiler/rustc_ast_lowering/src/expr.rs +++ b/compiler/rustc_ast_lowering/src/expr.rs @@ -22,8 +22,8 @@ use crate::diagnostics::{ AsyncCoroutinesNotSupported, AwaitOnlyInAsyncFnAndBlocks, FunctionalRecordUpdateDestructuringAssignment, InclusiveRangeWithNoEnd, InvalidLegacyConstGenericArg, MatchArmWithNoBody, MoveExprOnlyInSupportedContexts, - NeverPatternWithBody, NeverPatternWithGuard, UnderscoreExprLhsAssign, UseConstGenericArg, - YieldInClosure, + NestedMoveExprWithoutEnclosingContext, NeverPatternWithBody, NeverPatternWithGuard, + UnderscoreExprLhsAssign, UseConstGenericArg, YieldInClosure, }; use crate::{ AllowReturnTypeNotation, GenericArgsMode, ImplTraitContext, ImplTraitPosition, LoweringContext, @@ -121,6 +121,14 @@ impl<'hir> LoweringContext<'_, 'hir> { (result, state) } + fn with_move_expr_initializer(&mut self, f: impl FnOnce(&mut Self) -> T) -> T { + let old = self.lowering_move_expr_initializer; + self.lowering_move_expr_initializer = true; + let result = f(self); + self.lowering_move_expr_initializer = old; + result + } + fn record_move_expr(&mut self, id: NodeId, inner: &Expr, move_kw_span: Span) -> (Ident, HirId) { let index = self .move_expr_bindings @@ -184,7 +192,7 @@ impl<'hir> LoweringContext<'_, 'hir> { // This state has already been popped, so a nested `move(...)` in // the initializer is recorded by the immediately enclosing // closure-like body instead of this one. - let init = self.lower_expr(expr); + let init = self.with_move_expr_initializer(|this| this.lower_expr(expr)); stmts.push(self.stmt_let_pat( None, expr.span, @@ -369,6 +377,12 @@ impl<'hir> LoweringContext<'_, 'hir> { ], }), )) + } else if self.lowering_move_expr_initializer && self.move_expr_bindings.is_empty() + { + let guar = self + .dcx() + .emit_err(NestedMoveExprWithoutEnclosingContext { span: *move_kw_span }); + hir::ExprKind::Err(guar) } else { let guar = self .dcx() diff --git a/compiler/rustc_ast_lowering/src/lib.rs b/compiler/rustc_ast_lowering/src/lib.rs index f5dc9d5203c13..43eb398e38e56 100644 --- a/compiler/rustc_ast_lowering/src/lib.rs +++ b/compiler/rustc_ast_lowering/src/lib.rs @@ -325,6 +325,9 @@ struct LoweringContext<'a, 'hir> { /// should lower to. Nested bodies that cannot use `move(...)` push `None`. move_expr_bindings: Vec>>, + /// Whether an initializer for a recorded `move(...)` is currently being lowered. + lowering_move_expr_initializer: bool, + attribute_parser: AttributeParser<'hir>, } @@ -371,6 +374,7 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> { allow_async_iterator: [sym::gen_future, sym::async_iterator].into(), move_expr_bindings: Vec::new(), + lowering_move_expr_initializer: false, attribute_parser: AttributeParser::new( tcx.sess, tcx.features(), From 7fabedc462b3901f4c3147d0c5d2a9da473637d3 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Wed, 12 Aug 2026 18:58:50 +0900 Subject: [PATCH 10/13] add UI coverage for nested move expressions --- tests/ui/move-expr/async-blocks.rs | 48 ++++++++++++++++--- tests/ui/move-expr/async-closures.rs | 11 ++++- tests/ui/move-expr/async-gen-blocks.rs | 14 ++++-- tests/ui/move-expr/gen-blocks.rs | 14 ++++-- .../move-expr/nested-async-block-ownership.rs | 17 +++++++ .../nested-async-block-ownership.stderr | 23 +++++++++ tests/ui/move-expr/nested-move-exhausted.rs | 17 +++++++ .../ui/move-expr/nested-move-exhausted.stderr | 26 ++++++++++ tests/ui/move-expr/nested-move-expr.rs | 18 ++++--- 9 files changed, 165 insertions(+), 23 deletions(-) create mode 100644 tests/ui/move-expr/nested-async-block-ownership.rs create mode 100644 tests/ui/move-expr/nested-async-block-ownership.stderr create mode 100644 tests/ui/move-expr/nested-move-exhausted.rs create mode 100644 tests/ui/move-expr/nested-move-exhausted.stderr diff --git a/tests/ui/move-expr/async-blocks.rs b/tests/ui/move-expr/async-blocks.rs index e6bee3b3c6c06..4f211cc961572 100644 --- a/tests/ui/move-expr/async-blocks.rs +++ b/tests/ui/move-expr/async-blocks.rs @@ -4,7 +4,20 @@ #![feature(move_expr)] use std::cell::Cell; +use std::future::Future; use std::sync::Arc; +use std::task::{Context, Poll, Waker}; + +fn block_on(future: F) -> F::Output { + let mut future = Box::pin(future); + let cx = &mut Context::from_waker(Waker::noop()); + loop { + match future.as_mut().poll(cx) { + Poll::Ready(output) => return output, + Poll::Pending => {} + } + } +} fn main() { let created = Cell::new(0); @@ -25,12 +38,35 @@ fn main() { drop(fut); assert_eq!(Arc::strong_count(&x), 1); - let y = Arc::new(String::from("nested")); - assert_eq!(Arc::strong_count(&y), 1); - let fut = async { move(move(y.clone())) }; - assert_eq!(Arc::strong_count(&y), 2); - drop(fut); - assert_eq!(Arc::strong_count(&y), 1); + let y = Arc::new(String::from("nested once")); + let weak = Arc::downgrade(&y); + let fut = async { + let inner = async { + drop(move(y.clone())); + }; + assert_eq!(weak.strong_count(), 2); + inner.await; + assert_eq!(weak.strong_count(), 1); + drop(y); + }; + assert_eq!(weak.strong_count(), 1); + block_on(fut); + assert_eq!(weak.strong_count(), 0); + + let y = Arc::new(String::from("nested twice")); + let weak = Arc::downgrade(&y); + let fut = async { + let inner = async { + drop(move(move(y.clone()))); + }; + assert_eq!(weak.strong_count(), 2); + inner.await; + assert_eq!(weak.strong_count(), 1); + }; + assert_eq!(weak.strong_count(), 2); + block_on(fut); + assert_eq!(weak.strong_count(), 1); + assert_eq!(&*y, "nested twice"); let z = Arc::new(String::from("async move")); assert_eq!(Arc::strong_count(&z), 1); diff --git a/tests/ui/move-expr/async-closures.rs b/tests/ui/move-expr/async-closures.rs index 5f93915a9f877..0c5d651719dbe 100644 --- a/tests/ui/move-expr/async-closures.rs +++ b/tests/ui/move-expr/async-closures.rs @@ -29,5 +29,14 @@ fn main() { assert_eq!(Arc::strong_count(&x), 2); drop(fut); assert_eq!(Arc::strong_count(&x), 1); - assert_eq!(Arc::strong_count(&x), 1); + + let y = Arc::new(String::from("nested")); + assert_eq!(Arc::strong_count(&y), 1); + let c = async || move(move(y.clone())); + assert_eq!(Arc::strong_count(&y), 2); + let fut = c(); + assert_eq!(Arc::strong_count(&y), 2); + drop(fut); + assert_eq!(Arc::strong_count(&y), 1); + assert_eq!(&*y, "nested"); } diff --git a/tests/ui/move-expr/async-gen-blocks.rs b/tests/ui/move-expr/async-gen-blocks.rs index 4037b276ebcb7..f77123751e1d5 100644 --- a/tests/ui/move-expr/async-gen-blocks.rs +++ b/tests/ui/move-expr/async-gen-blocks.rs @@ -77,15 +77,19 @@ fn main() { assert_eq!(Arc::strong_count(&x), 1); let y = Arc::new(String::from("nested")); - assert_eq!(Arc::strong_count(&y), 1); + let weak = Arc::downgrade(&y); let mut iter = Box::pin(async gen { - let value = move(move(y.clone())); - yield Arc::strong_count(&value); + let mut inner = Box::pin(async gen { + let value = move(move(y.clone())); + yield Arc::strong_count(&value); + }); + yield ready_next(inner.as_mut()).unwrap(); }); - assert_eq!(Arc::strong_count(&y), 2); + assert_eq!(weak.strong_count(), 2); assert_eq!(ready_next(iter.as_mut()), Some(2)); drop(iter); - assert_eq!(Arc::strong_count(&y), 1); + assert_eq!(weak.strong_count(), 1); + assert_eq!(&*y, "nested"); let z = Arc::new(String::from("async gen move")); assert_eq!(Arc::strong_count(&z), 1); diff --git a/tests/ui/move-expr/gen-blocks.rs b/tests/ui/move-expr/gen-blocks.rs index c6cec6491cc6e..b38313b83a8f7 100644 --- a/tests/ui/move-expr/gen-blocks.rs +++ b/tests/ui/move-expr/gen-blocks.rs @@ -35,15 +35,19 @@ fn main() { assert_eq!(Arc::strong_count(&x), 1); let y = Arc::new(String::from("nested")); - assert_eq!(Arc::strong_count(&y), 1); + let weak = Arc::downgrade(&y); let mut iter = gen { - let value = move(move(y.clone())); - yield Arc::strong_count(&value); + let mut inner = gen { + let value = move(move(y.clone())); + yield Arc::strong_count(&value); + }; + yield inner.next().unwrap(); }; - assert_eq!(Arc::strong_count(&y), 2); + assert_eq!(weak.strong_count(), 2); assert_eq!(iter.next(), Some(2)); drop(iter); - assert_eq!(Arc::strong_count(&y), 1); + assert_eq!(weak.strong_count(), 1); + assert_eq!(&*y, "nested"); let z = Arc::new(String::from("gen move")); assert_eq!(Arc::strong_count(&z), 1); diff --git a/tests/ui/move-expr/nested-async-block-ownership.rs b/tests/ui/move-expr/nested-async-block-ownership.rs new file mode 100644 index 0000000000000..32dbd808861f8 --- /dev/null +++ b/tests/ui/move-expr/nested-async-block-ownership.rs @@ -0,0 +1,17 @@ +//@ edition: 2021 +#![allow(incomplete_features)] +#![feature(move_expr)] + +use std::sync::Arc; + +fn main() { + let c = Arc::new(String::new()); + let _future = async { + let f = async { + drop(move(c.clone())); + }; + f.await; + drop(c); + }; + println!("{c}"); //~ ERROR the type `Arc` does not implement `Copy` +} diff --git a/tests/ui/move-expr/nested-async-block-ownership.stderr b/tests/ui/move-expr/nested-async-block-ownership.stderr new file mode 100644 index 0000000000000..fb3030d995230 --- /dev/null +++ b/tests/ui/move-expr/nested-async-block-ownership.stderr @@ -0,0 +1,23 @@ +error[E0382]: the type `Arc` does not implement `Copy` + --> $DIR/nested-async-block-ownership.rs:16:16 + | +LL | let c = Arc::new(String::new()); + | - this move could be avoided by cloning the original `Arc`, which is inexpensive +LL | let _future = async { + | ----- value moved here +... +LL | drop(c); + | - variable moved due to use in coroutine +LL | }; +LL | println!("{c}"); + | ^ value borrowed here after move + | + = note: consider using `Arc::clone` +help: clone the value to increment its reference count + | +LL | drop(c.clone()); + | ++++++++ + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0382`. diff --git a/tests/ui/move-expr/nested-move-exhausted.rs b/tests/ui/move-expr/nested-move-exhausted.rs new file mode 100644 index 0000000000000..d809020d3af6b --- /dev/null +++ b/tests/ui/move-expr/nested-move-exhausted.rs @@ -0,0 +1,17 @@ +//@ edition: 2024 +#![allow(incomplete_features)] +#![feature(async_iterator, gen_blocks, move_expr)] + +fn main() { + let _ = || move(move(0)); + //~^ ERROR nested `move(expr)` requires another enclosing closure + + let _ = async { move(move(0)) }; + //~^ ERROR nested `move(expr)` requires another enclosing closure + + let _ = gen { move(move(0)) }; + //~^ ERROR nested `move(expr)` requires another enclosing closure + + let _ = async gen { move(move(0)) }; + //~^ ERROR nested `move(expr)` requires another enclosing closure +} diff --git a/tests/ui/move-expr/nested-move-exhausted.stderr b/tests/ui/move-expr/nested-move-exhausted.stderr new file mode 100644 index 0000000000000..9743a96b81ef3 --- /dev/null +++ b/tests/ui/move-expr/nested-move-exhausted.stderr @@ -0,0 +1,26 @@ +error: nested `move(expr)` requires another enclosing closure, `async`, `gen`, or `async gen` block + --> $DIR/nested-move-exhausted.rs:6:21 + | +LL | let _ = || move(move(0)); + | ^^^^ + +error: nested `move(expr)` requires another enclosing closure, `async`, `gen`, or `async gen` block + --> $DIR/nested-move-exhausted.rs:9:26 + | +LL | let _ = async { move(move(0)) }; + | ^^^^ + +error: nested `move(expr)` requires another enclosing closure, `async`, `gen`, or `async gen` block + --> $DIR/nested-move-exhausted.rs:12:24 + | +LL | let _ = gen { move(move(0)) }; + | ^^^^ + +error: nested `move(expr)` requires another enclosing closure, `async`, `gen`, or `async gen` block + --> $DIR/nested-move-exhausted.rs:15:30 + | +LL | let _ = async gen { move(move(0)) }; + | ^^^^ + +error: aborting due to 4 previous errors + diff --git a/tests/ui/move-expr/nested-move-expr.rs b/tests/ui/move-expr/nested-move-expr.rs index cf3364c50aad7..f3ca679641238 100644 --- a/tests/ui/move-expr/nested-move-expr.rs +++ b/tests/ui/move-expr/nested-move-expr.rs @@ -1,12 +1,18 @@ -//@ check-pass +//@ run-pass #![allow(incomplete_features)] #![feature(move_expr)] +use std::sync::Arc; + fn main() { - let v = "Hello, Ferris".to_string(); - let r = || { - || (move(move(v.clone()))).len() - }; + let v = Arc::new("Hello, Ferris".to_string()); + let outer = || || (move(move(v.clone()))).len(); + + assert_eq!(Arc::strong_count(&v), 2); + let inner = outer(); + assert_eq!(Arc::strong_count(&v), 2); + assert_eq!(inner(), v.len()); + assert_eq!(Arc::strong_count(&v), 1); - assert_eq!(r()(), v.len()); + println!("{v}"); } From 216cf9e084a7bcae5104aebd41879fd879c0765a Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 20 Aug 2026 22:42:54 +0900 Subject: [PATCH 11/13] evaluate coroutine-closure move expressions at closure creation --- compiler/rustc_ast_lowering/src/expr.rs | 24 ++-- .../rustc_ast_lowering/src/expr/closure.rs | 40 +++---- compiler/rustc_hir_analysis/src/collect.rs | 10 +- compiler/rustc_hir_typeck/src/upvar.rs | 106 ++++++++---------- compiler/rustc_middle/src/ty/closure.rs | 18 ++- compiler/rustc_middle/src/ty/mod.rs | 4 +- .../src/coroutine/by_move_body.rs | 12 +- 7 files changed, 91 insertions(+), 123 deletions(-) diff --git a/compiler/rustc_ast_lowering/src/expr.rs b/compiler/rustc_ast_lowering/src/expr.rs index 00176fa9886ee..1c14c645d474c 100644 --- a/compiler/rustc_ast_lowering/src/expr.rs +++ b/compiler/rustc_ast_lowering/src/expr.rs @@ -928,17 +928,19 @@ impl<'hir> LoweringContext<'_, 'hir> { (params, res) }); - let explicit_captures: &'hir [hir::ExplicitCapture] = if let Some(move_expr_state) = - self.move_expr_bindings.last().and_then(Option::as_ref) - { - self.arena.alloc_from_iter( - move_expr_state - .occurrences - .iter() - .map(|occurrence| hir::ExplicitCapture { var_hir_id: occurrence.binding }), - ) - } else { - &[] + let explicit_captures: &'hir [hir::ExplicitCapture] = match coroutine_source { + hir::CoroutineSource::Block + if let Some(move_expr_state) = + self.move_expr_bindings.last().and_then(Option::as_ref) => + { + self.arena.alloc_from_iter( + move_expr_state + .occurrences + .iter() + .map(|occurrence| hir::ExplicitCapture { var_hir_id: occurrence.binding }), + ) + } + _ => &[], }; // `static |<_task_context?>| -> { }`: diff --git a/compiler/rustc_ast_lowering/src/expr/closure.rs b/compiler/rustc_ast_lowering/src/expr/closure.rs index 3d28b5e92ad3f..8505d39a718c4 100644 --- a/compiler/rustc_ast_lowering/src/expr/closure.rs +++ b/compiler/rustc_ast_lowering/src/expr/closure.rs @@ -99,9 +99,8 @@ impl<'hir> LoweringContext<'_, 'hir> { /// The lowering is split this way because `move(...)` initializers must be /// evaluated before the closure is created, but the closure body must still /// lower each `move(...)` occurrence as a use of the synthetic local that - /// will be introduced by that outer block. For example: - /// - /// For example, `|| move(foo.clone()).len()` becomes roughly: + /// will be introduced by that outer block. For example, + /// `|| move(foo.clone()).len()` becomes roughly: /// /// ```ignore (illustrative) /// { @@ -270,9 +269,9 @@ impl<'hir> LoweringContext<'_, 'hir> { } // Coroutine closures are lowered separately because they build a different - // body shape. The source body is still lowered with `MoveExprState` active, - // so `move(...)` occurrences are collected and then hoisted to the outer - // closure body, immediately before the generated coroutine is created. + // body shape. The source body is lowered with the caller's `MoveExprState` + // active, so `move(...)` occurrences are collected and hoisted into a block + // around the outer closure expression. fn lower_expr_coroutine_closure( &mut self, binder: &ClosureBinder, @@ -308,27 +307,14 @@ impl<'hir> LoweringContext<'_, 'hir> { // Transform `async |x: u8| -> X { ... }` into // `|x: u8| || -> X { ... }`. let body_id = this.lower_body(|this| { - let ((parameters, expr), move_expr_state) = - this.with_move_expr_bindings(Some(MoveExprState::default()), |this| { - this.lower_coroutine_body_with_moved_arguments( - &inner_decl, - |this| { - this.with_new_scopes(fn_decl_span, |this| this.lower_expr_mut(body)) - }, - fn_decl_span, - body.span, - coroutine_marker, - hir::CoroutineSource::Closure, - ) - }); - let Some(move_expr_state) = move_expr_state else { - span_bug!( - fn_decl_span, - "coroutine closure lowering did not return `move(...)` state" - ); - }; - - let expr = this.lower_expr_with_move_exprs(expr, move_expr_state, body, body.span); + let (parameters, expr) = this.lower_coroutine_body_with_moved_arguments( + &inner_decl, + |this| this.with_new_scopes(fn_decl_span, |this| this.lower_expr_mut(body)), + fn_decl_span, + body.span, + coroutine_marker, + hir::CoroutineSource::Closure, + ); this.maybe_forward_track_caller(closure_hir_id, expr.hir_id); diff --git a/compiler/rustc_hir_analysis/src/collect.rs b/compiler/rustc_hir_analysis/src/collect.rs index 2f61ed9be30b8..2e4da8d948f07 100644 --- a/compiler/rustc_hir_analysis/src/collect.rs +++ b/compiler/rustc_hir_analysis/src/collect.rs @@ -1709,14 +1709,6 @@ fn coroutine_for_closure(tcx: TyCtxt<'_>, def_id: LocalDefId) -> DefId { bug!() }; - let body = tcx.hir_body(body).value; - // `move(...)` in coroutine closures wraps the generated coroutine in an - // outer block of synthetic initializer lets. - let body = match body.kind { - hir::ExprKind::Block(block, None) if let Some(tail) = block.expr => tail, - _ => body, - }; - let &hir::Expr { kind: hir::ExprKind::Closure(&rustc_hir::Closure { @@ -1725,7 +1717,7 @@ fn coroutine_for_closure(tcx: TyCtxt<'_>, def_id: LocalDefId) -> DefId { .. }), .. - } = body + } = tcx.hir_body(body).value else { bug!() }; diff --git a/compiler/rustc_hir_typeck/src/upvar.rs b/compiler/rustc_hir_typeck/src/upvar.rs index c9dc0f33da8e2..167cb1f272533 100644 --- a/compiler/rustc_hir_typeck/src/upvar.rs +++ b/compiler/rustc_hir_typeck/src/upvar.rs @@ -290,18 +290,12 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> { // moved, and so on. let _ = euv::ExprUseVisitor::new(&closure_fcx, &mut delegate).consume_body(body); - // `consume_body` only sees how the lowered closure body uses those - // places. For `move(foo).clone()`, the body may only borrow the - // synthetic local for `foo`, but the source `move(...)` still requires - // capturing that local by value. + // Save the captures that must be upgraded to by-value after inferring + // the closure kind from the operations in the body. let explicit_captures = match self.tcx.hir_node(closure_hir_id).expect_expr().kind { hir::ExprKind::Closure(closure) => closure.explicit_captures, _ => bug!("expected closure expr for {:?}", closure_hir_id), }; - for capture in explicit_captures { - let place = closure_fcx.place_for_root_variable(closure_def_id, capture.var_hir_id); - delegate.consume(&PlaceWithHirId { hir_id: capture.var_hir_id, place }, closure_hir_id); - } // There are several curious situations with coroutine-closures where // analysis is too aggressive with borrows when the coroutine-closure is @@ -400,9 +394,25 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> { self.log_capture_analysis_first_pass(closure_def_id, &delegate.capture_information, span); - let (capture_information, closure_kind, origin) = self + let (mut capture_information, closure_kind, origin) = self .process_collected_capture_information(capture_clause, &delegate.capture_information); + // `move(expr)` requires its synthetic local to be captured by value, + // regardless of how the closure body uses it. Apply that requirement + // after closure-kind inference so capturing a value does not by itself + // make the closure `FnOnce`. + for capture in explicit_captures { + let place = closure_fcx.place_for_root_variable(closure_def_id, capture.var_hir_id); + capture_information.push(( + place, + ty::CaptureInfo { + capture_kind_expr_id: Some(closure_hir_id), + path_expr_id: Some(closure_hir_id), + capture_kind: UpvarCapture::ByValue, + }, + )); + } + self.compute_min_captures(closure_def_id, capture_information, span); let closure_hir_id = self.tcx.local_def_id_to_hir_id(closure_def_id); @@ -504,64 +514,44 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> { .tupled_inputs_ty .tuple_fields() .len(); - let coroutine_def_id = - self.tcx.coroutine_for_closure(closure_def_id).expect_local(); - let explicit_captures = self - .tcx - .hir_node_by_def_id(coroutine_def_id) - .expect_closure() - .explicit_captures; let typeck_results = self.typeck_results.borrow(); - let parent_captures = typeck_results - .closure_min_captures_flattened(closure_def_id) - .collect::>(); let tupled_upvars_ty_for_borrow = Ty::new_tup_from_iter( self.tcx, - typeck_results - .closure_min_captures_flattened(coroutine_def_id) - // Skip the captures that are just moving the closure's args - // into the coroutine. These are always by move, and we append - // those later in the `CoroutineClosureSignature` helper functions. - .skip(num_args) - .map(|child_capture| { + ty::analyze_coroutine_closure_captures( + typeck_results.closure_min_captures_flattened(closure_def_id), + typeck_results + .closure_min_captures_flattened( + self.tcx.coroutine_for_closure(closure_def_id).expect_local(), + ) + // Skip the captures that are just moving the closure's args + // into the coroutine. These are always by move, and we append + // those later in the `CoroutineClosureSignature` helper functions. + .skip(num_args), + |(_, parent_capture), (_, child_capture)| { + // This is subtle. See documentation on function. + let needs_ref = should_reborrow_from_env_of_parent_coroutine_closure( + parent_capture, + child_capture, + ); + let upvar_ty = child_capture.place.ty(); let capture = child_capture.info.capture_kind; - let region = if explicit_captures.iter().any(|explicit| { - explicit.var_hir_id == child_capture.get_root_variable() - }) { - // Synthetic move-expression locals are captured by - // value into the generated coroutine. They do not - // reborrow from the parent coroutine-closure env. - self.tcx.lifetimes.re_erased - } else { - let Some(parent_capture) = - parent_captures.iter().copied().find(|parent_capture| { - ty::child_prefix_matches_parent_projections( - parent_capture, - child_capture, - ) - }) - else { - bug!("child capture did not match a parent coroutine capture"); - }; - - // This is subtle. See documentation on function. - if should_reborrow_from_env_of_parent_coroutine_closure( - parent_capture, - child_capture, - ) { - closure_env_region - } else { - self.tcx.lifetimes.re_erased - } - }; - // Not all upvars are captured by ref, so use // `apply_capture_kind_on_capture_ty` to ensure that we // compute the right captured type. - apply_capture_kind_on_capture_ty(self.tcx, upvar_ty, capture, region) - }), + apply_capture_kind_on_capture_ty( + self.tcx, + upvar_ty, + capture, + if needs_ref { + closure_env_region + } else { + self.tcx.lifetimes.re_erased + }, + ) + }, + ), ); let coroutine_captures_by_ref_ty = Ty::new_fn_ptr( self.tcx, diff --git a/compiler/rustc_middle/src/ty/closure.rs b/compiler/rustc_middle/src/ty/closure.rs index 7bf143f625405..c6cdb9b9e2b96 100644 --- a/compiler/rustc_middle/src/ty/closure.rs +++ b/compiler/rustc_middle/src/ty/closure.rs @@ -434,11 +434,11 @@ pub fn analyze_coroutine_closure_captures<'a, 'tcx: 'a, T>( // refining the set of captures via edition-2021 precise captures. We want to // match up any number of child captures with one parent capture, so we keep // peeking off this `Peekable` until the child doesn't match anymore. - // - // Do not require every parent capture to match a child capture. A parent - // capture may be used only while evaluating a coroutine-closure - // `move(expr)` initializer, before the child coroutine is created. for (parent_field_idx, parent_capture) in parent_captures.into_iter().enumerate() { + // Make sure we use every field at least once, b/c why are we capturing something + // if it's not used in the inner coroutine. + let mut field_used_at_least_once = false; + // A parent matches a child if they share the same prefix of projections. // The child may have more, if it is capturing sub-fields out of // something that is captured by-move in the parent closure. @@ -458,13 +458,21 @@ pub fn analyze_coroutine_closure_captures<'a, 'tcx: 'a, T>( (parent_field_idx, parent_capture), (child_field_idx, child_capture), ); + + field_used_at_least_once = true; } + + // Make sure the field was used at least once. + assert!( + field_used_at_least_once, + "we captured {parent_capture:#?} but it was not used in the child coroutine?" + ); } assert_eq!(child_captures.next(), None, "leftover child captures?"); } } -pub fn child_prefix_matches_parent_projections( +fn child_prefix_matches_parent_projections( parent_capture: &ty::CapturedPlace<'_>, child_capture: &ty::CapturedPlace<'_>, ) -> bool { diff --git a/compiler/rustc_middle/src/ty/mod.rs b/compiler/rustc_middle/src/ty/mod.rs index 9328bf44ee04f..cc6a8619e1e74 100644 --- a/compiler/rustc_middle/src/ty/mod.rs +++ b/compiler/rustc_middle/src/ty/mod.rs @@ -64,8 +64,8 @@ pub use vtable::*; pub use self::closure::{ BorrowKind, CAPTURE_STRUCT_LOCAL, CaptureInfo, CapturedPlace, ClosureTypeInfo, MinCaptureInformationMap, MinCaptureList, RootVariableMinCaptureList, UpvarCapture, UpvarId, - UpvarPath, analyze_coroutine_closure_captures, child_prefix_matches_parent_projections, - is_ancestor_or_same_capture, place_to_string_for_capture, + UpvarPath, analyze_coroutine_closure_captures, is_ancestor_or_same_capture, + place_to_string_for_capture, }; pub use self::consts::{ AliasConst, AliasConstKind, AtomicOrdering, Const, ConstInt, ConstKind, ConstToValTreeResult, diff --git a/compiler/rustc_mir_transform/src/coroutine/by_move_body.rs b/compiler/rustc_mir_transform/src/coroutine/by_move_body.rs index 6a66b9c1f9478..88ffe5861a697 100644 --- a/compiler/rustc_mir_transform/src/coroutine/by_move_body.rs +++ b/compiler/rustc_mir_transform/src/coroutine/by_move_body.rs @@ -125,20 +125,10 @@ pub(crate) fn coroutine_by_move_body_def_id<'tcx>( .tupled_inputs_ty .tuple_fields() .len(); - let explicit_captures = - tcx.hir_node_by_def_id(coroutine_def_id).expect_closure().explicit_captures; let field_remapping: UnordMap<_, _> = ty::analyze_coroutine_closure_captures( tcx.closure_captures(parent_def_id).iter().copied(), - tcx.closure_captures(coroutine_def_id) - .iter() - .skip(num_args) - .filter(|capture| { - !explicit_captures - .iter() - .any(|explicit| explicit.var_hir_id == capture.get_root_variable()) - }) - .copied(), + tcx.closure_captures(coroutine_def_id).iter().skip(num_args).copied(), |(parent_field_idx, parent_capture), (child_field_idx, child_capture)| { // Store this set of additional projections (fields and derefs). // We need to re-apply them later. From afff72e3fb2c002da44b06aa363f5736d03e19f0 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 20 Aug 2026 22:44:50 +0900 Subject: [PATCH 12/13] update move-expression closure semantics tests --- tests/ui/move-expr/async-closures.rs | 43 +++++++++++++------ tests/ui/move-expr/nested-move-exhausted.rs | 3 ++ .../ui/move-expr/nested-move-exhausted.stderr | 14 ++++-- tests/ui/move-expr/nested-move-expr.rs | 3 ++ tests/ui/move-expr/plain-closure.rs | 18 +++++++- 5 files changed, 62 insertions(+), 19 deletions(-) diff --git a/tests/ui/move-expr/async-closures.rs b/tests/ui/move-expr/async-closures.rs index 0c5d651719dbe..b467248048367 100644 --- a/tests/ui/move-expr/async-closures.rs +++ b/tests/ui/move-expr/async-closures.rs @@ -4,7 +4,26 @@ #![feature(move_expr)] use std::cell::Cell; +use std::future::Future; +use std::pin::pin; use std::sync::Arc; +use std::task::{Context, Poll, Waker}; + +fn block_on(future: impl Future) -> T { + let mut future = pin!(future); + let context = &mut Context::from_waker(Waker::noop()); + + loop { + match future.as_mut().poll(context) { + Poll::Ready(value) => return value, + Poll::Pending => {} + } + } +} + +async fn call_once(closure: impl AsyncFnOnce() -> T) -> T { + closure().await +} fn main() { let created = Cell::new(0); @@ -15,28 +34,26 @@ fn main() { }); n }; - assert_eq!(created.get(), 0); - let fut = c(); assert_eq!(created.get(), 1); - drop(fut); + assert_eq!(block_on(c()), 1); + assert_eq!(block_on(c()), 1); + assert_eq!(created.get(), 1); let x = Arc::new(String::from("hello")); assert_eq!(Arc::strong_count(&x), 1); let c = async || move(x.clone()); - assert_eq!(Arc::strong_count(&x), 1); + assert_eq!(Arc::strong_count(&x), 2); let fut = c(); assert_eq!(Arc::strong_count(&x), 2); drop(fut); assert_eq!(Arc::strong_count(&x), 1); - let y = Arc::new(String::from("nested")); - assert_eq!(Arc::strong_count(&y), 1); - let c = async || move(move(y.clone())); - assert_eq!(Arc::strong_count(&y), 2); - let fut = c(); - assert_eq!(Arc::strong_count(&y), 2); - drop(fut); - assert_eq!(Arc::strong_count(&y), 1); - assert_eq!(&*y, "nested"); + let a = String::from("a"); + let b = String::from("bbb"); + let c = async || { + let moved = move(a.clone()); + (moved, b.len()) + }; + assert_eq!(block_on(call_once(c)), (String::from("a"), 3)); } diff --git a/tests/ui/move-expr/nested-move-exhausted.rs b/tests/ui/move-expr/nested-move-exhausted.rs index d809020d3af6b..8508cc1c756b5 100644 --- a/tests/ui/move-expr/nested-move-exhausted.rs +++ b/tests/ui/move-expr/nested-move-exhausted.rs @@ -6,6 +6,9 @@ fn main() { let _ = || move(move(0)); //~^ ERROR nested `move(expr)` requires another enclosing closure + let _ = async || move(move(0)); + //~^ ERROR nested `move(expr)` requires another enclosing closure + let _ = async { move(move(0)) }; //~^ ERROR nested `move(expr)` requires another enclosing closure diff --git a/tests/ui/move-expr/nested-move-exhausted.stderr b/tests/ui/move-expr/nested-move-exhausted.stderr index 9743a96b81ef3..2c919666c75ae 100644 --- a/tests/ui/move-expr/nested-move-exhausted.stderr +++ b/tests/ui/move-expr/nested-move-exhausted.stderr @@ -5,22 +5,28 @@ LL | let _ = || move(move(0)); | ^^^^ error: nested `move(expr)` requires another enclosing closure, `async`, `gen`, or `async gen` block - --> $DIR/nested-move-exhausted.rs:9:26 + --> $DIR/nested-move-exhausted.rs:9:27 + | +LL | let _ = async || move(move(0)); + | ^^^^ + +error: nested `move(expr)` requires another enclosing closure, `async`, `gen`, or `async gen` block + --> $DIR/nested-move-exhausted.rs:12:26 | LL | let _ = async { move(move(0)) }; | ^^^^ error: nested `move(expr)` requires another enclosing closure, `async`, `gen`, or `async gen` block - --> $DIR/nested-move-exhausted.rs:12:24 + --> $DIR/nested-move-exhausted.rs:15:24 | LL | let _ = gen { move(move(0)) }; | ^^^^ error: nested `move(expr)` requires another enclosing closure, `async`, `gen`, or `async gen` block - --> $DIR/nested-move-exhausted.rs:15:30 + --> $DIR/nested-move-exhausted.rs:18:30 | LL | let _ = async gen { move(move(0)) }; | ^^^^ -error: aborting due to 4 previous errors +error: aborting due to 5 previous errors diff --git a/tests/ui/move-expr/nested-move-expr.rs b/tests/ui/move-expr/nested-move-expr.rs index f3ca679641238..b6e0f70b355d7 100644 --- a/tests/ui/move-expr/nested-move-expr.rs +++ b/tests/ui/move-expr/nested-move-expr.rs @@ -12,6 +12,9 @@ fn main() { let inner = outer(); assert_eq!(Arc::strong_count(&v), 2); assert_eq!(inner(), v.len()); + assert_eq!(inner(), v.len()); + assert_eq!(Arc::strong_count(&v), 2); + drop(inner); assert_eq!(Arc::strong_count(&v), 1); println!("{v}"); diff --git a/tests/ui/move-expr/plain-closure.rs b/tests/ui/move-expr/plain-closure.rs index 788c631cf5fdf..3f58142f7ea9c 100644 --- a/tests/ui/move-expr/plain-closure.rs +++ b/tests/ui/move-expr/plain-closure.rs @@ -1,8 +1,23 @@ -//@ check-pass +//@ run-pass #![allow(incomplete_features)] #![feature(move_expr)] +use std::cell::Cell; + fn main() { + let created = Cell::new(0); + let c = || { + let n = move({ + created.set(created.get() + 1); + created.get() + }); + n + }; + assert_eq!(created.get(), 1); + assert_eq!(c(), 1); + assert_eq!(c(), 1); + assert_eq!(created.get(), 1); + let s = String::from("hello"); let c = || { let t = move(s); @@ -18,5 +33,4 @@ fn main() { println!("{} {}", x, y); }; c(); - } From 2ed8d0db767789d4b09976777bc4422ffa6ef109 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 20 Aug 2026 22:45:05 +0900 Subject: [PATCH 13/13] add move-expression tests for generator closures --- tests/ui/move-expr/gen-closures.rs | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 tests/ui/move-expr/gen-closures.rs diff --git a/tests/ui/move-expr/gen-closures.rs b/tests/ui/move-expr/gen-closures.rs new file mode 100644 index 0000000000000..5e74089710adf --- /dev/null +++ b/tests/ui/move-expr/gen-closures.rs @@ -0,0 +1,46 @@ +//@ run-pass + +#![allow(incomplete_features)] +#![feature(iter_macro, move_expr, yield_expr)] + +use std::cell::Cell; +use std::iter::iter; +use std::sync::Arc; + +fn main() { + let created = Cell::new(0); + let closure = iter! { || { + let n = move({ + created.set(created.get() + 1); + created.get() + }); + yield n; + }}; + assert_eq!(created.get(), 1); + assert_eq!(closure().next(), Some(1)); + assert_eq!(closure().next(), Some(1)); + assert_eq!(created.get(), 1); + + let x = Arc::new(String::from("hello")); + assert_eq!(Arc::strong_count(&x), 1); + + let closure = iter! { || { + yield move(x.clone()); + }}; + assert_eq!(Arc::strong_count(&x), 2); + let mut generator = closure(); + assert_eq!(Arc::strong_count(&x), 2); + let yielded = generator.next().unwrap(); + assert_eq!(Arc::strong_count(&x), 2); + assert_eq!(generator.next(), None); + drop(yielded); + assert_eq!(Arc::strong_count(&x), 1); + + let a = String::from("a"); + let b = String::from("bbb"); + let closure = iter! { || { + let moved = move(a.clone()); + yield (moved, b.len()); + }}; + assert_eq!(closure().next(), Some((String::from("a"), 3))); +}