From f62d5368a250d3379093964caf5c4938267b4e4e Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 9 Apr 2026 11:40:18 +0900 Subject: [PATCH 01/17] add move_expr feature flag --- compiler/rustc_feature/src/unstable.rs | 2 ++ compiler/rustc_span/src/symbol.rs | 1 + 2 files changed, 3 insertions(+) diff --git a/compiler/rustc_feature/src/unstable.rs b/compiler/rustc_feature/src/unstable.rs index 5af522486b1f6..93fc3e0478b6a 100644 --- a/compiler/rustc_feature/src/unstable.rs +++ b/compiler/rustc_feature/src/unstable.rs @@ -646,6 +646,8 @@ declare_features! ( (unstable, must_not_suspend, "1.57.0", Some(83310)), /// Allows `mut ref` and `mut ref mut` identifier patterns. (incomplete, mut_ref, "1.79.0", Some(123076)), + /// Allows `move(expr)` in closures. + (incomplete, move_expr, "CURRENT_RUSTC_VERSION", None), /// Allows using `#[naked]` on `extern "Rust"` functions. (unstable, naked_functions_rustic_abi, "1.88.0", Some(138997)), /// Allows using `#[target_feature(enable = "...")]` on `#[naked]` on functions. diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs index ea2880d8d7dff..e7cc43fdf8ebc 100644 --- a/compiler/rustc_span/src/symbol.rs +++ b/compiler/rustc_span/src/symbol.rs @@ -1323,6 +1323,7 @@ symbols! { more_qualified_paths, more_struct_aliases, movbe_target_feature, + move_expr, move_ref_pattern, move_size_limit, movrs_target_feature, From 98348de63a5047b3ee7d8182efa8486e6eaac79b Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 9 Apr 2026 21:14:27 +0900 Subject: [PATCH 02/17] add move(expr) syntax --- compiler/rustc_ast/src/ast.rs | 3 +++ compiler/rustc_ast/src/util/classify.rs | 2 ++ compiler/rustc_ast/src/visit.rs | 4 +++- compiler/rustc_ast_lowering/src/errors.rs | 7 +++++++ compiler/rustc_ast_lowering/src/expr.rs | 16 ++++++++++++++-- compiler/rustc_ast_passes/src/feature_gate.rs | 3 +++ .../rustc_ast_pretty/src/pprust/state/expr.rs | 5 +++++ .../rustc_builtin_macros/src/assert/context.rs | 3 +++ compiler/rustc_parse/src/parser/expr.rs | 17 +++++++++++++++++ compiler/rustc_passes/src/input_stats.rs | 2 +- .../ui/feature-gates/feature-gate-move_expr.rs | 4 ++++ .../feature-gates/feature-gate-move_expr.stderr | 12 ++++++++++++ tests/ui/macros/stringify.rs | 3 +++ 13 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 tests/ui/feature-gates/feature-gate-move_expr.rs create mode 100644 tests/ui/feature-gates/feature-gate-move_expr.stderr diff --git a/compiler/rustc_ast/src/ast.rs b/compiler/rustc_ast/src/ast.rs index 0be00f4d00be7..a6e63214b3bf3 100644 --- a/compiler/rustc_ast/src/ast.rs +++ b/compiler/rustc_ast/src/ast.rs @@ -1594,6 +1594,7 @@ impl Expr { // need parens sometimes. E.g. we can print `(let _ = a) && b` as `let _ = a && b` // but we need to print `(let _ = a) < b` as-is with parens. | ExprKind::Let(..) + | ExprKind::Move(..) | ExprKind::Unary(..) => ExprPrecedence::Prefix, // Need parens if and only if there are prefix attributes. @@ -1763,6 +1764,8 @@ pub enum ExprKind { Binary(BinOp, Box, Box), /// A unary operation (e.g., `!x`, `*x`). Unary(UnOp, Box), + /// A `move(expr)` expression. + Move(Box, Span), /// A literal (e.g., `1`, `"foo"`). Lit(token::Lit), /// A cast (e.g., `foo as f64`). diff --git a/compiler/rustc_ast/src/util/classify.rs b/compiler/rustc_ast/src/util/classify.rs index 43ef6897b79cf..0c98b0e5e7b4f 100644 --- a/compiler/rustc_ast/src/util/classify.rs +++ b/compiler/rustc_ast/src/util/classify.rs @@ -108,6 +108,7 @@ pub fn leading_labeled_expr(mut expr: &ast::Expr) -> bool { Assign(e, _, _) | AssignOp(_, e, _) | Await(e, _) + | Move(e, _) | Use(e, _) | Binary(_, e, _) | Call(e, _) @@ -183,6 +184,7 @@ pub fn expr_trailing_brace(mut expr: &ast::Expr) -> Option> { | Ret(Some(e)) | Unary(_, e) | Yeet(Some(e)) + | Move(e, _) | Become(e) => { expr = e; } diff --git a/compiler/rustc_ast/src/visit.rs b/compiler/rustc_ast/src/visit.rs index cbd7cb3ee8c6f..ed8c3787bfec4 100644 --- a/compiler/rustc_ast/src/visit.rs +++ b/compiler/rustc_ast/src/visit.rs @@ -1024,7 +1024,9 @@ macro_rules! common_visitor_and_walkers { visit_visitable!($($mut)? vis, block, opt_label), ExprKind::Gen(capt, body, kind, decl_span) => visit_visitable!($($mut)? vis, capt, body, kind, decl_span), - ExprKind::Await(expr, span) | ExprKind::Use(expr, span) => + ExprKind::Await(expr, span) + | ExprKind::Move(expr, span) + | ExprKind::Use(expr, span) => visit_visitable!($($mut)? vis, expr, span), ExprKind::Assign(lhs, rhs, span) => visit_visitable!($($mut)? vis, lhs, rhs, span), diff --git a/compiler/rustc_ast_lowering/src/errors.rs b/compiler/rustc_ast_lowering/src/errors.rs index 95b8bb48c6a9c..a1c1d1e11d694 100644 --- a/compiler/rustc_ast_lowering/src/errors.rs +++ b/compiler/rustc_ast_lowering/src/errors.rs @@ -136,6 +136,13 @@ pub(crate) struct ClosureCannotBeStatic { pub fn_decl_span: Span, } +#[derive(Diagnostic)] +#[diag("`move(expr)` is only supported in plain closures")] +pub(crate) struct MoveExprOnlyInPlainClosures { + #[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 eaa22e071af4b..8a86c66d6e9cb 100644 --- a/compiler/rustc_ast_lowering/src/expr.rs +++ b/compiler/rustc_ast_lowering/src/expr.rs @@ -19,8 +19,8 @@ use visit::{Visitor, walk_expr}; use super::errors::{ AsyncCoroutinesNotSupported, AwaitOnlyInAsyncFnAndBlocks, ClosureCannotBeStatic, CoroutineTooManyParameters, FunctionalRecordUpdateDestructuringAssignment, - InclusiveRangeWithNoEnd, MatchArmWithNoBody, NeverPatternWithBody, NeverPatternWithGuard, - UnderscoreExprLhsAssign, + InclusiveRangeWithNoEnd, MatchArmWithNoBody, MoveExprOnlyInPlainClosures, NeverPatternWithBody, + NeverPatternWithGuard, UnderscoreExprLhsAssign, }; use super::{ GenericArgsMode, ImplTraitContext, LoweringContext, ParamMode, ResolverAstLoweringExt, @@ -211,6 +211,18 @@ impl<'hir> LoweringContext<'_, 'hir> { }, ), ExprKind::Await(expr, await_kw_span) => self.lower_expr_await(*await_kw_span, expr), + ExprKind::Move(_, move_kw_span) => { + if !self.tcx.features().move_expr() { + return self.expr_err( + *move_kw_span, + self.dcx().span_delayed_bug(*move_kw_span, "invalid move(expr)"), + ); + } + self.dcx().emit_err(MoveExprOnlyInPlainClosures { span: *move_kw_span }); + hir::ExprKind::Err( + self.dcx().span_delayed_bug(*move_kw_span, "invalid move(expr)"), + ) + } ExprKind::Use(expr, use_kw_span) => self.lower_expr_use(*use_kw_span, expr), ExprKind::Closure(Closure { binder, diff --git a/compiler/rustc_ast_passes/src/feature_gate.rs b/compiler/rustc_ast_passes/src/feature_gate.rs index e38716cbb7fb7..b0679487b8ebb 100644 --- a/compiler/rustc_ast_passes/src/feature_gate.rs +++ b/compiler/rustc_ast_passes/src/feature_gate.rs @@ -352,6 +352,9 @@ impl<'a> Visitor<'a> for PostExpansionVisitor<'a> { } _ => (), }, + ast::ExprKind::Move(_, move_kw_span) => { + gate!(&self, move_expr, move_kw_span, "`move(expr)` syntax is experimental"); + } _ => {} } visit::walk_expr(self, e) diff --git a/compiler/rustc_ast_pretty/src/pprust/state/expr.rs b/compiler/rustc_ast_pretty/src/pprust/state/expr.rs index a20ef210da973..78aedbd4f7f4b 100644 --- a/compiler/rustc_ast_pretty/src/pprust/state/expr.rs +++ b/compiler/rustc_ast_pretty/src/pprust/state/expr.rs @@ -630,6 +630,11 @@ impl<'a> State<'a> { ); self.word(".await"); } + ast::ExprKind::Move(expr, _) => { + self.word("move("); + self.print_expr(expr, FixupContext::default()); + self.word(")"); + } ast::ExprKind::Use(expr, _) => { self.print_expr_cond_paren( expr, diff --git a/compiler/rustc_builtin_macros/src/assert/context.rs b/compiler/rustc_builtin_macros/src/assert/context.rs index 6ad9c61840fae..f15acc154baf3 100644 --- a/compiler/rustc_builtin_macros/src/assert/context.rs +++ b/compiler/rustc_builtin_macros/src/assert/context.rs @@ -248,6 +248,9 @@ impl<'cx, 'a> Context<'cx, 'a> { self.manage_cond_expr(arg); } } + ExprKind::Move(local_expr, _) => { + self.manage_cond_expr(local_expr); + } ExprKind::Path(_, Path { segments, .. }) if let [path_segment] = &segments[..] => { let path_ident = path_segment.ident; self.manage_initial_capture(expr, path_ident); diff --git a/compiler/rustc_parse/src/parser/expr.rs b/compiler/rustc_parse/src/parser/expr.rs index 35c271cb70204..e5de0d972df6a 100644 --- a/compiler/rustc_parse/src/parser/expr.rs +++ b/compiler/rustc_parse/src/parser/expr.rs @@ -564,6 +564,12 @@ impl<'a> Parser<'a> { token::Ident(..) if this.token.is_keyword(kw::Box) => { make_it!(this, attrs, |this, _| this.parse_expr_box(lo)) } + token::Ident(..) + if this.token.is_keyword(kw::Move) + && this.look_ahead(1, |t| *t == token::OpenParen) => + { + make_it!(this, attrs, |this, _| this.parse_expr_move(lo)) + } token::Ident(..) if this.may_recover() && this.is_mistaken_not_ident_negation() => { make_it!(this, attrs, |this, _| this.recover_not_expr(lo)) } @@ -607,6 +613,16 @@ impl<'a> Parser<'a> { Ok((span, ExprKind::Err(guar))) } + fn parse_expr_move(&mut self, move_kw: Span) -> PResult<'a, (Span, ExprKind)> { + self.bump(); + self.psess.gated_spans.gate(sym::move_expr, move_kw); + self.expect(exp!(OpenParen))?; + let expr = self.parse_expr()?; + self.expect(exp!(CloseParen))?; + let span = move_kw.to(self.prev_token.span); + Ok((span, ExprKind::Move(expr, move_kw))) + } + fn is_mistaken_not_ident_negation(&self) -> bool { let token_cannot_continue_expr = |t: &Token| match t.uninterpolate().kind { // These tokens can start an expression after `!`, but @@ -4387,6 +4403,7 @@ impl MutVisitor for CondChecker<'_> { } ExprKind::Unary(_, _) | ExprKind::Await(_, _) + | ExprKind::Move(_, _) | ExprKind::Use(_, _) | ExprKind::AssignOp(_, _, _) | ExprKind::Range(_, _, _) diff --git a/compiler/rustc_passes/src/input_stats.rs b/compiler/rustc_passes/src/input_stats.rs index e424cc09fb607..9127e4936803d 100644 --- a/compiler/rustc_passes/src/input_stats.rs +++ b/compiler/rustc_passes/src/input_stats.rs @@ -657,7 +657,7 @@ impl<'v> ast_visit::Visitor<'v> for StatCollector<'v> { (self, e, e.kind, None, ast, Expr, ExprKind), [ Array, ConstBlock, Call, MethodCall, Tup, Binary, Unary, Lit, Cast, Type, Let, - If, While, ForLoop, Loop, Match, Closure, Block, Await, Use, TryBlock, Assign, + If, While, ForLoop, Loop, Match, Closure, Block, Await, Move, Use, TryBlock, Assign, AssignOp, Field, Index, Range, Underscore, Path, AddrOf, Break, Continue, Ret, InlineAsm, FormatArgs, OffsetOf, MacCall, Struct, Repeat, Paren, Try, Yield, Yeet, Become, IncludedBytes, Gen, UnsafeBinderCast, Err, Dummy diff --git a/tests/ui/feature-gates/feature-gate-move_expr.rs b/tests/ui/feature-gates/feature-gate-move_expr.rs new file mode 100644 index 0000000000000..a2ab1bb8b1d00 --- /dev/null +++ b/tests/ui/feature-gates/feature-gate-move_expr.rs @@ -0,0 +1,4 @@ +fn main() { + let _ = || move(2); + //~^ ERROR `move(expr)` syntax is experimental +} diff --git a/tests/ui/feature-gates/feature-gate-move_expr.stderr b/tests/ui/feature-gates/feature-gate-move_expr.stderr new file mode 100644 index 0000000000000..28ab95ababc16 --- /dev/null +++ b/tests/ui/feature-gates/feature-gate-move_expr.stderr @@ -0,0 +1,12 @@ +error[E0658]: `move(expr)` syntax is experimental + --> $DIR/feature-gate-move_expr.rs:2:16 + | +LL | let _ = || move(2); + | ^^^^ + | + = help: add `#![feature(move_expr)]` to the crate attributes to enable + = note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0658`. diff --git a/tests/ui/macros/stringify.rs b/tests/ui/macros/stringify.rs index b48b037b22949..fec991ec95b7b 100644 --- a/tests/ui/macros/stringify.rs +++ b/tests/ui/macros/stringify.rs @@ -3,6 +3,7 @@ //@ compile-flags: --test #![allow(incomplete_features)] +#![allow(unused_features)] #![feature(auto_traits)] #![feature(box_patterns)] #![feature(const_block_items)] @@ -11,6 +12,7 @@ #![feature(decl_macro)] #![feature(macro_guard_matcher)] #![feature(more_qualified_paths)] +#![feature(move_expr)] #![feature(never_patterns)] #![feature(specialization)] #![feature(trait_alias)] @@ -110,6 +112,7 @@ fn test_expr() { c1!(expr, [ *expr ], "*expr"); c1!(expr, [ !expr ], "!expr"); c1!(expr, [ -expr ], "-expr"); + c1!(expr, [ move(expr) ], "move(expr)"); // ExprKind::Lit c1!(expr, [ 'x' ], "'x'"); From 9abe21c24bf958413ce30a4c9583bef8f6f361fb Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 9 Apr 2026 21:18:59 +0900 Subject: [PATCH 03/17] lower move(expr) in plain closures --- compiler/rustc_ast_lowering/src/expr.rs | 241 ++++++++++++++---- compiler/rustc_ast_lowering/src/lib.rs | 2 + compiler/rustc_hir/src/hir.rs | 7 + compiler/rustc_hir/src/intravisit.rs | 1 + compiler/rustc_hir_pretty/src/lib.rs | 1 + tests/ui/move-expr/outside-plain-closure.rs | 7 + .../ui/move-expr/outside-plain-closure.stderr | 8 + tests/ui/move-expr/plain-closure.rs | 12 + 8 files changed, 236 insertions(+), 43 deletions(-) create mode 100644 tests/ui/move-expr/outside-plain-closure.rs create mode 100644 tests/ui/move-expr/outside-plain-closure.stderr create mode 100644 tests/ui/move-expr/plain-closure.rs diff --git a/compiler/rustc_ast_lowering/src/expr.rs b/compiler/rustc_ast_lowering/src/expr.rs index 8a86c66d6e9cb..a5d07f02197a9 100644 --- a/compiler/rustc_ast_lowering/src/expr.rs +++ b/compiler/rustc_ast_lowering/src/expr.rs @@ -2,6 +2,7 @@ use std::mem; use std::ops::ControlFlow; use std::sync::Arc; +use rustc_ast::node_id::NodeMap; use rustc_ast::*; use rustc_ast_pretty::pprust::expr_to_string; use rustc_data_structures::stack::ensure_sufficient_stack; @@ -30,6 +31,41 @@ use crate::{AllowReturnTypeNotation, FnDeclKind, ImplTraitPosition, TryBlockScop pub(super) struct WillCreateDefIdsVisitor; +struct MoveExprOccurrence<'a> { + id: NodeId, + move_kw_span: Span, + expr: &'a Expr, +} + +struct MoveExprCollector<'a> { + occurrences: Vec>, +} + +impl<'a> MoveExprCollector<'a> { + fn collect(expr: &'a Expr) -> Vec> { + let mut this = Self { occurrences: Vec::new() }; + this.visit_expr(expr); + this.occurrences + } +} + +impl<'a> Visitor<'a> for MoveExprCollector<'a> { + fn visit_expr(&mut self, expr: &'a Expr) { + match &expr.kind { + ExprKind::Move(inner, move_kw_span) => { + self.visit_expr(inner); + self.occurrences.push(MoveExprOccurrence { + id: expr.id, + move_kw_span: *move_kw_span, + expr: inner, + }); + } + ExprKind::Closure(..) | ExprKind::Gen(..) | ExprKind::ConstBlock(..) => {} + _ => walk_expr(self, expr), + } + } +} + impl<'v> rustc_ast::visit::Visitor<'v> for WillCreateDefIdsVisitor { type Result = ControlFlow; @@ -94,11 +130,12 @@ impl<'hir> LoweringContext<'_, 'hir> { ExprKind::ForLoop { pat, iter, body, label, kind } => { return self.lower_expr_for(e, pat, iter, body, *label, *kind); } + ExprKind::Closure(box closure) => return self.lower_expr_closure_expr(e, closure), _ => (), } let expr_hir_id = self.lower_node_id(e.id); - let attrs = self.lower_attrs(expr_hir_id, &e.attrs, e.span, Target::from_expr(e)); + self.lower_attrs(expr_hir_id, &e.attrs, e.span, Target::from_expr(e)); let kind = match &e.kind { ExprKind::Array(exprs) => hir::ExprKind::Array(self.lower_exprs(exprs)), @@ -218,48 +255,34 @@ impl<'hir> LoweringContext<'_, 'hir> { self.dcx().span_delayed_bug(*move_kw_span, "invalid move(expr)"), ); } - self.dcx().emit_err(MoveExprOnlyInPlainClosures { span: *move_kw_span }); - hir::ExprKind::Err( - self.dcx().span_delayed_bug(*move_kw_span, "invalid move(expr)"), - ) + if let Some((ident, binding)) = self + .move_expr_bindings + .last() + .and_then(|bindings| bindings.get(&e.id).copied()) + { + hir::ExprKind::Path(hir::QPath::Resolved( + None, + self.arena.alloc(hir::Path { + span: self.lower_span(e.span), + res: Res::Local(binding), + segments: arena_vec![ + self; + hir::PathSegment::new( + self.lower_ident(ident), + self.next_id(), + Res::Local(binding), + ) + ], + }), + )) + } else { + self.dcx().emit_err(MoveExprOnlyInPlainClosures { span: *move_kw_span }); + hir::ExprKind::Err( + self.dcx().span_delayed_bug(*move_kw_span, "invalid move(expr)"), + ) + } } ExprKind::Use(expr, use_kw_span) => self.lower_expr_use(*use_kw_span, expr), - ExprKind::Closure(Closure { - binder, - capture_clause, - constness, - coroutine_kind, - movability, - fn_decl, - body, - fn_decl_span, - fn_arg_span, - }) => match coroutine_kind { - Some(coroutine_kind) => self.lower_expr_coroutine_closure( - binder, - *capture_clause, - e.id, - expr_hir_id, - *coroutine_kind, - *constness, - fn_decl, - body, - *fn_decl_span, - *fn_arg_span, - ), - None => self.lower_expr_closure( - attrs, - binder, - *capture_clause, - e.id, - *constness, - *movability, - fn_decl, - body, - *fn_decl_span, - *fn_arg_span, - ), - }, ExprKind::Gen(capture_clause, block, genblock_kind, decl_span) => { let desugaring_kind = match genblock_kind { GenBlockKind::Async => hir::CoroutineDesugaring::Async, @@ -394,7 +417,7 @@ impl<'hir> LoweringContext<'_, 'hir> { ExprKind::Try(sub_expr) => self.lower_expr_try(e.span, sub_expr), - ExprKind::Paren(_) | ExprKind::ForLoop { .. } => { + ExprKind::Paren(_) | ExprKind::ForLoop { .. } | ExprKind::Closure(..) => { unreachable!("already handled") } @@ -795,6 +818,7 @@ impl<'hir> LoweringContext<'_, 'hir> { fn_arg_span: None, kind: hir::ClosureKind::Coroutine(coroutine_kind), constness: hir::Constness::NotConst, + explicit_captures: &[], })) } @@ -1058,6 +1082,134 @@ impl<'hir> LoweringContext<'_, 'hir> { hir::ExprKind::Use(self.lower_expr(expr), self.lower_span(use_kw_span)) } + fn lower_expr_closure_expr(&mut self, e: &Expr, closure: &Closure) -> hir::Expr<'hir> { + let expr_hir_id = self.lower_node_id(e.id); + let attrs = self.lower_attrs(expr_hir_id, &e.attrs, e.span, Target::from_expr(e)); + + match closure.coroutine_kind { + Some(coroutine_kind) => hir::Expr { + hir_id: expr_hir_id, + kind: self.lower_expr_coroutine_closure( + &closure.binder, + closure.capture_clause, + e.id, + expr_hir_id, + coroutine_kind, + closure.constness, + &closure.fn_decl, + &closure.body, + closure.fn_decl_span, + closure.fn_arg_span, + ), + span: self.lower_span(e.span), + }, + None => self.lower_expr_plain_closure_with_move_exprs( + expr_hir_id, + attrs, + &closure.binder, + closure.capture_clause, + e.id, + closure.constness, + closure.movability, + &closure.fn_decl, + &closure.body, + closure.fn_decl_span, + closure.fn_arg_span, + e.span, + ), + } + } + + fn lower_expr_plain_closure_with_move_exprs( + &mut self, + expr_hir_id: HirId, + attrs: &[rustc_hir::Attribute], + binder: &ClosureBinder, + capture_clause: CaptureBy, + closure_id: NodeId, + constness: Const, + movability: Movability, + decl: &FnDecl, + body: &Expr, + fn_decl_span: Span, + fn_arg_span: Span, + whole_span: Span, + ) -> hir::Expr<'hir> { + let occurrences = MoveExprCollector::collect(body); + if occurrences.is_empty() { + return hir::Expr { + hir_id: expr_hir_id, + kind: self.lower_expr_closure( + attrs, + binder, + capture_clause, + closure_id, + constness, + movability, + decl, + body, + fn_decl_span, + fn_arg_span, + &[], + ), + span: self.lower_span(whole_span), + }; + } + + let mut bindings = NodeMap::default(); + let mut lowered_occurrences = Vec::with_capacity(occurrences.len()); + for (index, occurrence) in occurrences.iter().enumerate() { + let ident = + Ident::from_str_and_span(&format!("__move_expr_{index}"), occurrence.move_kw_span); + let (pat, binding) = self.pat_ident(occurrence.expr.span, ident); + bindings.insert(occurrence.id, (ident, binding)); + lowered_occurrences.push((occurrence, pat, binding)); + } + + self.move_expr_bindings.push(bindings); + let mut stmts = Vec::with_capacity(lowered_occurrences.len()); + for (occurrence, pat, _) in &lowered_occurrences { + let init = self.lower_expr(occurrence.expr); + stmts.push(self.stmt_let_pat( + None, + occurrence.expr.span, + Some(init), + *pat, + hir::LocalSource::Normal, + )); + } + + let explicit_captures = self.arena.alloc_from_iter(lowered_occurrences.iter().map( + |(occurrence, _, binding)| hir::ExplicitCapture { + var_hir_id: *binding, + origin_span: self.lower_span(occurrence.move_kw_span), + }, + )); + + let closure_expr = self.arena.alloc(hir::Expr { + hir_id: expr_hir_id, + kind: self.lower_expr_closure( + attrs, + binder, + capture_clause, + closure_id, + constness, + movability, + decl, + body, + fn_decl_span, + fn_arg_span, + explicit_captures, + ), + span: self.lower_span(whole_span), + }); + self.move_expr_bindings.pop(); + + let stmts = self.arena.alloc_from_iter(stmts); + let block = self.block_all(whole_span, stmts, Some(closure_expr)); + self.expr(whole_span, hir::ExprKind::Block(block, None)) + } + fn lower_expr_closure( &mut self, attrs: &[rustc_hir::Attribute], @@ -1070,6 +1222,7 @@ impl<'hir> LoweringContext<'_, 'hir> { body: &Expr, fn_decl_span: Span, fn_arg_span: Span, + explicit_captures: &'hir [hir::ExplicitCapture], ) -> hir::ExprKind<'hir> { let closure_def_id = self.local_def_id(closure_id); let (binder_clause, generic_params) = self.lower_closure_binder(binder); @@ -1105,6 +1258,7 @@ impl<'hir> LoweringContext<'_, 'hir> { fn_arg_span: Some(self.lower_span(fn_arg_span)), kind: closure_kind, constness: self.lower_constness(constness), + explicit_captures, }); hir::ExprKind::Closure(c) @@ -1226,7 +1380,8 @@ impl<'hir> LoweringContext<'_, 'hir> { // knows that a `FnDecl` output type like `-> &str` actually means // "coroutine that returns &str", rather than directly returning a `&str`. kind: hir::ClosureKind::CoroutineClosure(coroutine_desugaring), - constness: self.lower_constness(constness), + constness: hir::Constness::NotConst, + 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 1ce4478c09e8b..3af3e1b0e544a 100644 --- a/compiler/rustc_ast_lowering/src/lib.rs +++ b/compiler/rustc_ast_lowering/src/lib.rs @@ -158,6 +158,7 @@ struct LoweringContext<'a, 'hir> { allow_async_fn_traits: Arc<[Symbol]>, delayed_lints: Vec, + move_expr_bindings: Vec>, attribute_parser: AttributeParser<'hir>, } @@ -216,6 +217,7 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> { // interact with `gen`/`async gen` blocks allow_async_iterator: [sym::gen_future, sym::async_iterator].into(), + move_expr_bindings: Vec::new(), attribute_parser: AttributeParser::new( tcx.sess, tcx.features(), diff --git a/compiler/rustc_hir/src/hir.rs b/compiler/rustc_hir/src/hir.rs index 2f18b09cf1ae8..668367e066a40 100644 --- a/compiler/rustc_hir/src/hir.rs +++ b/compiler/rustc_hir/src/hir.rs @@ -1682,6 +1682,13 @@ pub struct Closure<'hir> { /// The span of the argument block `|...|` pub fn_arg_span: Option, pub kind: ClosureKind, + pub explicit_captures: &'hir [ExplicitCapture], +} + +#[derive(Debug, Clone, Copy, HashStable_Generic)] +pub struct ExplicitCapture { + pub var_hir_id: HirId, + pub origin_span: Span, } #[derive(Clone, PartialEq, Eq, Debug, Copy, Hash, StableHash, Encodable, Decodable)] diff --git a/compiler/rustc_hir/src/intravisit.rs b/compiler/rustc_hir/src/intravisit.rs index 68d8c12ec099c..85f0e97a0a9d6 100644 --- a/compiler/rustc_hir/src/intravisit.rs +++ b/compiler/rustc_hir/src/intravisit.rs @@ -893,6 +893,7 @@ pub fn walk_expr<'v, V: Visitor<'v>>(visitor: &mut V, expression: &'v Expr<'v>) fn_arg_span: _, kind: _, constness: _, + explicit_captures: _, }) => { walk_list!(visitor, visit_generic_param, bound_generic_params); try_visit!(visitor.visit_fn(FnKind::Closure, fn_decl, body, *span, def_id)); diff --git a/compiler/rustc_hir_pretty/src/lib.rs b/compiler/rustc_hir_pretty/src/lib.rs index 1a401af1d328d..637ae115131a1 100644 --- a/compiler/rustc_hir_pretty/src/lib.rs +++ b/compiler/rustc_hir_pretty/src/lib.rs @@ -1645,6 +1645,7 @@ impl<'a> State<'a> { fn_arg_span: _, kind: _, def_id: _, + explicit_captures: _, }) => { self.print_closure_binder(binder, bound_generic_params); self.print_constness(constness); diff --git a/tests/ui/move-expr/outside-plain-closure.rs b/tests/ui/move-expr/outside-plain-closure.rs new file mode 100644 index 0000000000000..c4aa6551119fe --- /dev/null +++ b/tests/ui/move-expr/outside-plain-closure.rs @@ -0,0 +1,7 @@ +#![allow(incomplete_features)] +#![feature(move_expr)] + +fn main() { + let _ = move(String::from("nope")); + //~^ ERROR `move(expr)` is only supported in plain closures +} diff --git a/tests/ui/move-expr/outside-plain-closure.stderr b/tests/ui/move-expr/outside-plain-closure.stderr new file mode 100644 index 0000000000000..68c4223641304 --- /dev/null +++ b/tests/ui/move-expr/outside-plain-closure.stderr @@ -0,0 +1,8 @@ +error: `move(expr)` is only supported in plain closures + --> $DIR/outside-plain-closure.rs:5:13 + | +LL | let _ = move(String::from("nope")); + | ^^^^ + +error: aborting due to 1 previous error + diff --git a/tests/ui/move-expr/plain-closure.rs b/tests/ui/move-expr/plain-closure.rs new file mode 100644 index 0000000000000..1047425b2d003 --- /dev/null +++ b/tests/ui/move-expr/plain-closure.rs @@ -0,0 +1,12 @@ +//@ check-pass +#![allow(incomplete_features)] +#![feature(move_expr)] + +fn main() { + let s = String::from("hello"); + let c = || { + let t = move(s); + println!("{}", t.len()); + }; + c(); +} From 0349562e92595f888ad12f2fcdfc39acf8bd2b66 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 9 Apr 2026 21:20:20 +0900 Subject: [PATCH 04/17] force move(expr) captures to ByValue --- compiler/rustc_hir_typeck/src/upvar.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/compiler/rustc_hir_typeck/src/upvar.rs b/compiler/rustc_hir_typeck/src/upvar.rs index dfdc7c3d8df0a..1d57a71b104d0 100644 --- a/compiler/rustc_hir_typeck/src/upvar.rs +++ b/compiler/rustc_hir_typeck/src/upvar.rs @@ -208,6 +208,22 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> { let _ = euv::ExprUseVisitor::new(&closure_fcx, &mut delegate).consume_body(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.capture_information.push(( + place, + ty::CaptureInfo { + capture_kind_expr_id: Some(capture.var_hir_id), + path_expr_id: Some(capture.var_hir_id), + capture_kind: UpvarCapture::ByValue, + }, + )); + } + // There are several curious situations with coroutine-closures where // analysis is too aggressive with borrows when the coroutine-closure is // marked `move`. Specifically: From 7fb00e119175841af9ad0363e67aba3a07fa6dab Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 9 Apr 2026 22:26:35 +0900 Subject: [PATCH 05/17] support `ast::ExprKind::Move` in clippy --- src/tools/clippy/clippy_utils/src/sugg.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tools/clippy/clippy_utils/src/sugg.rs b/src/tools/clippy/clippy_utils/src/sugg.rs index 92f08b604ca58..f194103ae2e88 100644 --- a/src/tools/clippy/clippy_utils/src/sugg.rs +++ b/src/tools/clippy/clippy_utils/src/sugg.rs @@ -231,6 +231,7 @@ impl<'a> Sugg<'a> { | ast::ExprKind::Loop(..) | ast::ExprKind::MacCall(..) | ast::ExprKind::MethodCall(..) + | ast::ExprKind::Move(..) | ast::ExprKind::Paren(..) | ast::ExprKind::Underscore | ast::ExprKind::Path(..) From fa706711f741a05c3214b538fef12712b800d6e1 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 9 Apr 2026 23:12:40 +0900 Subject: [PATCH 06/17] support `ast::ExprKind::Move` in rustfmt --- src/tools/rustfmt/src/expr.rs | 4 ++++ src/tools/rustfmt/src/utils.rs | 1 + 2 files changed, 5 insertions(+) diff --git a/src/tools/rustfmt/src/expr.rs b/src/tools/rustfmt/src/expr.rs index d34706a2ba5cd..daffc215c621c 100644 --- a/src/tools/rustfmt/src/expr.rs +++ b/src/tools/rustfmt/src/expr.rs @@ -125,6 +125,10 @@ pub(crate) fn format_expr( let callee_str = callee.rewrite_result(context, shape)?; rewrite_call(context, &callee_str, args, inner_span, shape) } + ast::ExprKind::Move(ref subexpr, move_kw_span) => { + let inner_span = mk_sp(move_kw_span.hi(), expr.span.hi()); + rewrite_call(context, "move", std::slice::from_ref(subexpr), inner_span, shape) + } ast::ExprKind::Paren(ref subexpr) => rewrite_paren(context, subexpr, shape, expr.span), ast::ExprKind::Binary(op, ref lhs, ref rhs) => { // FIXME: format comments between operands and operator diff --git a/src/tools/rustfmt/src/utils.rs b/src/tools/rustfmt/src/utils.rs index b052e74d8bf20..de72c9ce14bc3 100644 --- a/src/tools/rustfmt/src/utils.rs +++ b/src/tools/rustfmt/src/utils.rs @@ -553,6 +553,7 @@ pub(crate) fn is_block_expr(context: &RewriteContext<'_>, expr: &ast::Expr, repr | ast::ExprKind::Field(..) | ast::ExprKind::IncludedBytes(..) | ast::ExprKind::InlineAsm(..) + | ast::ExprKind::Move(..) | ast::ExprKind::OffsetOf(..) | ast::ExprKind::UnsafeBinderCast(..) | ast::ExprKind::Let(..) From 1c2edccdbfc0600019ae5d6728cff37155fd33a1 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 16 Apr 2026 21:24:46 +0900 Subject: [PATCH 07/17] fix tidy errors replace TODO with FIXME --- compiler/rustc_ast_lowering/src/expr.rs | 29 +++++++++---------- compiler/rustc_feature/src/unstable.rs | 4 +-- compiler/rustc_hir/src/hir.rs | 1 - compiler/rustc_hir_typeck/src/upvar.rs | 4 +-- src/tools/rustfmt/src/expr.rs | 8 ++++- tests/ui/README.md | 4 +++ .../feature-gate-move_expr.stderr | 1 + 7 files changed, 30 insertions(+), 21 deletions(-) diff --git a/compiler/rustc_ast_lowering/src/expr.rs b/compiler/rustc_ast_lowering/src/expr.rs index a5d07f02197a9..9d29a1037b60a 100644 --- a/compiler/rustc_ast_lowering/src/expr.rs +++ b/compiler/rustc_ast_lowering/src/expr.rs @@ -250,10 +250,7 @@ impl<'hir> LoweringContext<'_, 'hir> { ExprKind::Await(expr, await_kw_span) => self.lower_expr_await(*await_kw_span, expr), ExprKind::Move(_, move_kw_span) => { if !self.tcx.features().move_expr() { - return self.expr_err( - *move_kw_span, - self.dcx().span_delayed_bug(*move_kw_span, "invalid move(expr)"), - ); + return self.expr_err(*move_kw_span, self.dcx().has_errors().unwrap()); } if let Some((ident, binding)) = self .move_expr_bindings @@ -276,10 +273,10 @@ impl<'hir> LoweringContext<'_, 'hir> { }), )) } else { - self.dcx().emit_err(MoveExprOnlyInPlainClosures { span: *move_kw_span }); - hir::ExprKind::Err( - self.dcx().span_delayed_bug(*move_kw_span, "invalid move(expr)"), - ) + let guar = self + .dcx() + .emit_err(MoveExprOnlyInPlainClosures { span: *move_kw_span }); + hir::ExprKind::Err(guar) } } ExprKind::Use(expr, use_kw_span) => self.lower_expr_use(*use_kw_span, expr), @@ -1087,6 +1084,8 @@ impl<'hir> LoweringContext<'_, 'hir> { let attrs = self.lower_attrs(expr_hir_id, &e.attrs, e.span, Target::from_expr(e)); match closure.coroutine_kind { + // FIXME(TaKO8Ki): Support `move(expr)` in coroutine closures too. + // For the first step, we only support plain closures. Some(coroutine_kind) => hir::Expr { hir_id: expr_hir_id, kind: self.lower_expr_coroutine_closure( @@ -1179,12 +1178,11 @@ impl<'hir> LoweringContext<'_, 'hir> { )); } - let explicit_captures = self.arena.alloc_from_iter(lowered_occurrences.iter().map( - |(occurrence, _, binding)| hir::ExplicitCapture { - var_hir_id: *binding, - origin_span: self.lower_span(occurrence.move_kw_span), - }, - )); + let explicit_captures = self.arena.alloc_from_iter( + lowered_occurrences + .iter() + .map(|(_, _, binding)| hir::ExplicitCapture { var_hir_id: *binding }), + ); let closure_expr = self.arena.alloc(hir::Expr { hir_id: expr_hir_id, @@ -1380,9 +1378,10 @@ impl<'hir> LoweringContext<'_, 'hir> { // knows that a `FnDecl` output type like `-> &str` actually means // "coroutine that returns &str", rather than directly returning a `&str`. kind: hir::ClosureKind::CoroutineClosure(coroutine_desugaring), - constness: hir::Constness::NotConst, + constness: self.lower_constness(constness), explicit_captures: &[], }); + hir::ExprKind::Closure(c) } diff --git a/compiler/rustc_feature/src/unstable.rs b/compiler/rustc_feature/src/unstable.rs index 93fc3e0478b6a..17f84154b9fff 100644 --- a/compiler/rustc_feature/src/unstable.rs +++ b/compiler/rustc_feature/src/unstable.rs @@ -638,6 +638,8 @@ declare_features! ( (unstable, mips_target_feature, "1.27.0", Some(150253)), /// Allows qualified paths in struct expressions, struct patterns and tuple struct patterns. (unstable, more_qualified_paths, "1.54.0", Some(86935)), + /// Allows `move(expr)` in closures. + (incomplete, move_expr, "CURRENT_RUSTC_VERSION", Some(155050)), /// The `movrs` target feature on x86. (unstable, movrs_target_feature, "1.88.0", Some(137976)), /// Allows the `multiple_supertrait_upcastable` lint. @@ -646,8 +648,6 @@ declare_features! ( (unstable, must_not_suspend, "1.57.0", Some(83310)), /// Allows `mut ref` and `mut ref mut` identifier patterns. (incomplete, mut_ref, "1.79.0", Some(123076)), - /// Allows `move(expr)` in closures. - (incomplete, move_expr, "CURRENT_RUSTC_VERSION", None), /// Allows using `#[naked]` on `extern "Rust"` functions. (unstable, naked_functions_rustic_abi, "1.88.0", Some(138997)), /// Allows using `#[target_feature(enable = "...")]` on `#[naked]` on functions. diff --git a/compiler/rustc_hir/src/hir.rs b/compiler/rustc_hir/src/hir.rs index 668367e066a40..1c1a268462832 100644 --- a/compiler/rustc_hir/src/hir.rs +++ b/compiler/rustc_hir/src/hir.rs @@ -1688,7 +1688,6 @@ pub struct Closure<'hir> { #[derive(Debug, Clone, Copy, HashStable_Generic)] pub struct ExplicitCapture { pub var_hir_id: HirId, - pub origin_span: Span, } #[derive(Clone, PartialEq, Eq, Debug, Copy, Hash, StableHash, Encodable, Decodable)] diff --git a/compiler/rustc_hir_typeck/src/upvar.rs b/compiler/rustc_hir_typeck/src/upvar.rs index 1d57a71b104d0..15af428d8ec49 100644 --- a/compiler/rustc_hir_typeck/src/upvar.rs +++ b/compiler/rustc_hir_typeck/src/upvar.rs @@ -217,8 +217,8 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> { delegate.capture_information.push(( place, ty::CaptureInfo { - capture_kind_expr_id: Some(capture.var_hir_id), - path_expr_id: Some(capture.var_hir_id), + capture_kind_expr_id: Some(closure_hir_id), + path_expr_id: Some(closure_hir_id), capture_kind: UpvarCapture::ByValue, }, )); diff --git a/src/tools/rustfmt/src/expr.rs b/src/tools/rustfmt/src/expr.rs index daffc215c621c..8a3674bff1ca6 100644 --- a/src/tools/rustfmt/src/expr.rs +++ b/src/tools/rustfmt/src/expr.rs @@ -127,7 +127,13 @@ pub(crate) fn format_expr( } ast::ExprKind::Move(ref subexpr, move_kw_span) => { let inner_span = mk_sp(move_kw_span.hi(), expr.span.hi()); - rewrite_call(context, "move", std::slice::from_ref(subexpr), inner_span, shape) + rewrite_call( + context, + "move", + std::slice::from_ref(subexpr), + inner_span, + shape, + ) } ast::ExprKind::Paren(ref subexpr) => rewrite_paren(context, subexpr, shape, expr.span), ast::ExprKind::Binary(op, ref lhs, ref rhs) => { diff --git a/tests/ui/README.md b/tests/ui/README.md index 2fe1657e7ecf2..1ab22a65ce507 100644 --- a/tests/ui/README.md +++ b/tests/ui/README.md @@ -927,6 +927,10 @@ Tests on the module system. **FIXME**: `tests/ui/imports/` should probably be merged with this. +## `tests/ui/move-expr/` + +Tests for `#![feature(move_expr)]`. + ## `tests/ui/moves` Tests on moves (destructive moves). diff --git a/tests/ui/feature-gates/feature-gate-move_expr.stderr b/tests/ui/feature-gates/feature-gate-move_expr.stderr index 28ab95ababc16..8b1da2c06893d 100644 --- a/tests/ui/feature-gates/feature-gate-move_expr.stderr +++ b/tests/ui/feature-gates/feature-gate-move_expr.stderr @@ -4,6 +4,7 @@ error[E0658]: `move(expr)` syntax is experimental LL | let _ = || move(2); | ^^^^ | + = note: see issue #155050 for more information = help: add `#![feature(move_expr)]` to the crate attributes to enable = note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date From 5ea6b0fe4620970e827ac98434004db00413d6b3 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 16 Apr 2026 21:25:35 +0900 Subject: [PATCH 08/17] add ui tests for move expr --- tests/ui/move-expr/borrow-only.rs | 13 +++++++++++ tests/ui/move-expr/capture-reference.rs | 15 +++++++++++++ tests/ui/move-expr/capture-reference.stderr | 17 ++++++++++++++ tests/ui/move-expr/copy-type.rs | 15 +++++++++++++ tests/ui/move-expr/double-move.rs | 13 +++++++++++ tests/ui/move-expr/double-move.stderr | 23 +++++++++++++++++++ tests/ui/move-expr/move-fnonce.rs | 14 ++++++++++++ tests/ui/move-expr/move-fnonce.stderr | 22 ++++++++++++++++++ tests/ui/move-expr/name-resolution.rs | 13 +++++++++++ tests/ui/move-expr/nested-closures.rs | 15 +++++++++++++ tests/ui/move-expr/use-after-move.rs | 12 ++++++++++ tests/ui/move-expr/use-after-move.stderr | 25 +++++++++++++++++++++ 12 files changed, 197 insertions(+) create mode 100644 tests/ui/move-expr/borrow-only.rs create mode 100644 tests/ui/move-expr/capture-reference.rs create mode 100644 tests/ui/move-expr/capture-reference.stderr create mode 100644 tests/ui/move-expr/copy-type.rs create mode 100644 tests/ui/move-expr/double-move.rs create mode 100644 tests/ui/move-expr/double-move.stderr create mode 100644 tests/ui/move-expr/move-fnonce.rs create mode 100644 tests/ui/move-expr/move-fnonce.stderr create mode 100644 tests/ui/move-expr/name-resolution.rs create mode 100644 tests/ui/move-expr/nested-closures.rs create mode 100644 tests/ui/move-expr/use-after-move.rs create mode 100644 tests/ui/move-expr/use-after-move.stderr diff --git a/tests/ui/move-expr/borrow-only.rs b/tests/ui/move-expr/borrow-only.rs new file mode 100644 index 0000000000000..f5c6ba228cfb7 --- /dev/null +++ b/tests/ui/move-expr/borrow-only.rs @@ -0,0 +1,13 @@ +//@ check-pass +#![allow(incomplete_features)] +#![feature(move_expr)] + +fn main() { + let s = vec![1, 2, 3]; + let c = || { + let t = &move(s); + println!("{t:?}"); + }; + + c(); +} diff --git a/tests/ui/move-expr/capture-reference.rs b/tests/ui/move-expr/capture-reference.rs new file mode 100644 index 0000000000000..a4c2caee57052 --- /dev/null +++ b/tests/ui/move-expr/capture-reference.rs @@ -0,0 +1,15 @@ +#![allow(incomplete_features)] +#![feature(move_expr)] + +fn main() { + let c = { + let x = 22; + || { + let y = move(&x); + //~^ ERROR `x` does not live long enough + println!("{y:?}"); + } + }; + + c(); +} diff --git a/tests/ui/move-expr/capture-reference.stderr b/tests/ui/move-expr/capture-reference.stderr new file mode 100644 index 0000000000000..8b970e98d6bdb --- /dev/null +++ b/tests/ui/move-expr/capture-reference.stderr @@ -0,0 +1,17 @@ +error[E0597]: `x` does not live long enough + --> $DIR/capture-reference.rs:8:26 + | +LL | let c = { + | - borrow later captured here by closure +LL | let x = 22; + | - binding `x` declared here +LL | || { +LL | let y = move(&x); + | ^^ borrowed value does not live long enough +... +LL | }; + | - `x` dropped here while still borrowed + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0597`. diff --git a/tests/ui/move-expr/copy-type.rs b/tests/ui/move-expr/copy-type.rs new file mode 100644 index 0000000000000..4cc790f7103ff --- /dev/null +++ b/tests/ui/move-expr/copy-type.rs @@ -0,0 +1,15 @@ +//@ check-pass +#![allow(incomplete_features)] +#![feature(move_expr)] + +fn main() { + let x = 22; + let c = || { + let y = move(x); + let z = x; + assert_eq!(y + z, 44); + }; + + c(); + c(); +} diff --git a/tests/ui/move-expr/double-move.rs b/tests/ui/move-expr/double-move.rs new file mode 100644 index 0000000000000..2d4beb2246e1e --- /dev/null +++ b/tests/ui/move-expr/double-move.rs @@ -0,0 +1,13 @@ +#![allow(incomplete_features)] +#![feature(move_expr)] + +fn main() { + let x = vec![1, 2, 3]; + let _c = || { + let y = move(x); + let z = move(x); + //~^ ERROR use of moved value: `x` + drop(y); + drop(z); + }; +} diff --git a/tests/ui/move-expr/double-move.stderr b/tests/ui/move-expr/double-move.stderr new file mode 100644 index 0000000000000..ddb01814f7260 --- /dev/null +++ b/tests/ui/move-expr/double-move.stderr @@ -0,0 +1,23 @@ +error[E0382]: use of moved value: `x` + --> $DIR/double-move.rs:8:22 + | +LL | let x = vec![1, 2, 3]; + | - move occurs because `x` has type `Vec`, which does not implement the `Copy` trait +LL | let _c = || { +LL | let y = move(x); + | - value moved here +LL | let z = move(x); + | ^ value used here after move + | +help: consider cloning the value if the performance cost is acceptable + | +LL | let y = move(x.clone()); + | ++++++++ +help: borrow this binding in the pattern to avoid moving the value + | +LL | let y = move(ref x); + | +++ + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0382`. diff --git a/tests/ui/move-expr/move-fnonce.rs b/tests/ui/move-expr/move-fnonce.rs new file mode 100644 index 0000000000000..a9d44cad19502 --- /dev/null +++ b/tests/ui/move-expr/move-fnonce.rs @@ -0,0 +1,14 @@ +#![allow(incomplete_features)] +#![feature(move_expr)] + +fn main() { + let s = vec![1, 2, 3]; + let mut c = || { + let t = move(s); + println!("{t:?}"); + }; + + c(); + c(); + //~^ ERROR use of moved value: `c` +} diff --git a/tests/ui/move-expr/move-fnonce.stderr b/tests/ui/move-expr/move-fnonce.stderr new file mode 100644 index 0000000000000..635b3cd6a4fa2 --- /dev/null +++ b/tests/ui/move-expr/move-fnonce.stderr @@ -0,0 +1,22 @@ +error[E0382]: use of moved value: `c` + --> $DIR/move-fnonce.rs:12:5 + | +LL | c(); + | --- `c` moved due to this call +LL | c(); + | ^ value used here after move + | +note: closure cannot be invoked more than once because it moves the variable `__move_expr_0` out of its environment + --> $DIR/move-fnonce.rs:7:17 + | +LL | let t = move(s); + | ^^^^^^^ +note: this value implements `FnOnce`, which causes it to be moved when called + --> $DIR/move-fnonce.rs:11:5 + | +LL | c(); + | ^ + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0382`. diff --git a/tests/ui/move-expr/name-resolution.rs b/tests/ui/move-expr/name-resolution.rs new file mode 100644 index 0000000000000..7c0886183eb36 --- /dev/null +++ b/tests/ui/move-expr/name-resolution.rs @@ -0,0 +1,13 @@ +//@ check-pass +//@ ignore-test (#155050): currently ICEs instead of reporting a name-resolution error +// FIXME(TaKO8Ki): Remove this ignore once closure-local names in `move(expr)` produce a real +// diagnostic instead of hitting the current `Res::Err` ICE path. +#![allow(incomplete_features)] +#![feature(move_expr)] + +fn main() { + let _c = || { + let x = 3; + move(x); + }; +} diff --git a/tests/ui/move-expr/nested-closures.rs b/tests/ui/move-expr/nested-closures.rs new file mode 100644 index 0000000000000..b472dca4c0024 --- /dev/null +++ b/tests/ui/move-expr/nested-closures.rs @@ -0,0 +1,15 @@ +//@ check-pass +#![allow(incomplete_features)] +#![feature(move_expr)] + +fn main() { + let x = String::from("hello"); + let outer = || { + let inner = || move(x.clone()); + let y = inner(); + assert_eq!(y, "hello"); + assert_eq!(x, "hello"); + }; + + outer(); +} diff --git a/tests/ui/move-expr/use-after-move.rs b/tests/ui/move-expr/use-after-move.rs new file mode 100644 index 0000000000000..69f2bf35125df --- /dev/null +++ b/tests/ui/move-expr/use-after-move.rs @@ -0,0 +1,12 @@ +#![allow(incomplete_features)] +#![feature(move_expr)] + +fn main() { + let x = vec![1, 2, 3]; + let _c = || { + //~^ ERROR borrow of moved value: `x` + let y = move(x); + println!("{x:?}"); + drop(y); + }; +} diff --git a/tests/ui/move-expr/use-after-move.stderr b/tests/ui/move-expr/use-after-move.stderr new file mode 100644 index 0000000000000..4b97f812815c0 --- /dev/null +++ b/tests/ui/move-expr/use-after-move.stderr @@ -0,0 +1,25 @@ +error[E0382]: borrow of moved value: `x` + --> $DIR/use-after-move.rs:6:14 + | +LL | let x = vec![1, 2, 3]; + | - move occurs because `x` has type `Vec`, which does not implement the `Copy` trait +LL | let _c = || { + | ^^ value borrowed here after move +LL | +LL | let y = move(x); + | - value moved here +LL | println!("{x:?}"); + | - borrow occurs due to use in closure + | +help: consider cloning the value if the performance cost is acceptable + | +LL | let y = move(x.clone()); + | ++++++++ +help: borrow this binding in the pattern to avoid moving the value + | +LL | let y = move(ref x); + | +++ + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0382`. From ee5a270c81449f1c010dac79bfeffce391e1564f Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 16 Apr 2026 21:49:10 +0900 Subject: [PATCH 09/17] use pre-expansion feature gate, `gate_all!` instead --- compiler/rustc_ast_passes/src/feature_gate.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/compiler/rustc_ast_passes/src/feature_gate.rs b/compiler/rustc_ast_passes/src/feature_gate.rs index b0679487b8ebb..b43dbad611a0e 100644 --- a/compiler/rustc_ast_passes/src/feature_gate.rs +++ b/compiler/rustc_ast_passes/src/feature_gate.rs @@ -352,9 +352,6 @@ impl<'a> Visitor<'a> for PostExpansionVisitor<'a> { } _ => (), }, - ast::ExprKind::Move(_, move_kw_span) => { - gate!(&self, move_expr, move_kw_span, "`move(expr)` syntax is experimental"); - } _ => {} } visit::walk_expr(self, e) @@ -506,6 +503,7 @@ pub fn check_crate(krate: &ast::Crate, sess: &Session, features: &Features) { gate_all!(impl_restriction, "`impl` restrictions are experimental"); gate_all!(min_generic_const_args, "unbraced const blocks as const args are experimental"); gate_all!(more_qualified_paths, "usage of qualified paths in this context is experimental"); + gate_all!(move_expr, "`move(expr)` syntax is experimental"); gate_all!(mut_ref, "mutable by-reference bindings are experimental"); gate_all!(pin_ergonomics, "pinned reference syntax is experimental"); gate_all!(postfix_match, "postfix match is experimental"); From f848f32a9fd49eb100071dfb13efa120d6ea21b5 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 30 Apr 2026 15:22:46 +0900 Subject: [PATCH 10/17] use ExprUseVisitor delegate for explicit move captures --- compiler/rustc_hir_typeck/src/upvar.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/compiler/rustc_hir_typeck/src/upvar.rs b/compiler/rustc_hir_typeck/src/upvar.rs index 15af428d8ec49..5911f1c9e26ef 100644 --- a/compiler/rustc_hir_typeck/src/upvar.rs +++ b/compiler/rustc_hir_typeck/src/upvar.rs @@ -54,6 +54,7 @@ use tracing::{debug, instrument}; use super::FnCtxt; use crate::expr_use_visitor as euv; +use crate::expr_use_visitor::Delegate as _; /// Describe the relationship between the paths of two places /// eg: @@ -214,14 +215,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> { }; for capture in explicit_captures { let place = closure_fcx.place_for_root_variable(closure_def_id, capture.var_hir_id); - delegate.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, - }, - )); + delegate.consume(&PlaceWithHirId { hir_id: capture.var_hir_id, place }, closure_hir_id); } // There are several curious situations with coroutine-closures where From cf937b8da2a273199b22f4a45e65e73a320f9128 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 30 Apr 2026 15:58:58 +0900 Subject: [PATCH 11/17] test multiple move expressions in one closure --- tests/ui/move-expr/plain-closure.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/ui/move-expr/plain-closure.rs b/tests/ui/move-expr/plain-closure.rs index 1047425b2d003..2ec01e35569b4 100644 --- a/tests/ui/move-expr/plain-closure.rs +++ b/tests/ui/move-expr/plain-closure.rs @@ -9,4 +9,13 @@ fn main() { println!("{}", t.len()); }; c(); + + let a = String::from("hello"); + let b = String::from("world"); + let c = || { + let x = move(a); + let y = move(b); + println!("{} {}", x, y); + }; + c(); } From aac8bd23bb51624dc845f060fd2117e3d75af86b Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 30 Apr 2026 15:59:38 +0900 Subject: [PATCH 12/17] add move expression parser ambiguity tests --- tests/ui/move-expr/parse-ambiguity-errors.rs | 14 ++++++++++++++ tests/ui/move-expr/parse-ambiguity-errors.stderr | 14 ++++++++++++++ tests/ui/move-expr/parse-ambiguity.rs | 13 +++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 tests/ui/move-expr/parse-ambiguity-errors.rs create mode 100644 tests/ui/move-expr/parse-ambiguity-errors.stderr create mode 100644 tests/ui/move-expr/parse-ambiguity.rs diff --git a/tests/ui/move-expr/parse-ambiguity-errors.rs b/tests/ui/move-expr/parse-ambiguity-errors.rs new file mode 100644 index 0000000000000..c2927373cb8a7 --- /dev/null +++ b/tests/ui/move-expr/parse-ambiguity-errors.rs @@ -0,0 +1,14 @@ +#![allow(incomplete_features)] +#![feature(move_expr)] + +fn main() { + let x: bool = true; + let y: bool = true; + let _ = move(x) || y; + //~^ ERROR `move(expr)` is only supported in plain closures + + let x: bool = true; + let y: bool = true; + let _ = move[x] || y; + //~^ ERROR expected one of +} diff --git a/tests/ui/move-expr/parse-ambiguity-errors.stderr b/tests/ui/move-expr/parse-ambiguity-errors.stderr new file mode 100644 index 0000000000000..c4dc929eac36c --- /dev/null +++ b/tests/ui/move-expr/parse-ambiguity-errors.stderr @@ -0,0 +1,14 @@ +error: expected one of `async`, `|`, or `||`, found `[` + --> $DIR/parse-ambiguity-errors.rs:12:17 + | +LL | let _ = move[x] || y; + | ^ expected one of `async`, `|`, or `||` + +error: `move(expr)` is only supported in plain closures + --> $DIR/parse-ambiguity-errors.rs:7:13 + | +LL | let _ = move(x) || y; + | ^^^^ + +error: aborting due to 2 previous errors + diff --git a/tests/ui/move-expr/parse-ambiguity.rs b/tests/ui/move-expr/parse-ambiguity.rs new file mode 100644 index 0000000000000..bfe4b82b2b0ff --- /dev/null +++ b/tests/ui/move-expr/parse-ambiguity.rs @@ -0,0 +1,13 @@ +//@ check-pass +#![allow(incomplete_features)] +#![feature(move_expr)] + +fn main() { + let x: bool = true; + let y: bool = true; + let _ = || move(x) || y; + + let x: bool = true; + let y: bool = true; + let _ = move || move(x) || y; +} From fc6f09e19b9d6792f02f745edda0e591f8ae03df Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 30 Apr 2026 16:25:40 +0900 Subject: [PATCH 13/17] document move expression lowering bindings --- compiler/rustc_ast_lowering/src/expr.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/compiler/rustc_ast_lowering/src/expr.rs b/compiler/rustc_ast_lowering/src/expr.rs index 9d29a1037b60a..fc88b70267152 100644 --- a/compiler/rustc_ast_lowering/src/expr.rs +++ b/compiler/rustc_ast_lowering/src/expr.rs @@ -252,6 +252,9 @@ impl<'hir> LoweringContext<'_, 'hir> { if !self.tcx.features().move_expr() { return self.expr_err(*move_kw_span, self.dcx().has_errors().unwrap()); } + // `last()` selects the binding map for the closure body currently + // being lowered. The map is keyed by the AST `NodeId`, so `e.id` + // selects the synthetic local for this exact `move(...)` occurrence. if let Some((ident, binding)) = self .move_expr_bindings .last() @@ -1165,6 +1168,9 @@ impl<'hir> LoweringContext<'_, 'hir> { lowered_occurrences.push((occurrence, pat, binding)); } + // During body lowering, replace each `move(...)` occurrence with the + // synthetic local recorded in this closure's binding map. Nested closures + // push their own maps. self.move_expr_bindings.push(bindings); let mut stmts = Vec::with_capacity(lowered_occurrences.len()); for (occurrence, pat, _) in &lowered_occurrences { @@ -1201,6 +1207,8 @@ impl<'hir> LoweringContext<'_, 'hir> { ), span: self.lower_span(whole_span), }); + // Restore the enclosing closure's substitution map before lowering the + // block that contains the synthetic `let`s. self.move_expr_bindings.pop(); let stmts = self.arena.alloc_from_iter(stmts); From 01589557335237013d33092fddc1b350c58901e7 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 30 Apr 2026 17:44:28 +0900 Subject: [PATCH 14/17] document move expression lowering and capture flow document move expression lowering flow --- compiler/rustc_ast_lowering/src/expr.rs | 40 ++++++++++++++++++++++++- compiler/rustc_ast_lowering/src/lib.rs | 3 ++ compiler/rustc_hir/src/hir.rs | 2 ++ compiler/rustc_hir_typeck/src/upvar.rs | 7 +++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/compiler/rustc_ast_lowering/src/expr.rs b/compiler/rustc_ast_lowering/src/expr.rs index fc88b70267152..49df2ddcbf3e1 100644 --- a/compiler/rustc_ast_lowering/src/expr.rs +++ b/compiler/rustc_ast_lowering/src/expr.rs @@ -31,12 +31,22 @@ use crate::{AllowReturnTypeNotation, FnDeclKind, ImplTraitPosition, TryBlockScop pub(super) struct WillCreateDefIdsVisitor; +/// A `move(...)` expression found while scanning a plain closure body. struct MoveExprOccurrence<'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, } +/// Collects the `move(...)` expressions that belong to one plain closure body. +/// +/// For `|| move(foo.bar).clone()`, this records the outer `move(foo.bar)` +/// occurrence and the inner expression `foo.bar`. Nested closures, generators, +/// const blocks, and items are lowered as separate bodies, so this visitor does +/// not collect `move(...)` expressions from them. struct MoveExprCollector<'a> { occurrences: Vec>, } @@ -53,6 +63,8 @@ impl<'a> Visitor<'a> for MoveExprCollector<'a> { fn visit_expr(&mut self, expr: &'a Expr) { match &expr.kind { ExprKind::Move(inner, move_kw_span) => { + // For `move(foo.bar)`, first collect any nested `move(...)` + // expressions in `foo.bar`, then record this outer occurrence. self.visit_expr(inner); self.occurrences.push(MoveExprOccurrence { id: expr.id, @@ -64,6 +76,8 @@ impl<'a> Visitor<'a> for MoveExprCollector<'a> { _ => walk_expr(self, expr), } } + + fn visit_item(&mut self, _: &'a Item) {} } impl<'v> rustc_ast::visit::Visitor<'v> for WillCreateDefIdsVisitor { @@ -1082,6 +1096,8 @@ impl<'hir> LoweringContext<'_, 'hir> { hir::ExprKind::Use(self.lower_expr(expr), self.lower_span(use_kw_span)) } + // Lowers closure expressions, including the `move(...)` desugaring for + // plain closures. fn lower_expr_closure_expr(&mut self, e: &Expr, closure: &Closure) -> hir::Expr<'hir> { let expr_hir_id = self.lower_node_id(e.id); let attrs = self.lower_attrs(expr_hir_id, &e.attrs, e.span, Target::from_expr(e)); @@ -1137,8 +1153,18 @@ impl<'hir> LoweringContext<'_, 'hir> { fn_arg_span: Span, whole_span: Span, ) -> hir::Expr<'hir> { + // `move(...)` evaluates its inner expression when the closure is created + // and captures the result by value. For example: + // + // `|| move(foo).bar` + // + // is lowered roughly as: + // + // `let __move_expr_0 = foo; || __move_expr_0.bar` let occurrences = MoveExprCollector::collect(body); if occurrences.is_empty() { + // No `move(...)` expressions in this closure body; lower the closure + // normally, with no explicit captures. return hir::Expr { hir_id: expr_hir_id, kind: self.lower_expr_closure( @@ -1161,6 +1187,9 @@ impl<'hir> LoweringContext<'_, 'hir> { let mut bindings = NodeMap::default(); let mut lowered_occurrences = Vec::with_capacity(occurrences.len()); for (index, occurrence) in occurrences.iter().enumerate() { + // Create one synthetic local per `move(...)` expression and remember + // which AST node should be replaced by that local while lowering the + // closure body. let ident = Ident::from_str_and_span(&format!("__move_expr_{index}"), occurrence.move_kw_span); let (pat, binding) = self.pat_ident(occurrence.expr.span, ident); @@ -1174,6 +1203,10 @@ impl<'hir> LoweringContext<'_, 'hir> { self.move_expr_bindings.push(bindings); let mut stmts = Vec::with_capacity(lowered_occurrences.len()); for (occurrence, pat, _) in &lowered_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 init = self.lower_expr(occurrence.expr); stmts.push(self.stmt_let_pat( None, @@ -1187,9 +1220,15 @@ impl<'hir> LoweringContext<'_, 'hir> { let explicit_captures = self.arena.alloc_from_iter( lowered_occurrences .iter() + // Force the generated locals to be captured by value even if + // the lowered closure body only borrows them, as in + // `move(foo).clone()`. .map(|(_, _, binding)| hir::ExplicitCapture { var_hir_id: *binding }), ); + // Lower the closure itself while `move_expr_bindings` contains this + // closure's substitutions, so each `move(...)` in the body is replaced + // with its generated local. let closure_expr = self.arena.alloc(hir::Expr { hir_id: expr_hir_id, kind: self.lower_expr_closure( @@ -1389,7 +1428,6 @@ impl<'hir> LoweringContext<'_, 'hir> { constness: self.lower_constness(constness), 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 3af3e1b0e544a..43c24c6a61268 100644 --- a/compiler/rustc_ast_lowering/src/lib.rs +++ b/compiler/rustc_ast_lowering/src/lib.rs @@ -158,6 +158,9 @@ struct LoweringContext<'a, 'hir> { allow_async_fn_traits: Arc<[Symbol]>, delayed_lints: Vec, + /// Stack of per-closure `move(...)` substitution maps. Each map is keyed by + /// the AST `NodeId` of a `move(...)` occurrence and points to the synthetic + /// local used while lowering that closure body. move_expr_bindings: Vec>, attribute_parser: AttributeParser<'hir>, diff --git a/compiler/rustc_hir/src/hir.rs b/compiler/rustc_hir/src/hir.rs index 1c1a268462832..299b045370c9e 100644 --- a/compiler/rustc_hir/src/hir.rs +++ b/compiler/rustc_hir/src/hir.rs @@ -1685,6 +1685,8 @@ pub struct Closure<'hir> { pub explicit_captures: &'hir [ExplicitCapture], } +/// A HIR local that must be captured by value even if ordinary closure capture +/// analysis would infer a weaker capture kind from its uses in the body. #[derive(Debug, Clone, Copy, HashStable_Generic)] pub struct ExplicitCapture { pub var_hir_id: HirId, diff --git a/compiler/rustc_hir_typeck/src/upvar.rs b/compiler/rustc_hir_typeck/src/upvar.rs index 5911f1c9e26ef..25b5540457f19 100644 --- a/compiler/rustc_hir_typeck/src/upvar.rs +++ b/compiler/rustc_hir_typeck/src/upvar.rs @@ -207,8 +207,15 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> { fake_reads: Default::default(), }; + // First collect the captures implied by the operations in the closure + // body. This records how each place is actually used: borrowed, modified, + // 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. 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), From 7a80a36c4549db05d0ae41c8c84a4307814538df Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 30 Apr 2026 21:38:24 +0900 Subject: [PATCH 15/17] use `HashStable` instead of `HashStable_Generic` --- compiler/rustc_hir/src/hir.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/rustc_hir/src/hir.rs b/compiler/rustc_hir/src/hir.rs index 299b045370c9e..0a7aecb6470a0 100644 --- a/compiler/rustc_hir/src/hir.rs +++ b/compiler/rustc_hir/src/hir.rs @@ -1687,7 +1687,7 @@ pub struct Closure<'hir> { /// A HIR local that must be captured by value even if ordinary closure capture /// analysis would infer a weaker capture kind from its uses in the body. -#[derive(Debug, Clone, Copy, HashStable_Generic)] +#[derive(Debug, Clone, Copy, HashStable)] pub struct ExplicitCapture { pub var_hir_id: HirId, } From 16a446ec0b0e594242160f45273c2f7ec32c4870 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 7 May 2026 22:35:31 +0900 Subject: [PATCH 16/17] collect move exprs during closure lowering --- compiler/rustc_ast_lowering/src/expr.rs | 302 ++++++++++++++---------- compiler/rustc_ast_lowering/src/lib.rs | 8 +- compiler/rustc_hir/src/hir.rs | 2 +- tests/ui/move-expr/plain-closure.rs | 8 + 4 files changed, 184 insertions(+), 136 deletions(-) diff --git a/compiler/rustc_ast_lowering/src/expr.rs b/compiler/rustc_ast_lowering/src/expr.rs index 49df2ddcbf3e1..148814f9c259a 100644 --- a/compiler/rustc_ast_lowering/src/expr.rs +++ b/compiler/rustc_ast_lowering/src/expr.rs @@ -31,8 +31,8 @@ use crate::{AllowReturnTypeNotation, FnDeclKind, ImplTraitPosition, TryBlockScop pub(super) struct WillCreateDefIdsVisitor; -/// A `move(...)` expression found while scanning a plain closure body. -struct MoveExprOccurrence<'a> { +/// A `move(...)` expression found while looking up generated initializers. +struct MoveExprInitializer<'a> { /// The `NodeId` of the outer `move(...)` expression. id: NodeId, /// Span of the `move` token, used for the generated binding name. @@ -41,32 +41,45 @@ struct MoveExprOccurrence<'a> { expr: &'a Expr, } -/// Collects the `move(...)` expressions that belong to one plain closure body. -/// -/// For `|| move(foo.bar).clone()`, this records the outer `move(foo.bar)` -/// occurrence and the inner expression `foo.bar`. Nested closures, generators, -/// const blocks, and items are lowered as separate bodies, so this visitor does -/// not collect `move(...)` expressions from them. -struct MoveExprCollector<'a> { - occurrences: Vec>, +/// State for `move(...)` expressions found while lowering one plain closure body. +pub(super) struct MoveExprState<'hir> { + pub(super) bindings: NodeMap<(Ident, HirId)>, + pub(super) occurrences: Vec>, } -impl<'a> MoveExprCollector<'a> { - fn collect(expr: &'a Expr) -> Vec> { - let mut this = Self { occurrences: Vec::new() }; +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. +struct MoveExprInitializerFinder<'a> { + initializers: Vec>, +} + +impl<'a> MoveExprInitializerFinder<'a> { + fn collect(expr: &'a Expr) -> Vec> { + let mut this = Self { initializers: Vec::new() }; this.visit_expr(expr); - this.occurrences + this.initializers } } -impl<'a> Visitor<'a> for MoveExprCollector<'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) => { - // For `move(foo.bar)`, first collect any nested `move(...)` - // expressions in `foo.bar`, then record this outer occurrence. self.visit_expr(inner); - self.occurrences.push(MoveExprOccurrence { + self.initializers.push(MoveExprInitializer { id: expr.id, move_kw_span: *move_kw_span, expr: inner, @@ -102,6 +115,42 @@ impl<'v> rustc_ast::visit::Visitor<'v> for WillCreateDefIdsVisitor { } impl<'hir> LoweringContext<'_, 'hir> { + fn with_move_expr_bindings( + &mut self, + state: Option>, + f: impl FnOnce(&mut Self) -> T, + ) -> (T, Option>) { + self.move_expr_bindings.push(state); + let result = f(self); + let state = self.move_expr_bindings.pop().unwrap_or_else(|| { + span_bug!(DUMMY_SP, "`move_expr_bindings` stack was empty after lowering") + }); + (result, state) + } + + fn record_move_expr( + &mut self, + id: NodeId, + inner: &Expr, + move_kw_span: Span, + explicit_capture: bool, + ) -> (Ident, HirId) { + let index = self + .move_expr_bindings + .last() + .and_then(|state| state.as_ref()) + .map_or(0, |state| state.occurrences.len()); + let ident = Ident::from_str_and_span(&format!("__move_expr_{index}"), move_kw_span); + 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"); + }; + state.bindings.insert(id, (ident, binding)); + state.occurrences.push(MoveExprOccurrence { id, ident, pat, binding, explicit_capture }); + (ident, binding) + } + 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))) } @@ -144,7 +193,7 @@ impl<'hir> LoweringContext<'_, 'hir> { ExprKind::ForLoop { pat, iter, body, label, kind } => { return self.lower_expr_for(e, pat, iter, body, *label, *kind); } - ExprKind::Closure(box closure) => return self.lower_expr_closure_expr(e, closure), + ExprKind::Closure(closure) => return self.lower_expr_closure_expr(e, closure), _ => (), } @@ -262,18 +311,23 @@ impl<'hir> LoweringContext<'_, 'hir> { }, ), ExprKind::Await(expr, await_kw_span) => self.lower_expr_await(*await_kw_span, expr), - ExprKind::Move(_, move_kw_span) => { + ExprKind::Move(inner, move_kw_span) => { if !self.tcx.features().move_expr() { return self.expr_err(*move_kw_span, self.dcx().has_errors().unwrap()); } - // `last()` selects the binding map for the closure body currently - // being lowered. The map is keyed by the AST `NodeId`, so `e.id` - // selects the synthetic local for this exact `move(...)` occurrence. - if let Some((ident, binding)) = self - .move_expr_bindings - .last() - .and_then(|bindings| bindings.get(&e.id).copied()) - { + 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) + }); hir::ExprKind::Path(hir::QPath::Resolved( None, self.arena.alloc(hir::Path { @@ -311,7 +365,14 @@ impl<'hir> LoweringContext<'_, 'hir> { e.span, desugaring_kind, hir::CoroutineSource::Block, - |this| this.with_new_scopes(e.span, |this| this.lower_block_expr(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) => { @@ -445,11 +506,10 @@ impl<'hir> LoweringContext<'_, 'hir> { pub(crate) fn lower_const_block(&mut self, c: &AnonConst) -> hir::ConstBlock { self.with_new_scopes(c.value.span, |this| { let def_id = this.local_def_id(c.id); - hir::ConstBlock { - def_id, - hir_id: this.lower_node_id(c.id), - body: this.lower_const_body(c.value.span, Some(&c.value)), - } + let (body, _) = this.with_move_expr_bindings(None, |this| { + this.lower_const_body(c.value.span, Some(&c.value)) + }); + hir::ConstBlock { def_id, hir_id: this.lower_node_id(c.id), body } }) } @@ -1153,102 +1213,69 @@ impl<'hir> LoweringContext<'_, 'hir> { fn_arg_span: Span, whole_span: Span, ) -> hir::Expr<'hir> { - // `move(...)` evaluates its inner expression when the closure is created - // and captures the result by value. For example: - // - // `|| move(foo).bar` - // - // is lowered roughly as: - // - // `let __move_expr_0 = foo; || __move_expr_0.bar` - let occurrences = MoveExprCollector::collect(body); - if occurrences.is_empty() { - // No `move(...)` expressions in this closure body; lower the closure - // normally, with no explicit captures. + let (closure_kind, move_expr_state) = self.lower_expr_closure( + attrs, + binder, + capture_clause, + closure_id, + constness, + movability, + decl, + body, + fn_decl_span, + fn_arg_span, + ); + + if move_expr_state.occurrences.is_empty() { return hir::Expr { hir_id: expr_hir_id, - kind: self.lower_expr_closure( - attrs, - binder, - capture_clause, - closure_id, - constness, - movability, - decl, - body, - fn_decl_span, - fn_arg_span, - &[], - ), + kind: closure_kind, span: self.lower_span(whole_span), }; } - let mut bindings = NodeMap::default(); - let mut lowered_occurrences = Vec::with_capacity(occurrences.len()); - for (index, occurrence) in occurrences.iter().enumerate() { - // Create one synthetic local per `move(...)` expression and remember - // which AST node should be replaced by that local while lowering the - // closure body. - let ident = - Ident::from_str_and_span(&format!("__move_expr_{index}"), occurrence.move_kw_span); - let (pat, binding) = self.pat_ident(occurrence.expr.span, ident); - bindings.insert(occurrence.id, (ident, binding)); - lowered_occurrences.push((occurrence, pat, binding)); - } - - // During body lowering, replace each `move(...)` occurrence with the - // synthetic local recorded in this closure's binding map. Nested closures - // push their own maps. - self.move_expr_bindings.push(bindings); - let mut stmts = Vec::with_capacity(lowered_occurrences.len()); - for (occurrence, pat, _) in &lowered_occurrences { + 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 init = self.lower_expr(occurrence.expr); + 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, - occurrence.expr.span, + expr.span, Some(init), - *pat, + occurrence.pat, hir::LocalSource::Normal, )); + initializer_bindings.insert(occurrence.id, (occurrence.ident, occurrence.binding)); } - let explicit_captures = self.arena.alloc_from_iter( - lowered_occurrences - .iter() - // Force the generated locals to be captured by value even if - // the lowered closure body only borrows them, as in - // `move(foo).clone()`. - .map(|(_, _, binding)| hir::ExplicitCapture { var_hir_id: *binding }), - ); - - // Lower the closure itself while `move_expr_bindings` contains this - // closure's substitutions, so each `move(...)` in the body is replaced - // with its generated local. let closure_expr = self.arena.alloc(hir::Expr { hir_id: expr_hir_id, - kind: self.lower_expr_closure( - attrs, - binder, - capture_clause, - closure_id, - constness, - movability, - decl, - body, - fn_decl_span, - fn_arg_span, - explicit_captures, - ), + kind: closure_kind, span: self.lower_span(whole_span), }); - // Restore the enclosing closure's substitution map before lowering the - // block that contains the synthetic `let`s. - self.move_expr_bindings.pop(); let stmts = self.arena.alloc_from_iter(stmts); let block = self.block_all(whole_span, stmts, Some(closure_expr)); @@ -1267,26 +1294,37 @@ impl<'hir> LoweringContext<'_, 'hir> { body: &Expr, fn_decl_span: Span, fn_arg_span: Span, - explicit_captures: &'hir [hir::ExplicitCapture], - ) -> hir::ExprKind<'hir> { + ) -> (hir::ExprKind<'hir>, MoveExprState<'hir>) { let closure_def_id = self.local_def_id(closure_id); let (binder_clause, generic_params) = self.lower_closure_binder(binder); - let (body_id, closure_kind) = self.with_new_scopes(fn_decl_span, move |this| { + let ((body_id, closure_kind), move_expr_state) = self.with_new_scopes(fn_decl_span, move |this| { let mut coroutine_kind = find_attr!(attrs, Coroutine => hir::CoroutineKind::Coroutine(Movability::Movable)); - // FIXME(contracts): Support contracts on closures? - let body_id = this.lower_fn_body(decl, None, |this| { - this.coroutine_kind = coroutine_kind; - let e = this.lower_expr_mut(body); - coroutine_kind = this.coroutine_kind; - e - }); - let coroutine_option = - this.closure_movability_for_fn(decl, fn_decl_span, coroutine_kind, movability); - (body_id, coroutine_option) + this.with_move_expr_bindings(Some(MoveExprState::default()), |this| { + // FIXME(contracts): Support contracts on closures? + let body_id = this.lower_fn_body(decl, None, |this| { + this.coroutine_kind = coroutine_kind; + let e = this.lower_expr_mut(body); + coroutine_kind = this.coroutine_kind; + e + }); + let coroutine_option = + this.closure_movability_for_fn(decl, fn_decl_span, coroutine_kind, movability); + (body_id, coroutine_option) + }) }); + let Some(move_expr_state) = move_expr_state else { + 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 }) + }), + ); let bound_generic_params = self.lower_lifetime_binder(closure_id, generic_params); // Lower outside new scope to preserve `is_in_loop_condition`. @@ -1306,7 +1344,7 @@ impl<'hir> LoweringContext<'_, 'hir> { explicit_captures, }); - hir::ExprKind::Closure(c) + (hir::ExprKind::Closure(c), move_expr_state) } fn closure_movability_for_fn( @@ -1385,14 +1423,16 @@ impl<'hir> LoweringContext<'_, 'hir> { // Transform `async |x: u8| -> X { ... }` into // `|x: u8| || -> X { ... }`. let body_id = this.lower_body(|this| { - 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_kind, - hir::CoroutineSource::Closure, - ); + 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)), + fn_decl_span, + body.span, + coroutine_kind, + hir::CoroutineSource::Closure, + ) + }); this.maybe_forward_track_caller(body.span, closure_hir_id, expr.hir_id); diff --git a/compiler/rustc_ast_lowering/src/lib.rs b/compiler/rustc_ast_lowering/src/lib.rs index 43c24c6a61268..d8a7b6e93ab0c 100644 --- a/compiler/rustc_ast_lowering/src/lib.rs +++ b/compiler/rustc_ast_lowering/src/lib.rs @@ -158,10 +158,10 @@ struct LoweringContext<'a, 'hir> { allow_async_fn_traits: Arc<[Symbol]>, delayed_lints: Vec, - /// Stack of per-closure `move(...)` substitution maps. Each map is keyed by - /// the AST `NodeId` of a `move(...)` occurrence and points to the synthetic - /// local used while lowering that closure body. - move_expr_bindings: Vec>, + /// Stack of `move(...)` collection states. A plain closure 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>>, attribute_parser: AttributeParser<'hir>, } diff --git a/compiler/rustc_hir/src/hir.rs b/compiler/rustc_hir/src/hir.rs index 0a7aecb6470a0..84836e2a25c49 100644 --- a/compiler/rustc_hir/src/hir.rs +++ b/compiler/rustc_hir/src/hir.rs @@ -1687,7 +1687,7 @@ pub struct Closure<'hir> { /// A HIR local that must be captured by value even if ordinary closure capture /// analysis would infer a weaker capture kind from its uses in the body. -#[derive(Debug, Clone, Copy, HashStable)] +#[derive(Debug, Clone, Copy, StableHash)] pub struct ExplicitCapture { pub var_hir_id: HirId, } diff --git a/tests/ui/move-expr/plain-closure.rs b/tests/ui/move-expr/plain-closure.rs index 2ec01e35569b4..dc3e3b2956a15 100644 --- a/tests/ui/move-expr/plain-closure.rs +++ b/tests/ui/move-expr/plain-closure.rs @@ -18,4 +18,12 @@ fn main() { println!("{} {}", x, y); }; c(); + + let v = "Hello, Ferris".to_string(); + let r = || { + || { + (move(move(v.clone()))).len() + } + }; + assert_eq!(r()(), v.len()); } From 24d5224eaa545d0de09efd5c78093e79b5e62e3c Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 7 May 2026 23:33:29 +0900 Subject: [PATCH 17/17] Preserve const block HIR id order --- compiler/rustc_ast_lowering/src/expr.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/rustc_ast_lowering/src/expr.rs b/compiler/rustc_ast_lowering/src/expr.rs index 148814f9c259a..9fc69470cf546 100644 --- a/compiler/rustc_ast_lowering/src/expr.rs +++ b/compiler/rustc_ast_lowering/src/expr.rs @@ -506,10 +506,11 @@ impl<'hir> LoweringContext<'_, 'hir> { pub(crate) fn lower_const_block(&mut self, c: &AnonConst) -> hir::ConstBlock { self.with_new_scopes(c.value.span, |this| { let def_id = this.local_def_id(c.id); + let hir_id = this.lower_node_id(c.id); let (body, _) = this.with_move_expr_bindings(None, |this| { this.lower_const_body(c.value.span, Some(&c.value)) }); - hir::ConstBlock { def_id, hir_id: this.lower_node_id(c.id), body } + hir::ConstBlock { def_id, hir_id, body } }) }