From 17517fd3d112ec3841613b0d638224336aac723a Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 9 Apr 2026 11:40:18 +0900 Subject: [PATCH 01/10] 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 859a1ad391cb9..95f87ecbcc1f6 100644 --- a/compiler/rustc_feature/src/unstable.rs +++ b/compiler/rustc_feature/src/unstable.rs @@ -602,6 +602,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 33bc5a578e8b6..94b84ad48566f 100644 --- a/compiler/rustc_span/src/symbol.rs +++ b/compiler/rustc_span/src/symbol.rs @@ -1310,6 +1310,7 @@ symbols! { more_qualified_paths, more_struct_aliases, movbe_target_feature, + move_expr, move_ref_pattern, move_size_limit, movrs_target_feature, From 9f0f8c816a1d33239dd0766eee53e6ad8ab56c9d Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 9 Apr 2026 21:14:27 +0900 Subject: [PATCH 02/10] 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 ae4989fcbc6c9..2875e24dada23 100644 --- a/compiler/rustc_ast/src/ast.rs +++ b/compiler/rustc_ast/src/ast.rs @@ -1595,6 +1595,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. @@ -1764,6 +1765,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 6aa8d5f38ad24..9696551a55c40 100644 --- a/compiler/rustc_ast/src/visit.rs +++ b/compiler/rustc_ast/src/visit.rs @@ -1023,7 +1023,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 b6bc122051cbc..bcd60e55a3821 100644 --- a/compiler/rustc_ast_lowering/src/expr.rs +++ b/compiler/rustc_ast_lowering/src/expr.rs @@ -20,8 +20,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, @@ -212,6 +212,18 @@ impl<'hir, R: ResolverAstLoweringExt<'hir>> LoweringContext<'_, 'hir, R> { }, ), 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(box Closure { binder, diff --git a/compiler/rustc_ast_passes/src/feature_gate.rs b/compiler/rustc_ast_passes/src/feature_gate.rs index 1b615b611258f..7aac2ea8e73e8 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 701152e9f9529..798eaa30ecee2 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 c18e8c631fecc..7d827dbfe8b4b 100644 --- a/compiler/rustc_parse/src/parser/expr.rs +++ b/compiler/rustc_parse/src/parser/expr.rs @@ -563,6 +563,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)) } @@ -606,6 +612,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 46f50593c4e95..e907dbe62d12b 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 9953aa0dfadde60c5c6e3b75753c3684420d644c Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 9 Apr 2026 21:18:59 +0900 Subject: [PATCH 03/10] 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 bcd60e55a3821..668ebabe29eff 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; @@ -29,6 +30,41 @@ use super::{ use crate::errors::{InvalidLegacyConstGenericArg, UseConstGenericArg, YieldInClosure}; use crate::{AllowReturnTypeNotation, FnDeclKind, ImplTraitPosition, TryBlockScope}; +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), + } + } +} + struct WillCreateDefIdsVisitor {} impl<'v> rustc_ast::visit::Visitor<'v> for WillCreateDefIdsVisitor { @@ -95,11 +131,12 @@ impl<'hir, R: ResolverAstLoweringExt<'hir>> LoweringContext<'_, 'hir, R> { 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)), @@ -219,48 +256,34 @@ impl<'hir, R: ResolverAstLoweringExt<'hir>> LoweringContext<'_, 'hir, R> { 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(box 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, @@ -395,7 +418,7 @@ impl<'hir, R: ResolverAstLoweringExt<'hir>> LoweringContext<'_, 'hir, R> { 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") } @@ -804,6 +827,7 @@ impl<'hir, R: ResolverAstLoweringExt<'hir>> LoweringContext<'_, 'hir, R> { fn_arg_span: None, kind: hir::ClosureKind::Coroutine(coroutine_kind), constness: hir::Constness::NotConst, + explicit_captures: &[], })) } @@ -1067,6 +1091,134 @@ impl<'hir, R: ResolverAstLoweringExt<'hir>> LoweringContext<'_, 'hir, R> { 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], @@ -1079,6 +1231,7 @@ impl<'hir, R: ResolverAstLoweringExt<'hir>> LoweringContext<'_, 'hir, R> { 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); @@ -1120,6 +1273,7 @@ impl<'hir, R: ResolverAstLoweringExt<'hir>> LoweringContext<'_, 'hir, R> { fn_arg_span: Some(self.lower_span(fn_arg_span)), kind: closure_kind, constness: self.lower_constness(constness), + explicit_captures, }); hir::ExprKind::Closure(c) @@ -1241,7 +1395,8 @@ impl<'hir, R: ResolverAstLoweringExt<'hir>> LoweringContext<'_, 'hir, R> { // 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 0561490344d21..cf09123b55251 100644 --- a/compiler/rustc_ast_lowering/src/lib.rs +++ b/compiler/rustc_ast_lowering/src/lib.rs @@ -148,6 +148,7 @@ struct LoweringContext<'a, 'hir, R> { allow_async_fn_traits: Arc<[Symbol]>, delayed_lints: Vec, + move_expr_bindings: Vec>, attribute_parser: AttributeParser<'hir>, } @@ -205,6 +206,7 @@ impl<'a, 'hir, R: ResolverAstLoweringExt<'hir>> LoweringContext<'a, 'hir, R> { // 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 71ef6c9a9c03c..11d76bbb2f396 100644 --- a/compiler/rustc_hir/src/hir.rs +++ b/compiler/rustc_hir/src/hir.rs @@ -1690,6 +1690,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, HashStable_Generic, Encodable, Decodable)] diff --git a/compiler/rustc_hir/src/intravisit.rs b/compiler/rustc_hir/src/intravisit.rs index 25ef56f8b0f2c..92dd9bbea6448 100644 --- a/compiler/rustc_hir/src/intravisit.rs +++ b/compiler/rustc_hir/src/intravisit.rs @@ -894,6 +894,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 82540a9327410..7e68d8f02704d 100644 --- a/compiler/rustc_hir_pretty/src/lib.rs +++ b/compiler/rustc_hir_pretty/src/lib.rs @@ -1643,6 +1643,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 0dcce733eff784c47c807eb84e1d3eef54d751cd Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 9 Apr 2026 21:20:20 +0900 Subject: [PATCH 04/10] 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 df02974d2fb2d..18689652a88b4 100644 --- a/compiler/rustc_hir_typeck/src/upvar.rs +++ b/compiler/rustc_hir_typeck/src/upvar.rs @@ -210,6 +210,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 1203e52827eb63f82c6abe4cf8806887b9971020 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 9 Apr 2026 22:26:35 +0900 Subject: [PATCH 05/10] 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 641c6684a0bd1..14c5befb5634b 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 71d9fed58d7de14d680dac539ac5ae0c8214163f Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 9 Apr 2026 23:12:40 +0900 Subject: [PATCH 06/10] 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 662534d67434e365d6ca43a81a4bbc755c048b48 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 9 Apr 2026 23:26:28 +0900 Subject: [PATCH 07/10] fix tidy errors --- compiler/rustc_feature/src/unstable.rs | 4 ++-- tests/ui/README.md | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/compiler/rustc_feature/src/unstable.rs b/compiler/rustc_feature/src/unstable.rs index 95f87ecbcc1f6..1f3a35183664c 100644 --- a/compiler/rustc_feature/src/unstable.rs +++ b/compiler/rustc_feature/src/unstable.rs @@ -594,6 +594,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", None), /// The `movrs` target feature on x86. (unstable, movrs_target_feature, "1.88.0", Some(137976)), /// Allows the `multiple_supertrait_upcastable` lint. @@ -602,8 +604,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/tests/ui/README.md b/tests/ui/README.md index a9e7f022c2b60..07047097f93ee 100644 --- a/tests/ui/README.md +++ b/tests/ui/README.md @@ -933,6 +933,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). From 2bec61a42695bec430681095d010a1165462bf2a Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 9 Apr 2026 23:32:26 +0900 Subject: [PATCH 08/10] add tracking issue number --- compiler/rustc_feature/src/unstable.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/rustc_feature/src/unstable.rs b/compiler/rustc_feature/src/unstable.rs index 1f3a35183664c..fa2a606a0fc8a 100644 --- a/compiler/rustc_feature/src/unstable.rs +++ b/compiler/rustc_feature/src/unstable.rs @@ -595,7 +595,7 @@ declare_features! ( /// 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", None), + (incomplete, move_expr, "CURRENT_RUSTC_VERSION", Some(132290)), /// The `movrs` target feature on x86. (unstable, movrs_target_feature, "1.88.0", Some(137976)), /// Allows the `multiple_supertrait_upcastable` lint. From 17e124eeb42d8a57f26622064a392f8344ced46d Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Fri, 10 Apr 2026 01:04:27 +0900 Subject: [PATCH 09/10] fix issue number --- compiler/rustc_feature/src/unstable.rs | 2 +- tests/ui/feature-gates/feature-gate-move_expr.stderr | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/rustc_feature/src/unstable.rs b/compiler/rustc_feature/src/unstable.rs index fa2a606a0fc8a..8b0900e8eb537 100644 --- a/compiler/rustc_feature/src/unstable.rs +++ b/compiler/rustc_feature/src/unstable.rs @@ -595,7 +595,7 @@ declare_features! ( /// 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(132290)), + (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. 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 0dd583145969e2ae15fdc41466fad1e2980f2903 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Fri, 10 Apr 2026 02:42:26 +0900 Subject: [PATCH 10/10] fmt rustfmt --- src/tools/rustfmt/src/expr.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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) => {