From 77c297e75b294164843150ce1ffe024504e82bbc Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 9 Apr 2026 11:40:18 +0900 Subject: [PATCH 01/15] 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 e1f562ca3a59b..eacacecd920ae 100644 --- a/compiler/rustc_feature/src/unstable.rs +++ b/compiler/rustc_feature/src/unstable.rs @@ -639,6 +639,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 caeb923c18f78..efcb7f9ce63f7 100644 --- a/compiler/rustc_span/src/symbol.rs +++ b/compiler/rustc_span/src/symbol.rs @@ -1320,6 +1320,7 @@ symbols! { more_qualified_paths, more_struct_aliases, movbe_target_feature, + move_expr, move_ref_pattern, move_size_limit, movrs_target_feature, From 8a18dd850bb385bd3a98e8828ff36475af81ebd5 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 9 Apr 2026 21:14:27 +0900 Subject: [PATCH 02/15] 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 a768beedba31c..b759829c0578e 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 ee4b1d1354300..2c600658d7fd1 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 4e1b46d568099..e9735f6214249 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(box Closure { binder, diff --git a/compiler/rustc_ast_passes/src/feature_gate.rs b/compiler/rustc_ast_passes/src/feature_gate.rs index 5831636a81b2c..9476c8ba6f4ff 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 d55548dd72180..2c5b20e3923b4 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 @@ -4385,6 +4401,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 1eef81025df409e5114b20512e606095fd6c98f0 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 9 Apr 2026 21:18:59 +0900 Subject: [PATCH 03/15] 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 e9735f6214249..e161c20bbef9e 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(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, @@ -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); @@ -1104,6 +1257,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) @@ -1225,7 +1379,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 6ca6bf3e1f6dc..b07b4fa1b6fea 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>, } @@ -217,6 +218,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 c44ed05d6947a..0be9dd1913ef5 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, HashStable, Encodable, Decodable)] diff --git a/compiler/rustc_hir/src/intravisit.rs b/compiler/rustc_hir/src/intravisit.rs index e93059f752223..a494841d426e6 100644 --- a/compiler/rustc_hir/src/intravisit.rs +++ b/compiler/rustc_hir/src/intravisit.rs @@ -898,6 +898,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 0a099d79d2407b43407c82027fa9df53fe6eac49 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 9 Apr 2026 21:20:20 +0900 Subject: [PATCH 04/15] 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 04b548ce0e9f0..dcf940edba56b 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 0d6aa35e1e5b85061593febbf6f15e7a9a834da9 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 9 Apr 2026 22:26:35 +0900 Subject: [PATCH 05/15] 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 a5d17b76aa5e9..cb8816fc1b359 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 5d8ff9ddea582f4ba3b414238fc3d11035abfddd Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 9 Apr 2026 23:12:40 +0900 Subject: [PATCH 06/15] 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 6397ec19466dd9da71f8ca62e14a781dfe3ed75b Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 16 Apr 2026 21:24:46 +0900 Subject: [PATCH 07/15] 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 e161c20bbef9e..b5492788d033d 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, @@ -1379,9 +1377,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 eacacecd920ae..2c62c376131e9 100644 --- a/compiler/rustc_feature/src/unstable.rs +++ b/compiler/rustc_feature/src/unstable.rs @@ -631,6 +631,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. @@ -639,8 +641,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 0be9dd1913ef5..f504a2bc85286 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, HashStable, Encodable, Decodable)] diff --git a/compiler/rustc_hir_typeck/src/upvar.rs b/compiler/rustc_hir_typeck/src/upvar.rs index dcf940edba56b..49a522c8f594d 100644 --- a/compiler/rustc_hir_typeck/src/upvar.rs +++ b/compiler/rustc_hir_typeck/src/upvar.rs @@ -219,8 +219,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 d4b0e631f72e8c2f4eaf16090e995e181df63f31 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 16 Apr 2026 21:25:35 +0900 Subject: [PATCH 08/15] 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 c18b66a3587f8646b015f0878d8827f8c2a7c3a6 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 16 Apr 2026 21:49:10 +0900 Subject: [PATCH 09/15] 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 9476c8ba6f4ff..913b35748cf06 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) @@ -505,6 +502,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 c54f2bba3e78a1f21fc9d3e468d2a7d12e8298fc Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 30 Apr 2026 15:22:46 +0900 Subject: [PATCH 10/15] 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 49a522c8f594d..963305216b4ac 100644 --- a/compiler/rustc_hir_typeck/src/upvar.rs +++ b/compiler/rustc_hir_typeck/src/upvar.rs @@ -56,6 +56,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: @@ -216,14 +217,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 0822a3973649b6014357f3e39f77034812151eb8 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 30 Apr 2026 15:58:58 +0900 Subject: [PATCH 11/15] 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 6ed47f6ff0a605d4d2b47332468a7b609de98eae Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 30 Apr 2026 15:59:38 +0900 Subject: [PATCH 12/15] 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 1b4f1f57caca3196acdcb225bd4aec4e644f39ee Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 30 Apr 2026 16:25:40 +0900 Subject: [PATCH 13/15] 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 b5492788d033d..8f7d3d7396106 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 43ec998610b51c7ffaae82a63667df216cf11130 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 30 Apr 2026 17:44:28 +0900 Subject: [PATCH 14/15] 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 8f7d3d7396106..addc8747ab934 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( @@ -1388,7 +1427,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 b07b4fa1b6fea..4cb1fcad20236 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 f504a2bc85286..a1ea027be79de 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 963305216b4ac..dce8631a2e0d5 100644 --- a/compiler/rustc_hir_typeck/src/upvar.rs +++ b/compiler/rustc_hir_typeck/src/upvar.rs @@ -209,8 +209,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 fd79d824d96581a33d937049027828f611e466f9 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 30 Apr 2026 21:38:24 +0900 Subject: [PATCH 15/15] 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 a1ea027be79de..89d167523f282 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, }