From c496571859ce0abc51abb90fe9229120d04b70e8 Mon Sep 17 00:00:00 2001 From: Valentyn Kit Date: Thu, 30 Jul 2026 13:41:39 +0300 Subject: [PATCH 01/32] Implement `Thread::os_id` --- library/std/src/thread/current.rs | 5 +- library/std/src/thread/lifecycle.rs | 4 ++ library/std/src/thread/tests.rs | 13 ++++ library/std/src/thread/thread.rs | 96 +++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 1 deletion(-) diff --git a/library/std/src/thread/current.rs b/library/std/src/thread/current.rs index 508e35cefe88f..cd2b046f91293 100644 --- a/library/std/src/thread/current.rs +++ b/library/std/src/thread/current.rs @@ -246,7 +246,9 @@ pub(crate) fn current_or_unnamed() -> Thread { (*current).clone() } } else if current == DESTROYED { - Thread::new(id::get_or_init(), None) + let thread = Thread::new(id::get_or_init(), None); + thread.set_os_id_to_current(); + thread } else { init_current(current) } @@ -292,6 +294,7 @@ fn init_current(current: *mut ()) -> Thread { // If the thread ID was initialized already, use it. let id = id::get_or_init(); let thread = Thread::new(id, None); + thread.set_os_id_to_current(); // Make sure that `crate::rt::thread_cleanup` will be run, which will // call `drop_current`. diff --git a/library/std/src/thread/lifecycle.rs b/library/std/src/thread/lifecycle.rs index d3a97bbf08fa2..241f63d37f832 100644 --- a/library/std/src/thread/lifecycle.rs +++ b/library/std/src/thread/lifecycle.rs @@ -141,6 +141,10 @@ impl ThreadInit { rtabort!("current thread handle already set during thread spawn"); } + // The handle was created by the spawning thread, so only now that we are + // running can the OS id be filled in. + self.handle.set_os_id_to_current(); + if let Some(name) = self.handle.cname() { imp::set_name(name); } diff --git a/library/std/src/thread/tests.rs b/library/std/src/thread/tests.rs index 78b6f7c35e8db..0b2b96772279b 100644 --- a/library/std/src/thread/tests.rs +++ b/library/std/src/thread/tests.rs @@ -356,6 +356,19 @@ fn test_thread_os_id_not_equal() { assert!(current_id != spawned_id); } +#[test] +fn test_thread_os_id_matches_current() { + assert_eq!(thread::current().os_id(), crate::sys::thread::current_os_id()); +} + +#[test] +fn test_thread_os_id_of_spawned_thread() { + let spawned = thread::spawn(|| thread::current().os_id()); + let handle = spawned.thread().clone(); + let seen_by_the_thread_itself = spawned.join().unwrap(); + assert_eq!(handle.os_id(), seen_by_the_thread_itself); +} + #[test] fn test_scoped_threads_drop_result_before_join() { let actually_finished = &AtomicBool::new(false); diff --git a/library/std/src/thread/thread.rs b/library/std/src/thread/thread.rs index 7c9c91c3b0c78..a791f2147dbee 100644 --- a/library/std/src/thread/thread.rs +++ b/library/std/src/thread/thread.rs @@ -6,6 +6,7 @@ use crate::fmt; use crate::pin::Pin; use crate::sync::Arc; use crate::sys::sync::Parker; +use crate::sys::thread as imp; use crate::time::Duration; // This module ensures private fields are kept private, which is necessary to enforce the safety requirements. @@ -40,6 +41,59 @@ mod thread_name_string { use thread_name_string::ThreadNameString; +// The handle of a spawned thread exists before the thread does, so the thread +// stores its own id once it starts running, hence the atomic. 0 means "not known". +// +// Of the platform calls behind `current_os_id`, only Apple's yields a `uint64_t`, +// and every Apple target has 64-bit atomics, so `usize` loses nothing on the +// second arm. +cfg_select! { + target_has_atomic = "64" => { + use crate::sync::atomic::{Atomic, AtomicU64, Ordering::Relaxed}; + + struct OsId(Atomic); + + impl OsId { + const fn unknown() -> Self { + Self(AtomicU64::new(0)) + } + + fn get(&self) -> Option { + match self.0.load(Relaxed) { + 0 => None, + id => Some(id), + } + } + + fn set(&self, id: u64) { + self.0.store(id, Relaxed); + } + } + } + _ => { + use crate::sync::atomic::{Atomic, AtomicUsize, Ordering::Relaxed}; + + struct OsId(Atomic); + + impl OsId { + const fn unknown() -> Self { + Self(AtomicUsize::new(0)) + } + + fn get(&self) -> Option { + match self.0.load(Relaxed) { + 0 => None, + id => Some(id as u64), + } + } + + fn set(&self, id: u64) { + self.0.store(id as usize, Relaxed); + } + } + } +} + /// The internal representation of a `Thread` handle /// /// We explicitly set the alignment for our guarantee in Thread::into_raw. This @@ -49,6 +103,7 @@ use thread_name_string::ThreadNameString; struct Inner { name: Option, id: ThreadId, + os_id: OsId, parker: Parker, } @@ -103,6 +158,7 @@ impl Thread { let ptr = Arc::get_mut_unchecked(&mut arc).as_mut_ptr(); (&raw mut (*ptr).name).write(name); (&raw mut (*ptr).id).write(id); + (&raw mut (*ptr).os_id).write(OsId::unknown()); Parker::new_in_place(&raw mut (*ptr).parker); Pin::new_unchecked(arc.assume_init()) }; @@ -110,6 +166,17 @@ impl Thread { Thread { inner } } + /// Records the OS id of the calling thread in this handle. + /// + /// May only be called from the thread to which this handle belongs. A + /// spawned thread does this itself once it starts running, since its handle + /// already exists by then. + pub(crate) fn set_os_id_to_current(&self) { + if let Some(os_id) = imp::current_os_id() { + self.inner.os_id.set(os_id); + } + } + /// Like the public [`park`], but callable on any handle. This is used to /// allow parking in TLS destructors. /// @@ -204,6 +271,35 @@ impl Thread { self.inner.id } + /// Gets the id the operating system gave this thread, if it has one that can + /// be read. + /// + /// This is the id `ps`, `top`, a debugger or a crash log shows, unlike + /// [`ThreadId`], which is internal to Rust and unrelated to it. `None` means + /// the platform has no such id or offers no way to read it, or that the + /// thread has not started running yet. + /// + /// The operating system may hand the same id to a later thread once this one + /// exits, so it does not name a thread uniquely over the life of the + /// process. It may also no longer refer to this thread at all, since any + /// thread but the current one can exit at any point. For anything other than + /// the current thread, logging is the only safe use. + /// + /// # Examples + /// + /// ``` + /// #![feature(thread_os_id)] + /// use std::thread; + /// + /// let spawned = thread::spawn(|| thread::current().os_id()); + /// println!("spawned thread ran as {:?}", spawned.join().unwrap()); + /// ``` + #[unstable(feature = "thread_os_id", issue = "160215")] + #[must_use] + pub fn os_id(&self) -> Option { + self.inner.os_id.get() + } + /// Gets the thread's name. /// /// For more information about named threads, see From 84d2f10e28ff07f057497664bdb093ddde5bbc4d Mon Sep 17 00:00:00 2001 From: Valentyn Kit Date: Thu, 30 Jul 2026 17:54:25 +0300 Subject: [PATCH 02/32] Store the OS thread id in a `OnceLock` --- library/std/src/thread/thread.rs | 71 ++++---------------------------- 1 file changed, 9 insertions(+), 62 deletions(-) diff --git a/library/std/src/thread/thread.rs b/library/std/src/thread/thread.rs index a791f2147dbee..2509c87a96182 100644 --- a/library/std/src/thread/thread.rs +++ b/library/std/src/thread/thread.rs @@ -4,7 +4,7 @@ use crate::alloc::System; use crate::ffi::CStr; use crate::fmt; use crate::pin::Pin; -use crate::sync::Arc; +use crate::sync::{Arc, OnceLock}; use crate::sys::sync::Parker; use crate::sys::thread as imp; use crate::time::Duration; @@ -41,59 +41,6 @@ mod thread_name_string { use thread_name_string::ThreadNameString; -// The handle of a spawned thread exists before the thread does, so the thread -// stores its own id once it starts running, hence the atomic. 0 means "not known". -// -// Of the platform calls behind `current_os_id`, only Apple's yields a `uint64_t`, -// and every Apple target has 64-bit atomics, so `usize` loses nothing on the -// second arm. -cfg_select! { - target_has_atomic = "64" => { - use crate::sync::atomic::{Atomic, AtomicU64, Ordering::Relaxed}; - - struct OsId(Atomic); - - impl OsId { - const fn unknown() -> Self { - Self(AtomicU64::new(0)) - } - - fn get(&self) -> Option { - match self.0.load(Relaxed) { - 0 => None, - id => Some(id), - } - } - - fn set(&self, id: u64) { - self.0.store(id, Relaxed); - } - } - } - _ => { - use crate::sync::atomic::{Atomic, AtomicUsize, Ordering::Relaxed}; - - struct OsId(Atomic); - - impl OsId { - const fn unknown() -> Self { - Self(AtomicUsize::new(0)) - } - - fn get(&self) -> Option { - match self.0.load(Relaxed) { - 0 => None, - id => Some(id as u64), - } - } - - fn set(&self, id: u64) { - self.0.store(id as usize, Relaxed); - } - } - } -} - /// The internal representation of a `Thread` handle /// /// We explicitly set the alignment for our guarantee in Thread::into_raw. This @@ -103,7 +50,7 @@ cfg_select! { struct Inner { name: Option, id: ThreadId, - os_id: OsId, + os_id: OnceLock, parker: Parker, } @@ -158,7 +105,7 @@ impl Thread { let ptr = Arc::get_mut_unchecked(&mut arc).as_mut_ptr(); (&raw mut (*ptr).name).write(name); (&raw mut (*ptr).id).write(id); - (&raw mut (*ptr).os_id).write(OsId::unknown()); + (&raw mut (*ptr).os_id).write(OnceLock::new()); Parker::new_in_place(&raw mut (*ptr).parker); Pin::new_unchecked(arc.assume_init()) }; @@ -173,7 +120,7 @@ impl Thread { /// already exists by then. pub(crate) fn set_os_id_to_current(&self) { if let Some(os_id) = imp::current_os_id() { - self.inner.os_id.set(os_id); + let _ = self.inner.os_id.set(os_id); } } @@ -274,10 +221,10 @@ impl Thread { /// Gets the id the operating system gave this thread, if it has one that can /// be read. /// - /// This is the id `ps`, `top`, a debugger or a crash log shows, unlike - /// [`ThreadId`], which is internal to Rust and unrelated to it. `None` means - /// the platform has no such id or offers no way to read it, or that the - /// thread has not started running yet. + /// This is the id that shows up in tools like `ps` and `top`, debuggers and + /// crash logs, unlike [`ThreadId`], which has no guaranteed relationship to + /// it. `None` means the platform has no such id or offers no way to read it, + /// or that the thread has not started running yet. /// /// The operating system may hand the same id to a later thread once this one /// exits, so it does not name a thread uniquely over the life of the @@ -297,7 +244,7 @@ impl Thread { #[unstable(feature = "thread_os_id", issue = "160215")] #[must_use] pub fn os_id(&self) -> Option { - self.inner.os_id.get() + self.inner.os_id.get().copied() } /// Gets the thread's name. From 3a3ff338589edd5392aca0b033150052a2286afd Mon Sep 17 00:00:00 2001 From: Valentyn Kit Date: Sun, 2 Aug 2026 13:55:32 +0300 Subject: [PATCH 03/32] Add `Thread::new_current` for current-thread handles --- library/std/src/thread/current.rs | 8 +++----- library/std/src/thread/thread.rs | 9 +++++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/library/std/src/thread/current.rs b/library/std/src/thread/current.rs index cd2b046f91293..a452e5d4ef89a 100644 --- a/library/std/src/thread/current.rs +++ b/library/std/src/thread/current.rs @@ -246,9 +246,8 @@ pub(crate) fn current_or_unnamed() -> Thread { (*current).clone() } } else if current == DESTROYED { - let thread = Thread::new(id::get_or_init(), None); - thread.set_os_id_to_current(); - thread + let id = id::get_or_init(); + Thread::new_current(id, None) } else { init_current(current) } @@ -293,8 +292,7 @@ fn init_current(current: *mut ()) -> Thread { CURRENT.set(BUSY); // If the thread ID was initialized already, use it. let id = id::get_or_init(); - let thread = Thread::new(id, None); - thread.set_os_id_to_current(); + let thread = Thread::new_current(id, None); // Make sure that `crate::rt::thread_cleanup` will be run, which will // call `drop_current`. diff --git a/library/std/src/thread/thread.rs b/library/std/src/thread/thread.rs index 2509c87a96182..0ef7e8f458116 100644 --- a/library/std/src/thread/thread.rs +++ b/library/std/src/thread/thread.rs @@ -113,6 +113,15 @@ impl Thread { Thread { inner } } + /// Creates a handle for the calling thread, recording its OS id. + /// + /// `id` must be the `ThreadId` of the calling thread. + pub(crate) fn new_current(id: ThreadId, name: Option) -> Thread { + let thread = Thread::new(id, name); + thread.set_os_id_to_current(); + thread + } + /// Records the OS id of the calling thread in this handle. /// /// May only be called from the thread to which this handle belongs. A From 5c8cb4ee6de75bdade0c9f49670641f278e848d9 Mon Sep 17 00:00:00 2001 From: Valentyn Kit Date: Sun, 2 Aug 2026 13:59:03 +0300 Subject: [PATCH 04/32] Reword the `os_id` docs after review --- library/std/src/thread/thread.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/library/std/src/thread/thread.rs b/library/std/src/thread/thread.rs index 0ef7e8f458116..e195c8567a420 100644 --- a/library/std/src/thread/thread.rs +++ b/library/std/src/thread/thread.rs @@ -232,14 +232,15 @@ impl Thread { /// /// This is the id that shows up in tools like `ps` and `top`, debuggers and /// crash logs, unlike [`ThreadId`], which has no guaranteed relationship to - /// it. `None` means the platform has no such id or offers no way to read it, - /// or that the thread has not started running yet. + /// it. `None` means the platform has no such id, the thread has not started + /// running yet, or the id could not be read. /// - /// The operating system may hand the same id to a later thread once this one - /// exits, so it does not name a thread uniquely over the life of the - /// process. It may also no longer refer to this thread at all, since any - /// thread but the current one can exit at any point. For anything other than - /// the current thread, logging is the only safe use. + /// Ids are unique among threads running at the same moment, but the + /// operating system may reuse the id of a thread that has exited, and a + /// `Thread` handle can outlive the thread it refers to. As long as you know + /// the thread is running, the id still refers to that thread; for the + /// current thread you always know. When you do not, use the id only where a + /// repeated id is harmless, such as logging. /// /// # Examples /// @@ -247,8 +248,10 @@ impl Thread { /// #![feature(thread_os_id)] /// use std::thread; /// - /// let spawned = thread::spawn(|| thread::current().os_id()); - /// println!("spawned thread ran as {:?}", spawned.join().unwrap()); + /// let spawned = thread::spawn(|| thread::current().os_id()).join().unwrap(); + /// if spawned.is_some() { + /// assert_ne!(spawned, thread::current().os_id()); + /// } /// ``` #[unstable(feature = "thread_os_id", issue = "160215")] #[must_use] From 42cdf8cd2322789995768c4b47b87e900778c8e0 Mon Sep 17 00:00:00 2001 From: Valentyn Kit Date: Sun, 2 Aug 2026 14:42:46 +0300 Subject: [PATCH 05/32] Rename the spawned id binding in the `os_id` test --- library/std/src/thread/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/std/src/thread/tests.rs b/library/std/src/thread/tests.rs index 0b2b96772279b..d7f4a47cb4a47 100644 --- a/library/std/src/thread/tests.rs +++ b/library/std/src/thread/tests.rs @@ -365,8 +365,8 @@ fn test_thread_os_id_matches_current() { fn test_thread_os_id_of_spawned_thread() { let spawned = thread::spawn(|| thread::current().os_id()); let handle = spawned.thread().clone(); - let seen_by_the_thread_itself = spawned.join().unwrap(); - assert_eq!(handle.os_id(), seen_by_the_thread_itself); + let spawned_id = spawned.join().unwrap(); + assert_eq!(handle.os_id(), spawned_id); } #[test] From 71be0e93f7e21b122a9752851d272e7f5837b343 Mon Sep 17 00:00:00 2001 From: Valentyn Kit Date: Mon, 10 Aug 2026 14:43:26 +0300 Subject: [PATCH 06/32] Address review feedback on `Thread::os_id` Drop the unused name parameter from `Thread::new_current`, abort if the OS id is set twice, and stop promising uniqueness of os_id in the docs. --- library/std/src/thread/current.rs | 4 ++-- library/std/src/thread/thread.rs | 23 ++++++++++++++--------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/library/std/src/thread/current.rs b/library/std/src/thread/current.rs index a452e5d4ef89a..3512f04868303 100644 --- a/library/std/src/thread/current.rs +++ b/library/std/src/thread/current.rs @@ -247,7 +247,7 @@ pub(crate) fn current_or_unnamed() -> Thread { } } else if current == DESTROYED { let id = id::get_or_init(); - Thread::new_current(id, None) + Thread::new_current(id) } else { init_current(current) } @@ -292,7 +292,7 @@ fn init_current(current: *mut ()) -> Thread { CURRENT.set(BUSY); // If the thread ID was initialized already, use it. let id = id::get_or_init(); - let thread = Thread::new_current(id, None); + let thread = Thread::new_current(id); // Make sure that `crate::rt::thread_cleanup` will be run, which will // call `drop_current`. diff --git a/library/std/src/thread/thread.rs b/library/std/src/thread/thread.rs index e195c8567a420..ff6affae7f7da 100644 --- a/library/std/src/thread/thread.rs +++ b/library/std/src/thread/thread.rs @@ -116,8 +116,11 @@ impl Thread { /// Creates a handle for the calling thread, recording its OS id. /// /// `id` must be the `ThreadId` of the calling thread. - pub(crate) fn new_current(id: ThreadId, name: Option) -> Thread { - let thread = Thread::new(id, name); + /// + /// Takes no name because passing one into `Thread::new` allocates with the + /// global allocator, which `thread::current` is documented never to use. + pub(crate) fn new_current(id: ThreadId) -> Thread { + let thread = Thread::new(id, None); thread.set_os_id_to_current(); thread } @@ -127,9 +130,14 @@ impl Thread { /// May only be called from the thread to which this handle belongs. A /// spawned thread does this itself once it starts running, since its handle /// already exists by then. + /// + /// `imp::current_os_id` must not allocate with the global allocator or call + /// `thread::current`. pub(crate) fn set_os_id_to_current(&self) { if let Some(os_id) = imp::current_os_id() { - let _ = self.inner.os_id.set(os_id); + if self.inner.os_id.set(os_id).is_err() { + rtabort!("thread OS id already set"); + } } } @@ -235,12 +243,9 @@ impl Thread { /// it. `None` means the platform has no such id, the thread has not started /// running yet, or the id could not be read. /// - /// Ids are unique among threads running at the same moment, but the - /// operating system may reuse the id of a thread that has exited, and a - /// `Thread` handle can outlive the thread it refers to. As long as you know - /// the thread is running, the id still refers to that thread; for the - /// current thread you always know. When you do not, use the id only where a - /// repeated id is harmless, such as logging. + /// The operating system may reuse the id of a thread that has exited, and a + /// `Thread` handle can outlive the thread it refers to. Use the id only + /// where a reused id is harmless, such as logging. /// /// # Examples /// From 1af640ea41365e55ac3d5460d5ba3e3bf4095621 Mon Sep 17 00:00:00 2001 From: Valentyn Kit Date: Mon, 7 Sep 2026 15:39:51 +0300 Subject: [PATCH 07/32] docs: address docs refinements diff --git a/library/std/src/thread/thread.rs b/library/std/src/thread/thread.rs index ff6affae7f7..d70c244c65d 100644 --- a/library/std/src/thread/thread.rs +++ b/library/std/src/thread/thread.rs @@ -125,7 +125,8 @@ pub(crate) fn new_current(id: ThreadId) -> Thread { thread } - /// Records the OS id of the calling thread in this handle. + /// Records the calling thread's OS id, as reported by + /// `imp::current_os_id`, in this handle. /// /// May only be called from the thread to which this handle belongs. A /// spawned thread does this itself once it starts running, since its handle @@ -240,12 +241,17 @@ pub fn id(&self) -> ThreadId { /// /// This is the id that shows up in tools like `ps` and `top`, debuggers and /// crash logs, unlike [`ThreadId`], which has no guaranteed relationship to - /// it. `None` means the platform has no such id, the thread has not started - /// running yet, or the id could not be read. + /// it. On a platform with no OS-visible thread id, such as SGX, the value + /// may be some other per-thread value (there, the thread's address), which + /// such tools will not recognize. `None` means no id could be recorded: the + /// thread has not started running yet, or the platform has no way to read + /// one. /// /// The operating system may reuse the id of a thread that has exited, and a - /// `Thread` handle can outlive the thread it refers to. Use the id only - /// where a reused id is harmless, such as logging. + /// `Thread` handle can outlive the thread it refers to. After a `fork`, the + /// id recorded in the child process still refers to the parent's thread; it + /// is not re-read. Use the id only where a reused or stale id is harmless, + /// such as logging. /// /// # Examples /// --- library/std/src/thread/thread.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/library/std/src/thread/thread.rs b/library/std/src/thread/thread.rs index ff6affae7f7da..d70c244c65d90 100644 --- a/library/std/src/thread/thread.rs +++ b/library/std/src/thread/thread.rs @@ -125,7 +125,8 @@ impl Thread { thread } - /// Records the OS id of the calling thread in this handle. + /// Records the calling thread's OS id, as reported by + /// `imp::current_os_id`, in this handle. /// /// May only be called from the thread to which this handle belongs. A /// spawned thread does this itself once it starts running, since its handle @@ -240,12 +241,17 @@ impl Thread { /// /// This is the id that shows up in tools like `ps` and `top`, debuggers and /// crash logs, unlike [`ThreadId`], which has no guaranteed relationship to - /// it. `None` means the platform has no such id, the thread has not started - /// running yet, or the id could not be read. + /// it. On a platform with no OS-visible thread id, such as SGX, the value + /// may be some other per-thread value (there, the thread's address), which + /// such tools will not recognize. `None` means no id could be recorded: the + /// thread has not started running yet, or the platform has no way to read + /// one. /// /// The operating system may reuse the id of a thread that has exited, and a - /// `Thread` handle can outlive the thread it refers to. Use the id only - /// where a reused id is harmless, such as logging. + /// `Thread` handle can outlive the thread it refers to. After a `fork`, the + /// id recorded in the child process still refers to the parent's thread; it + /// is not re-read. Use the id only where a reused or stale id is harmless, + /// such as logging. /// /// # Examples /// From e018c2400797f6b118a6f3a55993fd16bc29a035 Mon Sep 17 00:00:00 2001 From: Yukang Date: Mon, 7 Sep 2026 14:07:20 +0800 Subject: [PATCH 08/32] Add regression test for redundant shared reference suggestions --- ...undant-shared-reference-issue-133685.fixed | 35 +++++ ...redundant-shared-reference-issue-133685.rs | 35 +++++ ...ndant-shared-reference-issue-133685.stderr | 120 ++++++++++++++++++ 3 files changed, 190 insertions(+) create mode 100644 tests/ui/suggestions/redundant-shared-reference-issue-133685.fixed create mode 100644 tests/ui/suggestions/redundant-shared-reference-issue-133685.rs create mode 100644 tests/ui/suggestions/redundant-shared-reference-issue-133685.stderr diff --git a/tests/ui/suggestions/redundant-shared-reference-issue-133685.fixed b/tests/ui/suggestions/redundant-shared-reference-issue-133685.fixed new file mode 100644 index 0000000000000..64c60270775ab --- /dev/null +++ b/tests/ui/suggestions/redundant-shared-reference-issue-133685.fixed @@ -0,0 +1,35 @@ +//! Regression test for https://github.com/rust-lang/rust/issues/133685. +//! Prefer removing an extra shared borrow over reborrowing an existing shared reference. + +//@ run-rustfix + +#![allow(unused_parens)] + +fn consume<'a>(_: impl IntoIterator) {} + +fn main() { + let a: Vec = Vec::new(); + let ref_a = &a; + let mut b: Vec = Vec::new(); + b.extend(&*ref_a); + //~^ ERROR is not an iterator + + consume(&*ref_a); + //~^ ERROR is not an iterator + consume((&*ref_a)); + //~^ ERROR is not an iterator + + let slice = &a[..]; + consume(&*slice); + //~^ ERROR is not an iterator + + // These still need a dereference: neither operand has the required shared-reference type. + let boxed = Box::new(a.clone()); + consume(&*boxed); + //~^ ERROR is not an iterator + + let mut values = a.clone(); + let mut_ref = &mut values; + consume(&*mut_ref); + //~^ ERROR is not an iterator +} diff --git a/tests/ui/suggestions/redundant-shared-reference-issue-133685.rs b/tests/ui/suggestions/redundant-shared-reference-issue-133685.rs new file mode 100644 index 0000000000000..4eed1ed4b6169 --- /dev/null +++ b/tests/ui/suggestions/redundant-shared-reference-issue-133685.rs @@ -0,0 +1,35 @@ +//! Regression test for https://github.com/rust-lang/rust/issues/133685. +//! Prefer removing an extra shared borrow over reborrowing an existing shared reference. + +//@ run-rustfix + +#![allow(unused_parens)] + +fn consume<'a>(_: impl IntoIterator) {} + +fn main() { + let a: Vec = Vec::new(); + let ref_a = &a; + let mut b: Vec = Vec::new(); + b.extend(&ref_a); + //~^ ERROR is not an iterator + + consume(&ref_a); + //~^ ERROR is not an iterator + consume((&ref_a)); + //~^ ERROR is not an iterator + + let slice = &a[..]; + consume(&slice); + //~^ ERROR is not an iterator + + // These still need a dereference: neither operand has the required shared-reference type. + let boxed = Box::new(a.clone()); + consume(&boxed); + //~^ ERROR is not an iterator + + let mut values = a.clone(); + let mut_ref = &mut values; + consume(&mut_ref); + //~^ ERROR is not an iterator +} diff --git a/tests/ui/suggestions/redundant-shared-reference-issue-133685.stderr b/tests/ui/suggestions/redundant-shared-reference-issue-133685.stderr new file mode 100644 index 0000000000000..a26f7eb2265c6 --- /dev/null +++ b/tests/ui/suggestions/redundant-shared-reference-issue-133685.stderr @@ -0,0 +1,120 @@ +error[E0277]: `&&Vec` is not an iterator + --> $DIR/redundant-shared-reference-issue-133685.rs:14:14 + | +LL | b.extend(&ref_a); + | ------ ^^^^^^ `&&Vec` is not an iterator + | | + | required by a bound introduced by this call + | + = help: the trait `Iterator` is not implemented for `&&Vec` + = note: required for `&&Vec` to implement `IntoIterator` +note: required by a bound in `extend` + --> $SRC_DIR/core/src/iter/traits/collect.rs:LL:COL +help: consider dereferencing here + | +LL | b.extend(&*ref_a); + | + + +error[E0277]: `&&Vec` is not an iterator + --> $DIR/redundant-shared-reference-issue-133685.rs:17:13 + | +LL | consume(&ref_a); + | ------- ^^^^^^ `&&Vec` is not an iterator + | | + | required by a bound introduced by this call + | + = help: the trait `Iterator` is not implemented for `&&Vec` + = note: required for `&&Vec` to implement `IntoIterator` +note: required by a bound in `consume` + --> $DIR/redundant-shared-reference-issue-133685.rs:8:24 + | +LL | fn consume<'a>(_: impl IntoIterator) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `consume` +help: consider dereferencing here + | +LL | consume(&*ref_a); + | + + +error[E0277]: `&&Vec` is not an iterator + --> $DIR/redundant-shared-reference-issue-133685.rs:19:13 + | +LL | consume((&ref_a)); + | ------- ^^^^^^^^ `&&Vec` is not an iterator + | | + | required by a bound introduced by this call + | + = help: the trait `Iterator` is not implemented for `&&Vec` + = note: required for `&&Vec` to implement `IntoIterator` +note: required by a bound in `consume` + --> $DIR/redundant-shared-reference-issue-133685.rs:8:24 + | +LL | fn consume<'a>(_: impl IntoIterator) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `consume` +help: consider dereferencing here + | +LL | consume((&*ref_a)); + | + + +error[E0277]: `&&[i32]` is not an iterator + --> $DIR/redundant-shared-reference-issue-133685.rs:23:13 + | +LL | consume(&slice); + | ------- ^^^^^^ `&&[i32]` is not an iterator + | | + | required by a bound introduced by this call + | + = help: the trait `Iterator` is not implemented for `&&[i32]` + = note: required for `&&[i32]` to implement `IntoIterator` +note: required by a bound in `consume` + --> $DIR/redundant-shared-reference-issue-133685.rs:8:24 + | +LL | fn consume<'a>(_: impl IntoIterator) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `consume` +help: consider dereferencing here + | +LL | consume(&*slice); + | + + +error[E0277]: `&Box>` is not an iterator + --> $DIR/redundant-shared-reference-issue-133685.rs:28:13 + | +LL | consume(&boxed); + | ------- ^^^^^^ `&Box>` is not an iterator + | | + | required by a bound introduced by this call + | + = help: the trait `Iterator` is not implemented for `&Box>` + = note: required for `&Box>` to implement `IntoIterator` +note: required by a bound in `consume` + --> $DIR/redundant-shared-reference-issue-133685.rs:8:24 + | +LL | fn consume<'a>(_: impl IntoIterator) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `consume` +help: consider dereferencing here + | +LL | consume(&*boxed); + | + + +error[E0277]: `&&mut Vec` is not an iterator + --> $DIR/redundant-shared-reference-issue-133685.rs:33:13 + | +LL | consume(&mut_ref); + | ------- ^^^^^^^^ `&&mut Vec` is not an iterator + | | + | required by a bound introduced by this call + | + = help: the trait `Iterator` is not implemented for `&&mut Vec` + = note: required for `&&mut Vec` to implement `IntoIterator` +note: required by a bound in `consume` + --> $DIR/redundant-shared-reference-issue-133685.rs:8:24 + | +LL | fn consume<'a>(_: impl IntoIterator) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `consume` +help: consider dereferencing here + | +LL | consume(&*mut_ref); + | + + +error: aborting due to 6 previous errors + +For more information about this error, try `rustc --explain E0277`. From fb197434d205eba41ec97dd5f864f2ce97e744ed Mon Sep 17 00:00:00 2001 From: Yukang Date: Mon, 7 Sep 2026 14:17:15 +0800 Subject: [PATCH 09/32] Prefer removing a redundant shared reference over reborrowing --- .../traits/fulfillment_errors.rs | 14 +++-- .../src/error_reporting/traits/suggestions.rs | 11 ++++ ...undant-shared-reference-issue-133685.fixed | 19 ++++-- ...redundant-shared-reference-issue-133685.rs | 11 ++++ ...ndant-shared-reference-issue-133685.stderr | 61 +++++++++++++------ 5 files changed, 87 insertions(+), 29 deletions(-) diff --git a/compiler/rustc_trait_selection/src/error_reporting/traits/fulfillment_errors.rs b/compiler/rustc_trait_selection/src/error_reporting/traits/fulfillment_errors.rs index 7788a1bb62a09..d8a22e745bcc2 100644 --- a/compiler/rustc_trait_selection/src/error_reporting/traits/fulfillment_errors.rs +++ b/compiler/rustc_trait_selection/src/error_reporting/traits/fulfillment_errors.rs @@ -478,7 +478,13 @@ impl<'a, 'tcx> TypeErrCtxt<'a, 'tcx> { leaf_trait_predicate, ); suggested |= - self.suggest_dereferences(&obligation, &mut err, leaf_trait_predicate); + self.suggest_dereferences(&obligation, &mut err, leaf_trait_predicate) + || self.suggest_remove_reference( + &obligation, + &mut err, + leaf_trait_predicate, + ); + suggested |= self.suggest_fn_call(&obligation, &mut err, leaf_trait_predicate); suggested |= self.suggest_cast_to_fn_pointer( @@ -488,11 +494,7 @@ impl<'a, 'tcx> TypeErrCtxt<'a, 'tcx> { main_trait_predicate, span, ); - suggested |= self.suggest_remove_reference( - &obligation, - &mut err, - leaf_trait_predicate, - ); + suggested |= self.suggest_semicolon_removal( &obligation, &mut err, diff --git a/compiler/rustc_trait_selection/src/error_reporting/traits/suggestions.rs b/compiler/rustc_trait_selection/src/error_reporting/traits/suggestions.rs index b8e4521451a25..2fe04a3c78384 100644 --- a/compiler/rustc_trait_selection/src/error_reporting/traits/suggestions.rs +++ b/compiler/rustc_trait_selection/src/error_reporting/traits/suggestions.rs @@ -780,6 +780,17 @@ impl<'a, 'tcx> TypeErrCtxt<'a, 'tcx> { if span.in_external_macro(self.tcx.sess.source_map()) { return false; } + + // For a shared reference, prefer removing the outer `&` over suggesting + // `&*reference`. Keep the reborrow for `&mut T` and smart pointers. + if is_under_ref.is_some() + && steps == 1 + && matches!(base_ty.kind(), ty::Ref(_, _, hir::Mutability::Not)) + && !expr.span.from_expansion() + && self.suggest_remove_reference(obligation, err, real_trait_pred) + { + return true; + } let derefs = "*".repeat(steps); let msg = "consider dereferencing here"; diff --git a/tests/ui/suggestions/redundant-shared-reference-issue-133685.fixed b/tests/ui/suggestions/redundant-shared-reference-issue-133685.fixed index 64c60270775ab..2c7d21d7788cc 100644 --- a/tests/ui/suggestions/redundant-shared-reference-issue-133685.fixed +++ b/tests/ui/suggestions/redundant-shared-reference-issue-133685.fixed @@ -7,20 +7,25 @@ fn consume<'a>(_: impl IntoIterator) {} +trait Value {} +struct Source; +impl Value for &Source {} +fn consume_value(_: impl Value) {} + fn main() { let a: Vec = Vec::new(); let ref_a = &a; let mut b: Vec = Vec::new(); - b.extend(&*ref_a); + b.extend(ref_a); //~^ ERROR is not an iterator - consume(&*ref_a); + consume(ref_a); //~^ ERROR is not an iterator - consume((&*ref_a)); + consume((ref_a)); //~^ ERROR is not an iterator let slice = &a[..]; - consume(&*slice); + consume(slice); //~^ ERROR is not an iterator // These still need a dereference: neither operand has the required shared-reference type. @@ -32,4 +37,10 @@ fn main() { let mut_ref = &mut values; consume(&*mut_ref); //~^ ERROR is not an iterator + + // Also cover a direct trait bound without the Iterator-to-IntoIterator blanket impl. + let source = Source; + let shared = &source; + consume_value(shared); + //~^ ERROR the trait bound } diff --git a/tests/ui/suggestions/redundant-shared-reference-issue-133685.rs b/tests/ui/suggestions/redundant-shared-reference-issue-133685.rs index 4eed1ed4b6169..38f699a8dfe85 100644 --- a/tests/ui/suggestions/redundant-shared-reference-issue-133685.rs +++ b/tests/ui/suggestions/redundant-shared-reference-issue-133685.rs @@ -7,6 +7,11 @@ fn consume<'a>(_: impl IntoIterator) {} +trait Value {} +struct Source; +impl Value for &Source {} +fn consume_value(_: impl Value) {} + fn main() { let a: Vec = Vec::new(); let ref_a = &a; @@ -32,4 +37,10 @@ fn main() { let mut_ref = &mut values; consume(&mut_ref); //~^ ERROR is not an iterator + + // Also cover a direct trait bound without the Iterator-to-IntoIterator blanket impl. + let source = Source; + let shared = &source; + consume_value(&shared); + //~^ ERROR the trait bound } diff --git a/tests/ui/suggestions/redundant-shared-reference-issue-133685.stderr b/tests/ui/suggestions/redundant-shared-reference-issue-133685.stderr index a26f7eb2265c6..e1115c2c91b3d 100644 --- a/tests/ui/suggestions/redundant-shared-reference-issue-133685.stderr +++ b/tests/ui/suggestions/redundant-shared-reference-issue-133685.stderr @@ -1,5 +1,5 @@ error[E0277]: `&&Vec` is not an iterator - --> $DIR/redundant-shared-reference-issue-133685.rs:14:14 + --> $DIR/redundant-shared-reference-issue-133685.rs:19:14 | LL | b.extend(&ref_a); | ------ ^^^^^^ `&&Vec` is not an iterator @@ -10,13 +10,14 @@ LL | b.extend(&ref_a); = note: required for `&&Vec` to implement `IntoIterator` note: required by a bound in `extend` --> $SRC_DIR/core/src/iter/traits/collect.rs:LL:COL -help: consider dereferencing here +help: consider removing the leading `&`-reference + | +LL - b.extend(&ref_a); +LL + b.extend(ref_a); | -LL | b.extend(&*ref_a); - | + error[E0277]: `&&Vec` is not an iterator - --> $DIR/redundant-shared-reference-issue-133685.rs:17:13 + --> $DIR/redundant-shared-reference-issue-133685.rs:22:13 | LL | consume(&ref_a); | ------- ^^^^^^ `&&Vec` is not an iterator @@ -30,13 +31,14 @@ note: required by a bound in `consume` | LL | fn consume<'a>(_: impl IntoIterator) {} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `consume` -help: consider dereferencing here +help: consider removing the leading `&`-reference + | +LL - consume(&ref_a); +LL + consume(ref_a); | -LL | consume(&*ref_a); - | + error[E0277]: `&&Vec` is not an iterator - --> $DIR/redundant-shared-reference-issue-133685.rs:19:13 + --> $DIR/redundant-shared-reference-issue-133685.rs:24:13 | LL | consume((&ref_a)); | ------- ^^^^^^^^ `&&Vec` is not an iterator @@ -50,13 +52,14 @@ note: required by a bound in `consume` | LL | fn consume<'a>(_: impl IntoIterator) {} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `consume` -help: consider dereferencing here +help: consider removing the leading `&`-reference + | +LL - consume((&ref_a)); +LL + consume((ref_a)); | -LL | consume((&*ref_a)); - | + error[E0277]: `&&[i32]` is not an iterator - --> $DIR/redundant-shared-reference-issue-133685.rs:23:13 + --> $DIR/redundant-shared-reference-issue-133685.rs:28:13 | LL | consume(&slice); | ------- ^^^^^^ `&&[i32]` is not an iterator @@ -70,13 +73,14 @@ note: required by a bound in `consume` | LL | fn consume<'a>(_: impl IntoIterator) {} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `consume` -help: consider dereferencing here +help: consider removing the leading `&`-reference + | +LL - consume(&slice); +LL + consume(slice); | -LL | consume(&*slice); - | + error[E0277]: `&Box>` is not an iterator - --> $DIR/redundant-shared-reference-issue-133685.rs:28:13 + --> $DIR/redundant-shared-reference-issue-133685.rs:33:13 | LL | consume(&boxed); | ------- ^^^^^^ `&Box>` is not an iterator @@ -96,7 +100,7 @@ LL | consume(&*boxed); | + error[E0277]: `&&mut Vec` is not an iterator - --> $DIR/redundant-shared-reference-issue-133685.rs:33:13 + --> $DIR/redundant-shared-reference-issue-133685.rs:38:13 | LL | consume(&mut_ref); | ------- ^^^^^^^^ `&&mut Vec` is not an iterator @@ -115,6 +119,25 @@ help: consider dereferencing here LL | consume(&*mut_ref); | + -error: aborting due to 6 previous errors +error[E0277]: the trait bound `&&Source: Value` is not satisfied + --> $DIR/redundant-shared-reference-issue-133685.rs:44:19 + | +LL | consume_value(&shared); + | ------------- ^^^^^^^ the trait `Value` is not implemented for `&&Source` + | | + | required by a bound introduced by this call + | +note: required by a bound in `consume_value` + --> $DIR/redundant-shared-reference-issue-133685.rs:13:26 + | +LL | fn consume_value(_: impl Value) {} + | ^^^^^ required by this bound in `consume_value` +help: consider removing the leading `&`-reference + | +LL - consume_value(&shared); +LL + consume_value(shared); + | + +error: aborting due to 7 previous errors For more information about this error, try `rustc --explain E0277`. From 273f7fe3299b1aa57dea5d2c97fb0ed0b56f2e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ber=C3=A1nek?= Date: Tue, 8 Sep 2026 13:40:48 +0200 Subject: [PATCH 10/32] Add support for `DocJson` to `x perf` --- src/bootstrap/src/core/build_steps/perf.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bootstrap/src/core/build_steps/perf.rs b/src/bootstrap/src/core/build_steps/perf.rs index cc81d9243fe26..239f434ebd69c 100644 --- a/src/bootstrap/src/core/build_steps/perf.rs +++ b/src/bootstrap/src/core/build_steps/perf.rs @@ -95,6 +95,7 @@ pub enum Profile { Check, Debug, Doc, + DocJson, Opt, Clippy, } @@ -105,6 +106,7 @@ impl Display for Profile { Profile::Check => "Check", Profile::Debug => "Debug", Profile::Doc => "Doc", + Profile::DocJson => "DocJson", Profile::Opt => "Opt", Profile::Clippy => "Clippy", }; From 04f4b41b626e6aea95aee08e77931135d5a5baad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ber=C3=A1nek?= Date: Tue, 8 Sep 2026 14:03:14 +0200 Subject: [PATCH 11/32] Allow passing trailing arguments to `rustc-perf` in `x perf` --- src/bootstrap/src/core/build_steps/perf.rs | 5 ++++- src/bootstrap/src/core/session.rs | 6 +++++- src/etc/completions/x.fish | 8 ++++---- src/etc/completions/x.py.fish | 8 ++++---- src/etc/completions/x.py.sh | 8 ++++---- src/etc/completions/x.py.zsh | 8 ++++---- src/etc/completions/x.sh | 8 ++++---- src/etc/completions/x.zsh | 8 ++++---- 8 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src/bootstrap/src/core/build_steps/perf.rs b/src/bootstrap/src/core/build_steps/perf.rs index 239f434ebd69c..930f114f1a70e 100644 --- a/src/bootstrap/src/core/build_steps/perf.rs +++ b/src/bootstrap/src/core/build_steps/perf.rs @@ -136,7 +136,7 @@ impl Display for Scenario { } /// Performs profiling using `rustc-perf` on a built version of the compiler. -pub fn perf(builder: &Builder<'_>, args: &PerfArgs) { +pub fn perf(builder: &Builder<'_>, args: &PerfArgs, trailing_args: &[String]) { let collector = builder.ensure(RustcPerf { compiler: builder.compiler(0, builder.config.host_target), target: builder.config.host_target, @@ -199,6 +199,7 @@ Consider setting `rust.debuginfo-level = 1` in `bootstrap.toml`."#); cmd.arg(prepare_rustc()); apply_shared_opts(&mut cmd, opts); + cmd.args(trailing_args); cmd.run(builder); println!("You can find the results at `{}`", results_dir.display()); @@ -210,6 +211,7 @@ Consider setting `rust.debuginfo-level = 1` in `bootstrap.toml`."#); cmd.arg(prepare_rustc()); apply_shared_opts(&mut cmd, opts); + cmd.args(trailing_args); cmd.run(builder); } PerfCommand::Compare { base, modified } => { @@ -217,6 +219,7 @@ Consider setting `rust.debuginfo-level = 1` in `bootstrap.toml`."#); cmd.arg("--db").arg(&db_path); cmd.arg(base).arg(modified); + cmd.args(trailing_args); cmd.run(builder); } } diff --git a/src/bootstrap/src/core/session.rs b/src/bootstrap/src/core/session.rs index 7207f7dd033ea..4ad04df753d00 100644 --- a/src/bootstrap/src/core/session.rs +++ b/src/bootstrap/src/core/session.rs @@ -586,7 +586,11 @@ impl Session { ); } Subcommand::Perf(args) => { - return crate::core::build_steps::perf::perf(&Builder::new(self), args); + return crate::core::build_steps::perf::perf( + &Builder::new(self), + args, + &self.config.free_args, + ); } _cmd => { debug!(cmd = ?_cmd, "not a hardcoded subcommand; returning to normal handling"); diff --git a/src/etc/completions/x.fish b/src/etc/completions/x.fish index e3cc4ac39d798..58c5e4255caf6 100644 --- a/src/etc/completions/x.fish +++ b/src/etc/completions/x.fish @@ -919,7 +919,7 @@ complete -c x -n "__fish_x_using_subcommand perf; and not __fish_seen_subcommand complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from eprintln" -l include -d 'Select the benchmarks that you want to run (separated by commas). If unspecified, all benchmarks will be executed' -r complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from eprintln" -l exclude -d 'Select the benchmarks matching a prefix in this comma-separated list that you don\'t want to run' -r complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from eprintln" -l scenarios -d 'Select the scenarios that should be benchmarked' -r -f -a "{Full\t'',IncrFull\t'',IncrUnchanged\t'',IncrPatched\t''}" -complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from eprintln" -l profiles -d 'Select the profiles that should be benchmarked' -r -f -a "{Check\t'',Debug\t'',Doc\t'',Opt\t'',Clippy\t''}" +complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from eprintln" -l profiles -d 'Select the profiles that should be benchmarked' -r -f -a "{Check\t'',Debug\t'',Doc\t'',DocJson\t'',Opt\t'',Clippy\t''}" complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from eprintln" -l config -d 'TOML configuration file for build' -r -F complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from eprintln" -l build-dir -d 'Build directory, overrides `build.build-dir` in `bootstrap.toml`' -r -f -a "(__fish_complete_directories)" complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from eprintln" -l build -d 'host target of the stage0 compiler' -r -f @@ -958,7 +958,7 @@ complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_fro complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from samply" -l include -d 'Select the benchmarks that you want to run (separated by commas). If unspecified, all benchmarks will be executed' -r complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from samply" -l exclude -d 'Select the benchmarks matching a prefix in this comma-separated list that you don\'t want to run' -r complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from samply" -l scenarios -d 'Select the scenarios that should be benchmarked' -r -f -a "{Full\t'',IncrFull\t'',IncrUnchanged\t'',IncrPatched\t''}" -complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from samply" -l profiles -d 'Select the profiles that should be benchmarked' -r -f -a "{Check\t'',Debug\t'',Doc\t'',Opt\t'',Clippy\t''}" +complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from samply" -l profiles -d 'Select the profiles that should be benchmarked' -r -f -a "{Check\t'',Debug\t'',Doc\t'',DocJson\t'',Opt\t'',Clippy\t''}" complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from samply" -l config -d 'TOML configuration file for build' -r -F complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from samply" -l build-dir -d 'Build directory, overrides `build.build-dir` in `bootstrap.toml`' -r -f -a "(__fish_complete_directories)" complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from samply" -l build -d 'host target of the stage0 compiler' -r -f @@ -997,7 +997,7 @@ complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_fro complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from cachegrind" -l include -d 'Select the benchmarks that you want to run (separated by commas). If unspecified, all benchmarks will be executed' -r complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from cachegrind" -l exclude -d 'Select the benchmarks matching a prefix in this comma-separated list that you don\'t want to run' -r complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from cachegrind" -l scenarios -d 'Select the scenarios that should be benchmarked' -r -f -a "{Full\t'',IncrFull\t'',IncrUnchanged\t'',IncrPatched\t''}" -complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from cachegrind" -l profiles -d 'Select the profiles that should be benchmarked' -r -f -a "{Check\t'',Debug\t'',Doc\t'',Opt\t'',Clippy\t''}" +complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from cachegrind" -l profiles -d 'Select the profiles that should be benchmarked' -r -f -a "{Check\t'',Debug\t'',Doc\t'',DocJson\t'',Opt\t'',Clippy\t''}" complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from cachegrind" -l config -d 'TOML configuration file for build' -r -F complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from cachegrind" -l build-dir -d 'Build directory, overrides `build.build-dir` in `bootstrap.toml`' -r -f -a "(__fish_complete_directories)" complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from cachegrind" -l build -d 'host target of the stage0 compiler' -r -f @@ -1036,7 +1036,7 @@ complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_fro complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from benchmark" -l include -d 'Select the benchmarks that you want to run (separated by commas). If unspecified, all benchmarks will be executed' -r complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from benchmark" -l exclude -d 'Select the benchmarks matching a prefix in this comma-separated list that you don\'t want to run' -r complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from benchmark" -l scenarios -d 'Select the scenarios that should be benchmarked' -r -f -a "{Full\t'',IncrFull\t'',IncrUnchanged\t'',IncrPatched\t''}" -complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from benchmark" -l profiles -d 'Select the profiles that should be benchmarked' -r -f -a "{Check\t'',Debug\t'',Doc\t'',Opt\t'',Clippy\t''}" +complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from benchmark" -l profiles -d 'Select the profiles that should be benchmarked' -r -f -a "{Check\t'',Debug\t'',Doc\t'',DocJson\t'',Opt\t'',Clippy\t''}" complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from benchmark" -l config -d 'TOML configuration file for build' -r -F complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from benchmark" -l build-dir -d 'Build directory, overrides `build.build-dir` in `bootstrap.toml`' -r -f -a "(__fish_complete_directories)" complete -c x -n "__fish_x_using_subcommand perf; and __fish_seen_subcommand_from benchmark" -l build -d 'host target of the stage0 compiler' -r -f diff --git a/src/etc/completions/x.py.fish b/src/etc/completions/x.py.fish index 2a2aad96c7cd7..292408fbe49f6 100644 --- a/src/etc/completions/x.py.fish +++ b/src/etc/completions/x.py.fish @@ -919,7 +919,7 @@ complete -c x.py -n "__fish_x.py_using_subcommand perf; and not __fish_seen_subc complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from eprintln" -l include -d 'Select the benchmarks that you want to run (separated by commas). If unspecified, all benchmarks will be executed' -r complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from eprintln" -l exclude -d 'Select the benchmarks matching a prefix in this comma-separated list that you don\'t want to run' -r complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from eprintln" -l scenarios -d 'Select the scenarios that should be benchmarked' -r -f -a "{Full\t'',IncrFull\t'',IncrUnchanged\t'',IncrPatched\t''}" -complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from eprintln" -l profiles -d 'Select the profiles that should be benchmarked' -r -f -a "{Check\t'',Debug\t'',Doc\t'',Opt\t'',Clippy\t''}" +complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from eprintln" -l profiles -d 'Select the profiles that should be benchmarked' -r -f -a "{Check\t'',Debug\t'',Doc\t'',DocJson\t'',Opt\t'',Clippy\t''}" complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from eprintln" -l config -d 'TOML configuration file for build' -r -F complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from eprintln" -l build-dir -d 'Build directory, overrides `build.build-dir` in `bootstrap.toml`' -r -f -a "(__fish_complete_directories)" complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from eprintln" -l build -d 'host target of the stage0 compiler' -r -f @@ -958,7 +958,7 @@ complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcomma complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from samply" -l include -d 'Select the benchmarks that you want to run (separated by commas). If unspecified, all benchmarks will be executed' -r complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from samply" -l exclude -d 'Select the benchmarks matching a prefix in this comma-separated list that you don\'t want to run' -r complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from samply" -l scenarios -d 'Select the scenarios that should be benchmarked' -r -f -a "{Full\t'',IncrFull\t'',IncrUnchanged\t'',IncrPatched\t''}" -complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from samply" -l profiles -d 'Select the profiles that should be benchmarked' -r -f -a "{Check\t'',Debug\t'',Doc\t'',Opt\t'',Clippy\t''}" +complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from samply" -l profiles -d 'Select the profiles that should be benchmarked' -r -f -a "{Check\t'',Debug\t'',Doc\t'',DocJson\t'',Opt\t'',Clippy\t''}" complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from samply" -l config -d 'TOML configuration file for build' -r -F complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from samply" -l build-dir -d 'Build directory, overrides `build.build-dir` in `bootstrap.toml`' -r -f -a "(__fish_complete_directories)" complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from samply" -l build -d 'host target of the stage0 compiler' -r -f @@ -997,7 +997,7 @@ complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcomma complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from cachegrind" -l include -d 'Select the benchmarks that you want to run (separated by commas). If unspecified, all benchmarks will be executed' -r complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from cachegrind" -l exclude -d 'Select the benchmarks matching a prefix in this comma-separated list that you don\'t want to run' -r complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from cachegrind" -l scenarios -d 'Select the scenarios that should be benchmarked' -r -f -a "{Full\t'',IncrFull\t'',IncrUnchanged\t'',IncrPatched\t''}" -complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from cachegrind" -l profiles -d 'Select the profiles that should be benchmarked' -r -f -a "{Check\t'',Debug\t'',Doc\t'',Opt\t'',Clippy\t''}" +complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from cachegrind" -l profiles -d 'Select the profiles that should be benchmarked' -r -f -a "{Check\t'',Debug\t'',Doc\t'',DocJson\t'',Opt\t'',Clippy\t''}" complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from cachegrind" -l config -d 'TOML configuration file for build' -r -F complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from cachegrind" -l build-dir -d 'Build directory, overrides `build.build-dir` in `bootstrap.toml`' -r -f -a "(__fish_complete_directories)" complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from cachegrind" -l build -d 'host target of the stage0 compiler' -r -f @@ -1036,7 +1036,7 @@ complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcomma complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from benchmark" -l include -d 'Select the benchmarks that you want to run (separated by commas). If unspecified, all benchmarks will be executed' -r complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from benchmark" -l exclude -d 'Select the benchmarks matching a prefix in this comma-separated list that you don\'t want to run' -r complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from benchmark" -l scenarios -d 'Select the scenarios that should be benchmarked' -r -f -a "{Full\t'',IncrFull\t'',IncrUnchanged\t'',IncrPatched\t''}" -complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from benchmark" -l profiles -d 'Select the profiles that should be benchmarked' -r -f -a "{Check\t'',Debug\t'',Doc\t'',Opt\t'',Clippy\t''}" +complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from benchmark" -l profiles -d 'Select the profiles that should be benchmarked' -r -f -a "{Check\t'',Debug\t'',Doc\t'',DocJson\t'',Opt\t'',Clippy\t''}" complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from benchmark" -l config -d 'TOML configuration file for build' -r -F complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from benchmark" -l build-dir -d 'Build directory, overrides `build.build-dir` in `bootstrap.toml`' -r -f -a "(__fish_complete_directories)" complete -c x.py -n "__fish_x.py_using_subcommand perf; and __fish_seen_subcommand_from benchmark" -l build -d 'host target of the stage0 compiler' -r -f diff --git a/src/etc/completions/x.py.sh b/src/etc/completions/x.py.sh index 1266f85addd8d..0c449988b0461 100644 --- a/src/etc/completions/x.py.sh +++ b/src/etc/completions/x.py.sh @@ -3113,7 +3113,7 @@ _x.py() { return 0 ;; --profiles) - COMPREPLY=($(compgen -W "Check Debug Doc Opt Clippy" -- "${cur}")) + COMPREPLY=($(compgen -W "Check Debug Doc DocJson Opt Clippy" -- "${cur}")) return 0 ;; --config) @@ -3311,7 +3311,7 @@ _x.py() { return 0 ;; --profiles) - COMPREPLY=($(compgen -W "Check Debug Doc Opt Clippy" -- "${cur}")) + COMPREPLY=($(compgen -W "Check Debug Doc DocJson Opt Clippy" -- "${cur}")) return 0 ;; --config) @@ -3695,7 +3695,7 @@ _x.py() { return 0 ;; --profiles) - COMPREPLY=($(compgen -W "Check Debug Doc Opt Clippy" -- "${cur}")) + COMPREPLY=($(compgen -W "Check Debug Doc DocJson Opt Clippy" -- "${cur}")) return 0 ;; --config) @@ -3893,7 +3893,7 @@ _x.py() { return 0 ;; --profiles) - COMPREPLY=($(compgen -W "Check Debug Doc Opt Clippy" -- "${cur}")) + COMPREPLY=($(compgen -W "Check Debug Doc DocJson Opt Clippy" -- "${cur}")) return 0 ;; --config) diff --git a/src/etc/completions/x.py.zsh b/src/etc/completions/x.py.zsh index bd199d2ac0f47..9ee5843a5abd7 100644 --- a/src/etc/completions/x.py.zsh +++ b/src/etc/completions/x.py.zsh @@ -1122,7 +1122,7 @@ _arguments "${_arguments_options[@]}" : \ '*--include=[Select the benchmarks that you want to run (separated by commas). If unspecified, all benchmarks will be executed]:INCLUDE:_default' \ '*--exclude=[Select the benchmarks matching a prefix in this comma-separated list that you don'\''t want to run]:EXCLUDE:_default' \ '*--scenarios=[Select the scenarios that should be benchmarked]:SCENARIOS:(Full IncrFull IncrUnchanged IncrPatched)' \ -'*--profiles=[Select the profiles that should be benchmarked]:PROFILES:(Check Debug Doc Opt Clippy)' \ +'*--profiles=[Select the profiles that should be benchmarked]:PROFILES:(Check Debug Doc DocJson Opt Clippy)' \ '--config=[TOML configuration file for build]:FILE:_files' \ '--build-dir=[Build directory, overrides \`build.build-dir\` in \`bootstrap.toml\`]:DIR:_files -/' \ '--build=[host target of the stage0 compiler]:BUILD:' \ @@ -1171,7 +1171,7 @@ _arguments "${_arguments_options[@]}" : \ '*--include=[Select the benchmarks that you want to run (separated by commas). If unspecified, all benchmarks will be executed]:INCLUDE:_default' \ '*--exclude=[Select the benchmarks matching a prefix in this comma-separated list that you don'\''t want to run]:EXCLUDE:_default' \ '*--scenarios=[Select the scenarios that should be benchmarked]:SCENARIOS:(Full IncrFull IncrUnchanged IncrPatched)' \ -'*--profiles=[Select the profiles that should be benchmarked]:PROFILES:(Check Debug Doc Opt Clippy)' \ +'*--profiles=[Select the profiles that should be benchmarked]:PROFILES:(Check Debug Doc DocJson Opt Clippy)' \ '--config=[TOML configuration file for build]:FILE:_files' \ '--build-dir=[Build directory, overrides \`build.build-dir\` in \`bootstrap.toml\`]:DIR:_files -/' \ '--build=[host target of the stage0 compiler]:BUILD:' \ @@ -1220,7 +1220,7 @@ _arguments "${_arguments_options[@]}" : \ '*--include=[Select the benchmarks that you want to run (separated by commas). If unspecified, all benchmarks will be executed]:INCLUDE:_default' \ '*--exclude=[Select the benchmarks matching a prefix in this comma-separated list that you don'\''t want to run]:EXCLUDE:_default' \ '*--scenarios=[Select the scenarios that should be benchmarked]:SCENARIOS:(Full IncrFull IncrUnchanged IncrPatched)' \ -'*--profiles=[Select the profiles that should be benchmarked]:PROFILES:(Check Debug Doc Opt Clippy)' \ +'*--profiles=[Select the profiles that should be benchmarked]:PROFILES:(Check Debug Doc DocJson Opt Clippy)' \ '--config=[TOML configuration file for build]:FILE:_files' \ '--build-dir=[Build directory, overrides \`build.build-dir\` in \`bootstrap.toml\`]:DIR:_files -/' \ '--build=[host target of the stage0 compiler]:BUILD:' \ @@ -1269,7 +1269,7 @@ _arguments "${_arguments_options[@]}" : \ '*--include=[Select the benchmarks that you want to run (separated by commas). If unspecified, all benchmarks will be executed]:INCLUDE:_default' \ '*--exclude=[Select the benchmarks matching a prefix in this comma-separated list that you don'\''t want to run]:EXCLUDE:_default' \ '*--scenarios=[Select the scenarios that should be benchmarked]:SCENARIOS:(Full IncrFull IncrUnchanged IncrPatched)' \ -'*--profiles=[Select the profiles that should be benchmarked]:PROFILES:(Check Debug Doc Opt Clippy)' \ +'*--profiles=[Select the profiles that should be benchmarked]:PROFILES:(Check Debug Doc DocJson Opt Clippy)' \ '--config=[TOML configuration file for build]:FILE:_files' \ '--build-dir=[Build directory, overrides \`build.build-dir\` in \`bootstrap.toml\`]:DIR:_files -/' \ '--build=[host target of the stage0 compiler]:BUILD:' \ diff --git a/src/etc/completions/x.sh b/src/etc/completions/x.sh index 644c656514f6c..f8897cc5f5ec8 100644 --- a/src/etc/completions/x.sh +++ b/src/etc/completions/x.sh @@ -3113,7 +3113,7 @@ _x() { return 0 ;; --profiles) - COMPREPLY=($(compgen -W "Check Debug Doc Opt Clippy" -- "${cur}")) + COMPREPLY=($(compgen -W "Check Debug Doc DocJson Opt Clippy" -- "${cur}")) return 0 ;; --config) @@ -3311,7 +3311,7 @@ _x() { return 0 ;; --profiles) - COMPREPLY=($(compgen -W "Check Debug Doc Opt Clippy" -- "${cur}")) + COMPREPLY=($(compgen -W "Check Debug Doc DocJson Opt Clippy" -- "${cur}")) return 0 ;; --config) @@ -3695,7 +3695,7 @@ _x() { return 0 ;; --profiles) - COMPREPLY=($(compgen -W "Check Debug Doc Opt Clippy" -- "${cur}")) + COMPREPLY=($(compgen -W "Check Debug Doc DocJson Opt Clippy" -- "${cur}")) return 0 ;; --config) @@ -3893,7 +3893,7 @@ _x() { return 0 ;; --profiles) - COMPREPLY=($(compgen -W "Check Debug Doc Opt Clippy" -- "${cur}")) + COMPREPLY=($(compgen -W "Check Debug Doc DocJson Opt Clippy" -- "${cur}")) return 0 ;; --config) diff --git a/src/etc/completions/x.zsh b/src/etc/completions/x.zsh index 29f05ab2fb7cd..f61942a11da4a 100644 --- a/src/etc/completions/x.zsh +++ b/src/etc/completions/x.zsh @@ -1122,7 +1122,7 @@ _arguments "${_arguments_options[@]}" : \ '*--include=[Select the benchmarks that you want to run (separated by commas). If unspecified, all benchmarks will be executed]:INCLUDE:_default' \ '*--exclude=[Select the benchmarks matching a prefix in this comma-separated list that you don'\''t want to run]:EXCLUDE:_default' \ '*--scenarios=[Select the scenarios that should be benchmarked]:SCENARIOS:(Full IncrFull IncrUnchanged IncrPatched)' \ -'*--profiles=[Select the profiles that should be benchmarked]:PROFILES:(Check Debug Doc Opt Clippy)' \ +'*--profiles=[Select the profiles that should be benchmarked]:PROFILES:(Check Debug Doc DocJson Opt Clippy)' \ '--config=[TOML configuration file for build]:FILE:_files' \ '--build-dir=[Build directory, overrides \`build.build-dir\` in \`bootstrap.toml\`]:DIR:_files -/' \ '--build=[host target of the stage0 compiler]:BUILD:' \ @@ -1171,7 +1171,7 @@ _arguments "${_arguments_options[@]}" : \ '*--include=[Select the benchmarks that you want to run (separated by commas). If unspecified, all benchmarks will be executed]:INCLUDE:_default' \ '*--exclude=[Select the benchmarks matching a prefix in this comma-separated list that you don'\''t want to run]:EXCLUDE:_default' \ '*--scenarios=[Select the scenarios that should be benchmarked]:SCENARIOS:(Full IncrFull IncrUnchanged IncrPatched)' \ -'*--profiles=[Select the profiles that should be benchmarked]:PROFILES:(Check Debug Doc Opt Clippy)' \ +'*--profiles=[Select the profiles that should be benchmarked]:PROFILES:(Check Debug Doc DocJson Opt Clippy)' \ '--config=[TOML configuration file for build]:FILE:_files' \ '--build-dir=[Build directory, overrides \`build.build-dir\` in \`bootstrap.toml\`]:DIR:_files -/' \ '--build=[host target of the stage0 compiler]:BUILD:' \ @@ -1220,7 +1220,7 @@ _arguments "${_arguments_options[@]}" : \ '*--include=[Select the benchmarks that you want to run (separated by commas). If unspecified, all benchmarks will be executed]:INCLUDE:_default' \ '*--exclude=[Select the benchmarks matching a prefix in this comma-separated list that you don'\''t want to run]:EXCLUDE:_default' \ '*--scenarios=[Select the scenarios that should be benchmarked]:SCENARIOS:(Full IncrFull IncrUnchanged IncrPatched)' \ -'*--profiles=[Select the profiles that should be benchmarked]:PROFILES:(Check Debug Doc Opt Clippy)' \ +'*--profiles=[Select the profiles that should be benchmarked]:PROFILES:(Check Debug Doc DocJson Opt Clippy)' \ '--config=[TOML configuration file for build]:FILE:_files' \ '--build-dir=[Build directory, overrides \`build.build-dir\` in \`bootstrap.toml\`]:DIR:_files -/' \ '--build=[host target of the stage0 compiler]:BUILD:' \ @@ -1269,7 +1269,7 @@ _arguments "${_arguments_options[@]}" : \ '*--include=[Select the benchmarks that you want to run (separated by commas). If unspecified, all benchmarks will be executed]:INCLUDE:_default' \ '*--exclude=[Select the benchmarks matching a prefix in this comma-separated list that you don'\''t want to run]:EXCLUDE:_default' \ '*--scenarios=[Select the scenarios that should be benchmarked]:SCENARIOS:(Full IncrFull IncrUnchanged IncrPatched)' \ -'*--profiles=[Select the profiles that should be benchmarked]:PROFILES:(Check Debug Doc Opt Clippy)' \ +'*--profiles=[Select the profiles that should be benchmarked]:PROFILES:(Check Debug Doc DocJson Opt Clippy)' \ '--config=[TOML configuration file for build]:FILE:_files' \ '--build-dir=[Build directory, overrides \`build.build-dir\` in \`bootstrap.toml\`]:DIR:_files -/' \ '--build=[host target of the stage0 compiler]:BUILD:' \ From 19cb4c5bdc4ee82b37b20769bf1da09cfd84e93b Mon Sep 17 00:00:00 2001 From: Joao Roberto Date: Thu, 3 Sep 2026 09:39:39 -0300 Subject: [PATCH 12/32] Derive region assumptions from type outlives clauses `Assumptions::new` now elaborates the clauses it is given, so callers which build assumptions straight from where clauses no longer each have to remember to do it themselves. A `Ty: 'a` clause also tells us that every region component of `Ty` outlives `'a`, and that the components themselves do, which placeholder and alias outlives need. It takes clauses rather than only the outlives ones because trait clauses imply outlives through their supertraits: `T: Bound<'a>` with `trait Bound<'c>: 'static` is evidence for `T: 'static`. Narrowing the input to outlives clauses would drop those before elaboration could reach them. The test harness keeps using `new_unelaborated` so that a `forall`'s assumptions are exactly the ones written down in the test, with no extra ones hidden behind the scenes. --- .../rustc_hir_analysis/src/check/wfcheck.rs | 8 ++- .../src/infer/outlives/obligations.rs | 22 +++++- .../eval_ctxt/solver_region_constraints.rs | 38 ++++------ .../rustc_type_ir/src/region_constraint.rs | 70 ++++++++++++++++++- ...supertrait-implied-outlives-assumptions.rs | 33 +++++++++ 5 files changed, 138 insertions(+), 33 deletions(-) create mode 100644 tests/ui/assumptions_on_binders/supertrait-implied-outlives-assumptions.rs diff --git a/compiler/rustc_hir_analysis/src/check/wfcheck.rs b/compiler/rustc_hir_analysis/src/check/wfcheck.rs index 0dee9690737df..34aafd72526a8 100644 --- a/compiler/rustc_hir_analysis/src/check/wfcheck.rs +++ b/compiler/rustc_hir_analysis/src/check/wfcheck.rs @@ -2406,8 +2406,12 @@ impl<'tcx> WfCheckingCtxt<'_, 'tcx> { for &(r1, r2) in &body.region_outlives { builder.add(r1, r2); } - let assumptions = - ty::region_constraint::Assumptions::new(body.type_outlives, builder.freeze()); + // Deliberately unelaborated: the assumptions of a `forall` are exactly the ones + // written down in the test, no extra ones hidden behind the scenes. + let assumptions = ty::region_constraint::Assumptions::new_unelaborated( + body.type_outlives, + builder.freeze(), + ); self.infcx.insert_placeholder_assumptions(u, Some(assumptions)); self.check_test_binder_body(body.value); let solver_region_constraint = self.infcx.get_solver_region_constraint(); diff --git a/compiler/rustc_infer/src/infer/outlives/obligations.rs b/compiler/rustc_infer/src/infer/outlives/obligations.rs index cbbf5e3c91c42..4389388d45407 100644 --- a/compiler/rustc_infer/src/infer/outlives/obligations.rs +++ b/compiler/rustc_infer/src/infer/outlives/obligations.rs @@ -66,7 +66,7 @@ use rustc_middle::mir::ConstraintCategory; use rustc_middle::ty::outlives::{Component, push_outlives_components}; use rustc_middle::ty::{ self, GenericArgKind, GenericArgsRef, PolyTypeOutlivesClause, Region, RegionVid, Ty, TyCtxt, - TypeVisitableExt, eager_resolve_vars, + TypeVisitableExt, Upcast, eager_resolve_vars, }; use rustc_span::Span; use rustc_type_ir::region_constraint::{self, LeafRegionConstraint}; @@ -235,8 +235,10 @@ impl<'tcx> InferCtxt<'tcx> { outlives_env: &OutlivesEnvironment<'tcx>, ) { let assumptions = rustc_type_ir::region_constraint::Assumptions::new( - outlives_env.known_type_outlives().into_iter().cloned().collect(), + self, + type_outlives_clauses(self.tcx, outlives_env.known_type_outlives().iter().copied()), outlives_env.free_region_map().relation.clone(), + ty::UniverseIndex::ROOT, ); self.destructure_solver_region_constraints(assumptions, self); } @@ -249,8 +251,10 @@ impl<'tcx> InferCtxt<'tcx> { region_outlives: TransitiveRelation, ) { let assumptions = region_constraint::Assumptions::new( - known_type_outlives.into_iter().cloned().collect(), + self, + type_outlives_clauses(self.tcx, known_type_outlives.iter().copied()), region_outlives.maybe_map(|r| Some(Region::new_var(self.tcx, r))).unwrap(), + ty::UniverseIndex::ROOT, ); self.destructure_solver_region_constraints(assumptions, conversion); } @@ -376,6 +380,18 @@ impl<'tcx> InferCtxt<'tcx> { } } +/// Turns type outlives where clauses into clauses for +/// [`region_constraint::Assumptions::new`] to elaborate. +fn type_outlives_clauses<'tcx>( + tcx: TyCtxt<'tcx>, + type_outlives: impl IntoIterator>, +) -> Vec> { + type_outlives + .into_iter() + .map(|c| c.map_bound(ty::ClauseKind::TypeOutlives).upcast(tcx)) + .collect() +} + /// The `TypeOutlives` struct has the job of "lowering" a `T: 'a` /// obligation into a series of `'a: 'b` constraints and "verify"s, as /// described on the module comment. The final constraints are emitted diff --git a/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/solver_region_constraints.rs b/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/solver_region_constraints.rs index 5a4daa5e44fc5..5ecc06b30f33b 100644 --- a/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/solver_region_constraints.rs +++ b/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/solver_region_constraints.rs @@ -2,7 +2,6 @@ #[cfg(feature = "nightly")] use rustc_data_structures::transitive_relation::TransitiveRelationBuilder; -use rustc_type_ir::ClauseKind::*; use rustc_type_ir::inherent::*; use rustc_type_ir::outlives::{Component, push_outlives_components}; #[cfg(not(feature = "nightly"))] @@ -12,8 +11,8 @@ use rustc_type_ir::region_constraint::{ propagate_ambiguity, }; use rustc_type_ir::{ - AliasTy, Binder, ClauseKind, InferCtxtLike, Interner, OutlivesClause, Region, TypeVisitable, - TypeVisitableExt, TypeVisitor, UniverseIndex, max_universe, + AliasTy, Binder, ClauseKind, InferCtxtLike, Interner, Region, TypeVisitable, TypeVisitableExt, + TypeVisitor, UniverseIndex, }; use tracing::{debug, instrument}; @@ -82,9 +81,6 @@ where t.visit_with(&mut reqs_builder); let reqs = reqs_builder.out; - let mut region_outlives_builder = TransitiveRelationBuilder::default(); - let mut type_outlives = vec![]; - // If there are inference variables in type outlives then we may not be able // to elaborate to the full set of implied bounds right now. To avoid incorrectly // NoSolution'ing when lifting constraints to a lower universe due to no usable @@ -102,25 +98,17 @@ where // FIXME(-Zassumptions-on-binders): we need to normalize here/somewhere // as we assume the type outlives assumptions only have rigid types :> - let clauses = rustc_type_ir::elaborate::elaborate( - self.cx(), - reqs.into_iter().filter_map(|goal| goal.predicate.as_clause()), - ); - - clauses.filter(move |clause| max_universe(&**self.delegate, *clause) == u).for_each( - |clause| match clause.kind().skip_binder() { - RegionOutlives(OutlivesClause(r1, r2)) => { - assert!(clause.kind().no_bound_vars().is_some()); - region_outlives_builder.add(r1, r2); - } - TypeOutlives(p) => { - type_outlives.push(clause.kind().map_bound(|_| p)); - } - _ => (), - }, - ); - - Some(Assumptions::new(type_outlives, region_outlives_builder.freeze())) + // + // `Assumptions::new` elaborates, restricts the clauses to `u` and picks out the + // outlives ones for us, so we just hand over everything the requirements gave us. + let clauses = reqs.into_iter().filter_map(|goal| goal.predicate.as_clause()); + + Some(Assumptions::new( + &**self.delegate, + clauses, + TransitiveRelationBuilder::default().freeze(), + u, + )) } #[instrument(level = "debug", skip(self), ret)] diff --git a/compiler/rustc_type_ir/src/region_constraint.rs b/compiler/rustc_type_ir/src/region_constraint.rs index 9a643b538d93f..815acb11b9955 100644 --- a/compiler/rustc_type_ir/src/region_constraint.rs +++ b/compiler/rustc_type_ir/src/region_constraint.rs @@ -51,14 +51,19 @@ use crate::fold::TypeSuperFoldable; use crate::inherent::*; use crate::relate::{Relate, RelateResult, TypeRelation, VarianceDiagInfo}; use crate::{ - AliasTy, Binder, BoundRegion, BoundVar, BoundVariableKind, DebruijnIndex, InferCtxtLike, - Interner, IsRigid, OutlivesClause, Region, RegionKind, TyKind, TypeFoldable, TypeFolder, - TypingMode, UniverseIndex, Variance, max_universe, set_aliases_to_non_rigid, + AliasTy, Binder, BoundRegion, BoundVar, BoundVariableKind, ClauseKind, DebruijnIndex, + InferCtxtLike, Interner, IsRigid, OutlivesClause, Region, RegionKind, TyKind, TypeFoldable, + TypeFolder, TypingMode, UniverseIndex, Variance, elaborate, max_universe, + set_aliases_to_non_rigid, }; #[derive_where(Clone, Debug; I: Interner)] pub struct Assumptions { pub type_outlives: Vec>>, + /// Known `'a: 'b` assumptions, stored as an edge from the outliving region to the + /// outlived one, i.e. an edge `('a, 'b)` means `'a: 'b`. Constructors expect a relation + /// with this direction, see [`regions_outlived_by`] and [`regions_outliving`] for how it + /// is consumed. pub region_outlives: TransitiveRelation>, pub inverse_region_outlives: TransitiveRelation>, } @@ -72,7 +77,66 @@ impl Assumptions { } } + /// Builds assumptions from `clauses`, elaborating them and keeping the outlives ones. + /// + /// Callers hand us their clauses straight from the environment, so we have to elaborate + /// here to get at the implied outlives bounds: + /// - a `Ty: 'a` clause tells us that every region component of `Ty` outlives `'a`, e.g. + /// `&'b u8: 'a` implies `'b: 'a`. Without it we'd fail to prove `'b: 'a` when leaving + /// the binder these assumptions belong to. + /// - it also gives us the components as type outlives, e.g. `Vec: 'a` implies `T: 'a`, + /// which we need for placeholder and alias outlives. + /// - trait clauses imply their supertraits, so `T: Bound<'a>` where `trait Bound<'c>: 'c` + /// gives us `T: 'a`. This is why we take clauses rather than just the outlives ones: + /// filtering down to outlives before elaborating would throw those away. + /// + /// Only the clauses whose max universe is exactly `universe` are kept, which is what the + /// solver wants when computing the assumptions of a single binder. This happens after + /// elaboration on purpose, so a clause whose regions live in more than one universe still + /// contributes its implied bounds to each of them: `(&'b u8, &'c u8): 'a` gives us + /// `'c: 'a` in `'c`s universe even though the clause itself is in `'b`s. + /// + /// Use [`Assumptions::new_unelaborated`] when the caller needs the assumptions to be + /// exactly the clauses it passed in. pub fn new( + infcx: &impl InferCtxtLike, + clauses: impl IntoIterator, + region_outlives: TransitiveRelation>, + universe: UniverseIndex, + ) -> Self { + let mut type_outlives = vec![]; + let mut region_outlives_builder = TransitiveRelationBuilder::default(); + for (r1, r2) in region_outlives.base_edges() { + region_outlives_builder.add(r1, r2); + } + + let clauses = elaborate::elaborate(infcx.cx(), clauses) + .filter(|clause| max_universe(infcx, *clause) == universe); + for clause in clauses { + match clause.kind().skip_binder() { + // The type outlives assumptions are kept around as they are required for + // proving placeholder and alias outlives. + ClauseKind::TypeOutlives(_) => { + type_outlives.push(clause.as_type_outlives_clause().unwrap()); + } + ClauseKind::RegionOutlives(OutlivesClause(r1, r2)) => { + // `elaborate` drops the components which are bound inside of the type and + // bails on `for<'a> Ty: 'a`, so both regions here are free even though the + // clause itself may still be under a binder. + debug_assert!(!r1.is_bound() && !r2.is_bound()); + region_outlives_builder.add(r1, r2); + } + // Anything else can't be used as an outlives assumption. + _ => (), + } + } + + Self::new_unelaborated(type_outlives, region_outlives_builder.freeze()) + } + + /// Builds assumptions from exactly the given clauses, see [`Assumptions::new`] for when + /// the clauses should get elaborated instead. + pub fn new_unelaborated( type_outlives: Vec>>, region_outlives: TransitiveRelation>, ) -> Self { diff --git a/tests/ui/assumptions_on_binders/supertrait-implied-outlives-assumptions.rs b/tests/ui/assumptions_on_binders/supertrait-implied-outlives-assumptions.rs new file mode 100644 index 0000000000000..8c30faecc11b0 --- /dev/null +++ b/tests/ui/assumptions_on_binders/supertrait-implied-outlives-assumptions.rs @@ -0,0 +1,33 @@ +//@ check-pass +//@ compile-flags: -Zassumptions-on-binders -Znext-solver=globally + +// A trait clause implies its supertraits, so a `&'x (): Bound<'x>` requirement is also evidence +// for `&'x (): 'static`, which elaborates to the region assumption `'x: 'static`. +// `Assumptions::new` therefore takes clauses and elaborates them itself; handing it only the +// outlives clauses would drop the trait clause before it could imply anything. +// +// The clause has to mention the binder's own `'x` to survive the `max_universe == u` filter, +// while the supertrait outlives is on `'static` so that the assumption can discharge `'x: 'a`. +// +// Keeping the requirement binder-local matters: the `for<'x> Wrap<'x>: 'a` bound is proven at the +// call site below, so failing to discharge `'x: 'a` is a `NoSolution` inside the solver rather +// than a constraint escaping to the root. Constraints reaching the root are still dropped, so a +// shape which lets `'x` escape (e.g. requiring `T: 'x` for an outer `T`) would pass either way. +// Removing the `&'x (): Bound<'x>` clause below makes this fail, as does dropping trait clauses +// before elaborating. + +trait Bound<'c>: 'static {} + +struct Wrap<'x>(&'x ()) +where + &'x (): Bound<'x>; + +fn foo<'a>(_a: &'a u32) +where + for<'x> Wrap<'x>: 'a, +{ +} + +fn main() { + foo(&10); +} From 3a57224ae6d9807e4f37f1435de412deac95bcbe Mon Sep 17 00:00:00 2001 From: Joao Roberto Date: Thu, 3 Sep 2026 09:43:06 -0300 Subject: [PATCH 13/32] Include implied type outlives bounds at the root `known_type_outlives` only holds the explicit `Ty: 'a` where clauses. The implied bounds, e.g. `T: 'a` from a `&'a T` argument, are tracked separately in `region_bound_pairs`, so both have to be passed in. Without them we fail to prove `T: 'a` for a `&'a T` argument whenever the only explicit bound on `T` mentions a different region. --- compiler/rustc_borrowck/src/type_check/mod.rs | 1 + .../src/infer/outlives/obligations.rs | 29 ++++++++++++++----- .../type-outlives-assumptions.rs | 26 +++++++++++++++++ 3 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 tests/ui/assumptions_on_binders/type-outlives-assumptions.rs diff --git a/compiler/rustc_borrowck/src/type_check/mod.rs b/compiler/rustc_borrowck/src/type_check/mod.rs index 9f23a0d5ab631..dde44e7e57713 100644 --- a/compiler/rustc_borrowck/src/type_check/mod.rs +++ b/compiler/rustc_borrowck/src/type_check/mod.rs @@ -187,6 +187,7 @@ pub(crate) fn type_check<'tcx>( typeck.infcx.destructure_solver_region_constraints_for_borrowck( &mut converter, typeck.known_type_outlives_obligations, + typeck.region_bound_pairs, universal_region_relations.outlives.clone(), ); } diff --git a/compiler/rustc_infer/src/infer/outlives/obligations.rs b/compiler/rustc_infer/src/infer/outlives/obligations.rs index 4389388d45407..314c0a06e50b8 100644 --- a/compiler/rustc_infer/src/infer/outlives/obligations.rs +++ b/compiler/rustc_infer/src/infer/outlives/obligations.rs @@ -236,7 +236,11 @@ impl<'tcx> InferCtxt<'tcx> { ) { let assumptions = rustc_type_ir::region_constraint::Assumptions::new( self, - type_outlives_clauses(self.tcx, outlives_env.known_type_outlives().iter().copied()), + assumed_type_outlives( + self.tcx, + outlives_env.known_type_outlives(), + outlives_env.region_bound_pairs(), + ), outlives_env.free_region_map().relation.clone(), ty::UniverseIndex::ROOT, ); @@ -248,11 +252,12 @@ impl<'tcx> InferCtxt<'tcx> { // this is always ConstraintConversion but lol conversion: impl TypeOutlivesDelegate<'tcx>, known_type_outlives: &[PolyTypeOutlivesClause<'tcx>], + region_bound_pairs: &RegionBoundPairs<'tcx>, region_outlives: TransitiveRelation, ) { let assumptions = region_constraint::Assumptions::new( self, - type_outlives_clauses(self.tcx, known_type_outlives.iter().copied()), + assumed_type_outlives(self.tcx, known_type_outlives, region_bound_pairs), region_outlives.maybe_map(|r| Some(Region::new_var(self.tcx, r))).unwrap(), ty::UniverseIndex::ROOT, ); @@ -380,14 +385,24 @@ impl<'tcx> InferCtxt<'tcx> { } } -/// Turns type outlives where clauses into clauses for +/// The type outlives assumptions available in the root context, as clauses for /// [`region_constraint::Assumptions::new`] to elaborate. -fn type_outlives_clauses<'tcx>( +/// +/// `known_type_outlives` only contains the explicit `Ty: 'a` where clauses. The implied bounds, +/// e.g. `T: 'a` from a `&'a T` argument, are only tracked in `region_bound_pairs` so we have to +/// pull them in separately. Without them we'd fail to prove `T: 'a` for a `&'a T` argument +/// whenever the only explicit bound on `T` mentions a different region. +fn assumed_type_outlives<'tcx>( tcx: TyCtxt<'tcx>, - type_outlives: impl IntoIterator>, + known_type_outlives: &[PolyTypeOutlivesClause<'tcx>], + region_bound_pairs: &RegionBoundPairs<'tcx>, ) -> Vec> { - type_outlives - .into_iter() + known_type_outlives + .iter() + .copied() + .chain(region_bound_pairs.iter().map(|&ty::OutlivesClause(kind, r)| { + ty::Binder::dummy(ty::OutlivesClause(kind.to_ty(tcx), r)) + })) .map(|c| c.map_bound(ty::ClauseKind::TypeOutlives).upcast(tcx)) .collect() } diff --git a/tests/ui/assumptions_on_binders/type-outlives-assumptions.rs b/tests/ui/assumptions_on_binders/type-outlives-assumptions.rs new file mode 100644 index 0000000000000..bc3b3a7f2c250 --- /dev/null +++ b/tests/ui/assumptions_on_binders/type-outlives-assumptions.rs @@ -0,0 +1,26 @@ +//@ check-pass +//@ compile-flags: -Zassumptions-on-binders + +// Regression test for rust-lang/project-assumptions-on-binders#19, based on the `syn` failure. +// The receiver gives us an implied `I: 'b` bound and `'b: 'a` lets that satisfy the object +// lifetime. The implied type bound has to be included in the root assumptions for that to work. +trait IterTrait<'a, T: 'a>: Iterator { + fn clone_box<'b>(&'b self) -> Box + 'a> + where + 'b: 'a; +} + +impl<'a, T, I> IterTrait<'a, T> for I +where + T: 'a, + I: Iterator + Clone, +{ + fn clone_box<'b>(&'b self) -> Box + 'a> + where + 'b: 'a, + { + Box::new(self.clone()) + } +} + +fn main() {} From 822cbdfa0bb4188e87a886d8d820629892aa79a1 Mon Sep 17 00:00:00 2001 From: Joao Roberto Date: Thu, 3 Sep 2026 11:41:49 -0300 Subject: [PATCH 14/32] Invert free region map edges in root assumptions `FreeRegionMap::relation` stores `'sub <= 'sup` edges while `Assumptions::region_outlives` expects `'longer: 'shorter` ones. The mismatch is not yet observable as nothing reads the region relation at the root, but `Assumptions::new` merges edges derived from type outlives clauses into the same relation, which would otherwise leave it with mixed edge directions. --- .../rustc_infer/src/infer/outlives/obligations.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/compiler/rustc_infer/src/infer/outlives/obligations.rs b/compiler/rustc_infer/src/infer/outlives/obligations.rs index 314c0a06e50b8..dbe85e5315500 100644 --- a/compiler/rustc_infer/src/infer/outlives/obligations.rs +++ b/compiler/rustc_infer/src/infer/outlives/obligations.rs @@ -59,7 +59,7 @@ //! might later infer `?U` to something like `&'b u32`, which would //! imply that `'b: 'a`. -use rustc_data_structures::transitive_relation::TransitiveRelation; +use rustc_data_structures::transitive_relation::{TransitiveRelation, TransitiveRelationBuilder}; use rustc_data_structures::undo_log::UndoLogs; use rustc_middle::bug; use rustc_middle::mir::ConstraintCategory; @@ -234,6 +234,13 @@ impl<'tcx> InferCtxt<'tcx> { &self, outlives_env: &OutlivesEnvironment<'tcx>, ) { + // `FreeRegionMap::relation` stores `'sub <= 'sup` edges while + // `Assumptions::region_outlives` expects `'longer: 'shorter` ones, so the + // edges have to be inverted here. + let mut region_outlives = TransitiveRelationBuilder::default(); + for (r1, r2) in outlives_env.free_region_map().relation.base_edges() { + region_outlives.add(r2, r1); + } let assumptions = rustc_type_ir::region_constraint::Assumptions::new( self, assumed_type_outlives( @@ -241,7 +248,7 @@ impl<'tcx> InferCtxt<'tcx> { outlives_env.known_type_outlives(), outlives_env.region_bound_pairs(), ), - outlives_env.free_region_map().relation.clone(), + region_outlives.freeze(), ty::UniverseIndex::ROOT, ); self.destructure_solver_region_constraints(assumptions, self); From 2bb92c7a0f29bb7010ad407de58a860b967ebefc Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Wed, 10 Jun 2026 04:01:26 +0900 Subject: [PATCH 15/32] lower move expressions in coroutine closures --- .../rustc_ast_lowering/src/diagnostics.rs | 2 +- compiler/rustc_ast_lowering/src/expr.rs | 15 +++- .../rustc_ast_lowering/src/expr/closure.rs | 71 +++++++++++-------- compiler/rustc_hir_analysis/src/collect.rs | 10 ++- 4 files changed, 67 insertions(+), 31 deletions(-) diff --git a/compiler/rustc_ast_lowering/src/diagnostics.rs b/compiler/rustc_ast_lowering/src/diagnostics.rs index b0fada9d3cd9e..aa8550f9b99ed 100644 --- a/compiler/rustc_ast_lowering/src/diagnostics.rs +++ b/compiler/rustc_ast_lowering/src/diagnostics.rs @@ -148,7 +148,7 @@ pub(crate) struct ClosureCannotBeStatic { } #[derive(Diagnostic)] -#[diag("`move(expr)` is only supported in plain closures")] +#[diag("`move(expr)` is only supported in closures")] pub(crate) struct MoveExprOnlyInPlainClosures { #[primary_span] pub span: Span, diff --git a/compiler/rustc_ast_lowering/src/expr.rs b/compiler/rustc_ast_lowering/src/expr.rs index 0a4a2ae7145e3..2d5f760114ec5 100644 --- a/compiler/rustc_ast_lowering/src/expr.rs +++ b/compiler/rustc_ast_lowering/src/expr.rs @@ -865,6 +865,19 @@ impl<'hir> LoweringContext<'_, 'hir> { (params, res) }); + let explicit_captures: &'hir [hir::ExplicitCapture] = + if let Some(move_expr_state) = self.move_expr_bindings.last().and_then(Option::as_ref) { + self.arena.alloc_from_iter(move_expr_state.occurrences.iter().filter_map( + |occurrence| { + occurrence + .explicit_capture + .then_some(hir::ExplicitCapture { var_hir_id: occurrence.binding }) + }, + )) + } else { + &[] + }; + // `static |<_task_context?>| -> { }`: hir::ExprKind::Closure(self.arena.alloc(hir::Closure { def_id: closure_def_id, @@ -877,7 +890,7 @@ impl<'hir> LoweringContext<'_, 'hir> { fn_arg_span: None, kind: hir::ClosureKind::Coroutine(coroutine_kind), constness: hir::Constness::NotConst, - explicit_captures: &[], + explicit_captures, })) } diff --git a/compiler/rustc_ast_lowering/src/expr/closure.rs b/compiler/rustc_ast_lowering/src/expr/closure.rs index 2831fb4fa8352..95f54ba281e17 100644 --- a/compiler/rustc_ast_lowering/src/expr/closure.rs +++ b/compiler/rustc_ast_lowering/src/expr/closure.rs @@ -12,8 +12,8 @@ use crate::diagnostics::{ClosureCannotBeStatic, CoroutineTooManyParameters}; impl<'hir> LoweringContext<'_, 'hir> { // Entry point for `ExprKind::Closure`. Plain closures go through // `lower_expr_plain_closure_with_move_exprs`, which can wrap the lowered - // closure in `let` initializers for `move(...)`. Coroutine closures keep the - // existing coroutine-specific path and reject `move(...)` for now. + // closure in `let` initializers for `move(...)`. Coroutine closures use the + // same wrapper after building their coroutine-specific body shape. pub(super) fn lower_expr_closure_expr( &mut self, e: &Expr, @@ -23,8 +23,6 @@ impl<'hir> LoweringContext<'_, 'hir> { let attrs = self.lower_attrs(expr_hir_id, &e.attrs, e.span, Target::from_expr(e)); match closure.coroutine_marker { - // FIXME(TaKO8Ki): Support `move(expr)` in coroutine closures too. - // For the first step, we only support plain closures. Some(coroutine_marker) => hir::Expr { hir_id: expr_hir_id, kind: self.lower_expr_coroutine_closure( @@ -117,12 +115,24 @@ impl<'hir> LoweringContext<'_, 'hir> { fn_arg_span, ); + let closure_expr = hir::Expr { + hir_id: expr_hir_id, + kind: closure_kind, + span: self.lower_span(whole_span), + }; + + self.lower_expr_with_move_exprs(closure_expr, move_expr_state, body, whole_span) + } + + fn lower_expr_with_move_exprs( + &mut self, + expr: hir::Expr<'hir>, + move_expr_state: MoveExprState<'hir>, + body: &Expr, + whole_span: Span, + ) -> hir::Expr<'hir> { if move_expr_state.occurrences.is_empty() { - return hir::Expr { - hir_id: expr_hir_id, - kind: closure_kind, - span: self.lower_span(whole_span), - }; + return expr; } let initializers = MoveExprInitializerFinder::collect(body) @@ -162,14 +172,8 @@ impl<'hir> LoweringContext<'_, 'hir> { initializer_bindings.insert(occurrence.id, (occurrence.ident, occurrence.binding)); } - let closure_expr = self.arena.alloc(hir::Expr { - hir_id: expr_hir_id, - kind: closure_kind, - span: self.lower_span(whole_span), - }); - let stmts = self.arena.alloc_from_iter(stmts); - let block = self.block_all(whole_span, stmts, Some(closure_expr)); + let block = self.block_all(whole_span, stmts, Some(self.arena.alloc(expr))); self.expr(whole_span, hir::ExprKind::Block(block, None)) } @@ -294,9 +298,9 @@ impl<'hir> LoweringContext<'_, 'hir> { } // Coroutine closures are lowered separately because they build a different - // body shape. This path pushes `None` for `move_expr_bindings`, so any - // `move(...)` in the coroutine body gets a targeted unsupported-position - // error instead of being collected like a plain closure occurrence. + // body shape. The source body is still lowered with `MoveExprState` active, + // so `move(...)` occurrences are collected and then hoisted to the outer + // closure body, immediately before the generated coroutine is created. fn lower_expr_coroutine_closure( &mut self, binder: &ClosureBinder, @@ -332,16 +336,27 @@ impl<'hir> LoweringContext<'_, 'hir> { // Transform `async |x: u8| -> X { ... }` into // `|x: u8| || -> X { ... }`. let body_id = this.lower_body(|this| { - let ((parameters, expr), _) = this.with_move_expr_bindings(None, |this| { - this.lower_coroutine_body_with_moved_arguments( - &inner_decl, - |this| this.with_new_scopes(fn_decl_span, |this| this.lower_expr_mut(body)), + let ((parameters, expr), move_expr_state) = + this.with_move_expr_bindings(Some(MoveExprState::default()), |this| { + this.lower_coroutine_body_with_moved_arguments( + &inner_decl, + |this| { + this.with_new_scopes(fn_decl_span, |this| this.lower_expr_mut(body)) + }, + fn_decl_span, + body.span, + coroutine_marker, + hir::CoroutineSource::Closure, + ) + }); + let Some(move_expr_state) = move_expr_state else { + span_bug!( fn_decl_span, - body.span, - coroutine_marker, - hir::CoroutineSource::Closure, - ) - }); + "coroutine closure lowering did not return `move(...)` state" + ); + }; + + let expr = this.lower_expr_with_move_exprs(expr, move_expr_state, body, body.span); this.maybe_forward_track_caller(closure_hir_id, expr.hir_id); diff --git a/compiler/rustc_hir_analysis/src/collect.rs b/compiler/rustc_hir_analysis/src/collect.rs index 2e4da8d948f07..2f61ed9be30b8 100644 --- a/compiler/rustc_hir_analysis/src/collect.rs +++ b/compiler/rustc_hir_analysis/src/collect.rs @@ -1709,6 +1709,14 @@ fn coroutine_for_closure(tcx: TyCtxt<'_>, def_id: LocalDefId) -> DefId { bug!() }; + let body = tcx.hir_body(body).value; + // `move(...)` in coroutine closures wraps the generated coroutine in an + // outer block of synthetic initializer lets. + let body = match body.kind { + hir::ExprKind::Block(block, None) if let Some(tail) = block.expr => tail, + _ => body, + }; + let &hir::Expr { kind: hir::ExprKind::Closure(&rustc_hir::Closure { @@ -1717,7 +1725,7 @@ fn coroutine_for_closure(tcx: TyCtxt<'_>, def_id: LocalDefId) -> DefId { .. }), .. - } = tcx.hir_body(body).value + } = body else { bug!() }; From ee0ee6b66fcc2a3172ad4fb4e084639cf513649f Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 11 Jun 2026 13:07:15 +0900 Subject: [PATCH 16/32] handle move-expression captures in coroutine closures --- compiler/rustc_hir_typeck/src/upvar.rs | 78 ++++++++++++------- compiler/rustc_middle/src/ty/closure.rs | 18 ++--- compiler/rustc_middle/src/ty/mod.rs | 2 +- .../src/coroutine/by_move_body.rs | 12 ++- 4 files changed, 66 insertions(+), 44 deletions(-) diff --git a/compiler/rustc_hir_typeck/src/upvar.rs b/compiler/rustc_hir_typeck/src/upvar.rs index 38839c598f913..89b91094f50d2 100644 --- a/compiler/rustc_hir_typeck/src/upvar.rs +++ b/compiler/rustc_hir_typeck/src/upvar.rs @@ -504,44 +504,64 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> { .tupled_inputs_ty .tuple_fields() .len(); + let coroutine_def_id = + self.tcx.coroutine_for_closure(closure_def_id).expect_local(); + let explicit_captures = self + .tcx + .hir_node_by_def_id(coroutine_def_id) + .expect_closure() + .explicit_captures; let typeck_results = self.typeck_results.borrow(); + let parent_captures = typeck_results + .closure_min_captures_flattened(closure_def_id) + .collect::>(); let tupled_upvars_ty_for_borrow = Ty::new_tup_from_iter( self.tcx, - ty::analyze_coroutine_closure_captures( - typeck_results.closure_min_captures_flattened(closure_def_id), - typeck_results - .closure_min_captures_flattened( - self.tcx.coroutine_for_closure(closure_def_id).expect_local(), - ) - // Skip the captures that are just moving the closure's args - // into the coroutine. These are always by move, and we append - // those later in the `CoroutineClosureSignature` helper functions. - .skip(num_args), - |(_, parent_capture), (_, child_capture)| { - // This is subtle. See documentation on function. - let needs_ref = should_reborrow_from_env_of_parent_coroutine_closure( - parent_capture, - child_capture, - ); - + typeck_results + .closure_min_captures_flattened(coroutine_def_id) + // Skip the captures that are just moving the closure's args + // into the coroutine. These are always by move, and we append + // those later in the `CoroutineClosureSignature` helper functions. + .skip(num_args) + .map(|child_capture| { let upvar_ty = child_capture.place.ty(); let capture = child_capture.info.capture_kind; - // Not all upvars are captured by ref, so use - // `apply_capture_kind_on_capture_ty` to ensure that we - // compute the right captured type. - apply_capture_kind_on_capture_ty( - self.tcx, - upvar_ty, - capture, - if needs_ref { + let region = if explicit_captures.iter().any(|explicit| { + explicit.var_hir_id == child_capture.get_root_variable() + }) { + // Synthetic move-expression locals are captured by + // value into the generated coroutine. They do not + // reborrow from the parent coroutine-closure env. + self.tcx.lifetimes.re_erased + } else { + let Some(parent_capture) = parent_captures.iter().copied().find( + |parent_capture| { + ty::child_prefix_matches_parent_projections( + parent_capture, + child_capture, + ) + }, + ) else { + bug!("child capture did not match a parent coroutine capture"); + }; + + // This is subtle. See documentation on function. + if should_reborrow_from_env_of_parent_coroutine_closure( + parent_capture, + child_capture, + ) { closure_env_region } else { self.tcx.lifetimes.re_erased - }, - ) - }, - ), + } + }; + + // Not all upvars are captured by ref, so use + // `apply_capture_kind_on_capture_ty` to ensure that we + // compute the right captured type. + apply_capture_kind_on_capture_ty(self.tcx, upvar_ty, capture, region) + }), ); let coroutine_captures_by_ref_ty = Ty::new_fn_ptr( self.tcx, diff --git a/compiler/rustc_middle/src/ty/closure.rs b/compiler/rustc_middle/src/ty/closure.rs index c6cdb9b9e2b96..7bf143f625405 100644 --- a/compiler/rustc_middle/src/ty/closure.rs +++ b/compiler/rustc_middle/src/ty/closure.rs @@ -434,11 +434,11 @@ pub fn analyze_coroutine_closure_captures<'a, 'tcx: 'a, T>( // refining the set of captures via edition-2021 precise captures. We want to // match up any number of child captures with one parent capture, so we keep // peeking off this `Peekable` until the child doesn't match anymore. + // + // Do not require every parent capture to match a child capture. A parent + // capture may be used only while evaluating a coroutine-closure + // `move(expr)` initializer, before the child coroutine is created. for (parent_field_idx, parent_capture) in parent_captures.into_iter().enumerate() { - // Make sure we use every field at least once, b/c why are we capturing something - // if it's not used in the inner coroutine. - let mut field_used_at_least_once = false; - // A parent matches a child if they share the same prefix of projections. // The child may have more, if it is capturing sub-fields out of // something that is captured by-move in the parent closure. @@ -458,21 +458,13 @@ pub fn analyze_coroutine_closure_captures<'a, 'tcx: 'a, T>( (parent_field_idx, parent_capture), (child_field_idx, child_capture), ); - - field_used_at_least_once = true; } - - // Make sure the field was used at least once. - assert!( - field_used_at_least_once, - "we captured {parent_capture:#?} but it was not used in the child coroutine?" - ); } assert_eq!(child_captures.next(), None, "leftover child captures?"); } } -fn child_prefix_matches_parent_projections( +pub fn child_prefix_matches_parent_projections( parent_capture: &ty::CapturedPlace<'_>, child_capture: &ty::CapturedPlace<'_>, ) -> bool { diff --git a/compiler/rustc_middle/src/ty/mod.rs b/compiler/rustc_middle/src/ty/mod.rs index cc6a8619e1e74..3506b771eabf4 100644 --- a/compiler/rustc_middle/src/ty/mod.rs +++ b/compiler/rustc_middle/src/ty/mod.rs @@ -65,7 +65,7 @@ pub use self::closure::{ BorrowKind, CAPTURE_STRUCT_LOCAL, CaptureInfo, CapturedPlace, ClosureTypeInfo, MinCaptureInformationMap, MinCaptureList, RootVariableMinCaptureList, UpvarCapture, UpvarId, UpvarPath, analyze_coroutine_closure_captures, is_ancestor_or_same_capture, - place_to_string_for_capture, + child_prefix_matches_parent_projections, place_to_string_for_capture, }; pub use self::consts::{ AliasConst, AliasConstKind, AtomicOrdering, Const, ConstInt, ConstKind, ConstToValTreeResult, diff --git a/compiler/rustc_mir_transform/src/coroutine/by_move_body.rs b/compiler/rustc_mir_transform/src/coroutine/by_move_body.rs index 88ffe5861a697..6a66b9c1f9478 100644 --- a/compiler/rustc_mir_transform/src/coroutine/by_move_body.rs +++ b/compiler/rustc_mir_transform/src/coroutine/by_move_body.rs @@ -125,10 +125,20 @@ pub(crate) fn coroutine_by_move_body_def_id<'tcx>( .tupled_inputs_ty .tuple_fields() .len(); + let explicit_captures = + tcx.hir_node_by_def_id(coroutine_def_id).expect_closure().explicit_captures; let field_remapping: UnordMap<_, _> = ty::analyze_coroutine_closure_captures( tcx.closure_captures(parent_def_id).iter().copied(), - tcx.closure_captures(coroutine_def_id).iter().skip(num_args).copied(), + tcx.closure_captures(coroutine_def_id) + .iter() + .skip(num_args) + .filter(|capture| { + !explicit_captures + .iter() + .any(|explicit| explicit.var_hir_id == capture.get_root_variable()) + }) + .copied(), |(parent_field_idx, parent_capture), (child_field_idx, child_capture)| { // Store this set of additional projections (fields and derefs). // We need to re-apply them later. From 79bad7dbba961066cb8d372a8fb8e564f5523337 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 11 Jun 2026 13:07:29 +0900 Subject: [PATCH 17/32] update move-expression coroutine closure tests --- tests/ui/move-expr/async-closures.rs | 30 ++++++++++++++++--- tests/ui/move-expr/async-closures.stderr | 8 ----- tests/ui/move-expr/outside-plain-closure.rs | 2 +- .../ui/move-expr/outside-plain-closure.stderr | 2 +- tests/ui/move-expr/parse-ambiguity-errors.rs | 2 +- .../move-expr/parse-ambiguity-errors.stderr | 2 +- 6 files changed, 30 insertions(+), 16 deletions(-) delete mode 100644 tests/ui/move-expr/async-closures.stderr diff --git a/tests/ui/move-expr/async-closures.rs b/tests/ui/move-expr/async-closures.rs index eea93f02b807a..5f93915a9f877 100644 --- a/tests/ui/move-expr/async-closures.rs +++ b/tests/ui/move-expr/async-closures.rs @@ -1,11 +1,33 @@ //@ edition: 2021 +//@ run-pass #![allow(incomplete_features)] #![feature(move_expr)] +use std::cell::Cell; +use std::sync::Arc; + fn main() { - let s = String::from("hello"); - let _ = async || { - move(s); - //~^ ERROR `move(expr)` is only supported in plain closures + let created = Cell::new(0); + let c = async || { + let n = move({ + created.set(created.get() + 1); + created.get() + }); + n }; + assert_eq!(created.get(), 0); + let fut = c(); + assert_eq!(created.get(), 1); + drop(fut); + + let x = Arc::new(String::from("hello")); + assert_eq!(Arc::strong_count(&x), 1); + + let c = async || move(x.clone()); + assert_eq!(Arc::strong_count(&x), 1); + let fut = c(); + assert_eq!(Arc::strong_count(&x), 2); + drop(fut); + assert_eq!(Arc::strong_count(&x), 1); + assert_eq!(Arc::strong_count(&x), 1); } diff --git a/tests/ui/move-expr/async-closures.stderr b/tests/ui/move-expr/async-closures.stderr deleted file mode 100644 index d0fd5c8ee7df0..0000000000000 --- a/tests/ui/move-expr/async-closures.stderr +++ /dev/null @@ -1,8 +0,0 @@ -error: `move(expr)` is only supported in plain closures - --> $DIR/async-closures.rs:8:9 - | -LL | move(s); - | ^^^^ - -error: aborting due to 1 previous error - diff --git a/tests/ui/move-expr/outside-plain-closure.rs b/tests/ui/move-expr/outside-plain-closure.rs index c4aa6551119fe..64bf8374d92d0 100644 --- a/tests/ui/move-expr/outside-plain-closure.rs +++ b/tests/ui/move-expr/outside-plain-closure.rs @@ -3,5 +3,5 @@ fn main() { let _ = move(String::from("nope")); - //~^ ERROR `move(expr)` is only supported in plain closures + //~^ ERROR `move(expr)` is only supported in closures } diff --git a/tests/ui/move-expr/outside-plain-closure.stderr b/tests/ui/move-expr/outside-plain-closure.stderr index 68c4223641304..c84d71a3579c3 100644 --- a/tests/ui/move-expr/outside-plain-closure.stderr +++ b/tests/ui/move-expr/outside-plain-closure.stderr @@ -1,4 +1,4 @@ -error: `move(expr)` is only supported in plain closures +error: `move(expr)` is only supported in closures --> $DIR/outside-plain-closure.rs:5:13 | LL | let _ = move(String::from("nope")); diff --git a/tests/ui/move-expr/parse-ambiguity-errors.rs b/tests/ui/move-expr/parse-ambiguity-errors.rs index c2927373cb8a7..68b64d1582ff8 100644 --- a/tests/ui/move-expr/parse-ambiguity-errors.rs +++ b/tests/ui/move-expr/parse-ambiguity-errors.rs @@ -5,7 +5,7 @@ fn main() { let x: bool = true; let y: bool = true; let _ = move(x) || y; - //~^ ERROR `move(expr)` is only supported in plain closures + //~^ ERROR `move(expr)` is only supported in closures let x: bool = true; let y: bool = true; diff --git a/tests/ui/move-expr/parse-ambiguity-errors.stderr b/tests/ui/move-expr/parse-ambiguity-errors.stderr index c4dc929eac36c..5990bf861f609 100644 --- a/tests/ui/move-expr/parse-ambiguity-errors.stderr +++ b/tests/ui/move-expr/parse-ambiguity-errors.stderr @@ -4,7 +4,7 @@ error: expected one of `async`, `|`, or `||`, found `[` LL | let _ = move[x] || y; | ^ expected one of `async`, `|`, or `||` -error: `move(expr)` is only supported in plain closures +error: `move(expr)` is only supported in closures --> $DIR/parse-ambiguity-errors.rs:7:13 | LL | let _ = move(x) || y; From 24590c86f619e90146b80ade7fe5d4bd0fe81a93 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 11 Jun 2026 13:11:44 +0900 Subject: [PATCH 18/32] rustfmt --- compiler/rustc_ast_lowering/src/expr.rs | 25 +++++++++++++------------ compiler/rustc_hir_typeck/src/upvar.rs | 8 ++++---- compiler/rustc_middle/src/ty/mod.rs | 4 ++-- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/compiler/rustc_ast_lowering/src/expr.rs b/compiler/rustc_ast_lowering/src/expr.rs index 2d5f760114ec5..d849dc87275be 100644 --- a/compiler/rustc_ast_lowering/src/expr.rs +++ b/compiler/rustc_ast_lowering/src/expr.rs @@ -865,18 +865,19 @@ impl<'hir> LoweringContext<'_, 'hir> { (params, res) }); - let explicit_captures: &'hir [hir::ExplicitCapture] = - if let Some(move_expr_state) = self.move_expr_bindings.last().and_then(Option::as_ref) { - self.arena.alloc_from_iter(move_expr_state.occurrences.iter().filter_map( - |occurrence| { - occurrence - .explicit_capture - .then_some(hir::ExplicitCapture { var_hir_id: occurrence.binding }) - }, - )) - } else { - &[] - }; + let explicit_captures: &'hir [hir::ExplicitCapture] = if let Some(move_expr_state) = + self.move_expr_bindings.last().and_then(Option::as_ref) + { + self.arena.alloc_from_iter(move_expr_state.occurrences.iter().filter_map( + |occurrence| { + occurrence + .explicit_capture + .then_some(hir::ExplicitCapture { var_hir_id: occurrence.binding }) + }, + )) + } else { + &[] + }; // `static |<_task_context?>| -> { }`: hir::ExprKind::Closure(self.arena.alloc(hir::Closure { diff --git a/compiler/rustc_hir_typeck/src/upvar.rs b/compiler/rustc_hir_typeck/src/upvar.rs index 89b91094f50d2..c9dc0f33da8e2 100644 --- a/compiler/rustc_hir_typeck/src/upvar.rs +++ b/compiler/rustc_hir_typeck/src/upvar.rs @@ -535,14 +535,14 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> { // reborrow from the parent coroutine-closure env. self.tcx.lifetimes.re_erased } else { - let Some(parent_capture) = parent_captures.iter().copied().find( - |parent_capture| { + let Some(parent_capture) = + parent_captures.iter().copied().find(|parent_capture| { ty::child_prefix_matches_parent_projections( parent_capture, child_capture, ) - }, - ) else { + }) + else { bug!("child capture did not match a parent coroutine capture"); }; diff --git a/compiler/rustc_middle/src/ty/mod.rs b/compiler/rustc_middle/src/ty/mod.rs index 3506b771eabf4..9328bf44ee04f 100644 --- a/compiler/rustc_middle/src/ty/mod.rs +++ b/compiler/rustc_middle/src/ty/mod.rs @@ -64,8 +64,8 @@ pub use vtable::*; pub use self::closure::{ BorrowKind, CAPTURE_STRUCT_LOCAL, CaptureInfo, CapturedPlace, ClosureTypeInfo, MinCaptureInformationMap, MinCaptureList, RootVariableMinCaptureList, UpvarCapture, UpvarId, - UpvarPath, analyze_coroutine_closure_captures, is_ancestor_or_same_capture, - child_prefix_matches_parent_projections, place_to_string_for_capture, + UpvarPath, analyze_coroutine_closure_captures, child_prefix_matches_parent_projections, + is_ancestor_or_same_capture, place_to_string_for_capture, }; pub use self::consts::{ AliasConst, AliasConstKind, AtomicOrdering, Const, ConstInt, ConstKind, ConstToValTreeResult, From db7693a6ccf247fc288b0fbe7e55a3b2e1702651 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Tue, 23 Jun 2026 02:24:44 +0900 Subject: [PATCH 19/32] refactor move expr initializer wrapping --- compiler/rustc_ast_lowering/src/expr.rs | 134 +++++++++++++++--- .../rustc_ast_lowering/src/expr/closure.rs | 56 +------- 2 files changed, 115 insertions(+), 75 deletions(-) diff --git a/compiler/rustc_ast_lowering/src/expr.rs b/compiler/rustc_ast_lowering/src/expr.rs index d849dc87275be..2bf239545bd81 100644 --- a/compiler/rustc_ast_lowering/src/expr.rs +++ b/compiler/rustc_ast_lowering/src/expr.rs @@ -21,7 +21,7 @@ mod closure; use crate::diagnostics::{ AsyncCoroutinesNotSupported, AwaitOnlyInAsyncFnAndBlocks, FunctionalRecordUpdateDestructuringAssignment, InclusiveRangeWithNoEnd, - InvalidLegacyConstGenericArg, MatchArmWithNoBody, MoveExprOnlyInPlainClosures, + InvalidLegacyConstGenericArg, MatchArmWithNoBody, MoveExprOnlyInSupportedContexts, NeverPatternWithBody, NeverPatternWithGuard, UnderscoreExprLhsAssign, UseConstGenericArg, YieldInClosure, }; @@ -42,7 +42,7 @@ struct MoveExprInitializer<'a> { expr: &'a Expr, } -/// State for `move(...)` expressions found while lowering one plain closure body. +/// State for `move(...)` expressions found while lowering one closure-like body. pub(super) struct MoveExprState<'hir> { pub(super) bindings: NodeMap<(Ident, HirId)>, pub(super) occurrences: Vec>, @@ -73,6 +73,12 @@ impl<'a> MoveExprInitializerFinder<'a> { this.visit_expr(expr); this.initializers } + + fn collect_block(block: &'a Block) -> Vec> { + let mut this = Self { initializers: Vec::new() }; + this.visit_block(block); + this.initializers + } } impl<'a> Visitor<'a> for MoveExprInitializerFinder<'a> { @@ -145,13 +151,88 @@ impl<'hir> LoweringContext<'_, 'hir> { let (pat, binding) = self.pat_ident(inner.span, ident); let Some(state) = self.move_expr_bindings.last_mut().and_then(|state| state.as_mut()) else { - span_bug!(move_kw_span, "`move(...)` lowered without a plain closure body state"); + span_bug!(move_kw_span, "`move(...)` lowered without a closure-like body state"); }; state.bindings.insert(id, (ident, binding)); state.occurrences.push(MoveExprOccurrence { id, ident, pat, binding, explicit_capture }); (ident, binding) } + fn lower_expr_with_move_exprs( + &mut self, + expr: hir::Expr<'hir>, + move_expr_state: MoveExprState<'hir>, + body: &Expr, + whole_span: Span, + ) -> hir::Expr<'hir> { + let initializers = MoveExprInitializerFinder::collect(body); + self.lower_expr_with_move_expr_initializers(expr, move_expr_state, initializers, whole_span) + } + + fn lower_expr_with_move_exprs_in_block( + &mut self, + expr: hir::Expr<'hir>, + move_expr_state: MoveExprState<'hir>, + body: &Block, + whole_span: Span, + ) -> hir::Expr<'hir> { + let initializers = MoveExprInitializerFinder::collect_block(body); + self.lower_expr_with_move_expr_initializers(expr, move_expr_state, initializers, whole_span) + } + + fn lower_expr_with_move_expr_initializers( + &mut self, + expr: hir::Expr<'hir>, + move_expr_state: MoveExprState<'hir>, + initializers: Vec>, + whole_span: Span, + ) -> hir::Expr<'hir> { + if move_expr_state.occurrences.is_empty() { + return expr; + } + + let initializers = initializers + .into_iter() + .map(|initializer| (initializer.id, initializer.expr)) + .collect::>(); + let mut stmts = Vec::with_capacity(move_expr_state.occurrences.len()); + let mut initializer_bindings = NodeMap::default(); + for occurrence in &move_expr_state.occurrences { + // Evaluate the expression inside `move(...)` before creating the + // closure/coroutine and store it in a synthetic local: + // `|| move(foo).bar` becomes roughly + // `let __move_expr_0 = foo; || __move_expr_0.bar`. + let expr = initializers[&occurrence.id]; + let init = if initializer_bindings.is_empty() { + self.lower_expr(expr) + } else { + // Earlier entries cover nested `move(...)` expressions that + // appear inside this initializer, as in + // `move(move(foo.clone()))`. + let (init, _) = self.with_move_expr_bindings( + Some(MoveExprState { + bindings: initializer_bindings.clone(), + occurrences: Vec::new(), + }), + |this| this.lower_expr(expr), + ); + init + }; + stmts.push(self.stmt_let_pat( + None, + expr.span, + Some(init), + occurrence.pat, + hir::LocalSource::Normal, + )); + initializer_bindings.insert(occurrence.id, (occurrence.ident, occurrence.binding)); + } + + let stmts = self.arena.alloc_from_iter(stmts); + let block = self.block_all(whole_span, stmts, Some(self.arena.alloc(expr))); + self.expr(whole_span, hir::ExprKind::Block(block, None)) + } + fn lower_exprs(&mut self, exprs: &[Box]) -> &'hir [hir::Expr<'hir>] { self.arena.alloc_from_iter(exprs.iter().map(|x| self.lower_expr_mut(x))) } @@ -334,8 +415,9 @@ impl<'hir> LoweringContext<'_, 'hir> { }), )) } else { - let guar = - self.dcx().emit_err(MoveExprOnlyInPlainClosures { span: *move_kw_span }); + let guar = self + .dcx() + .emit_err(MoveExprOnlyInSupportedContexts { span: *move_kw_span }); hir::ExprKind::Err(guar) } } @@ -346,22 +428,34 @@ impl<'hir> LoweringContext<'_, 'hir> { CoroutineKind::Gen => hir::CoroutineDesugaring::Gen, CoroutineKind::AsyncGen => hir::CoroutineDesugaring::AsyncGen, }; - self.make_desugared_coroutine_expr( - *capture_clause, - e.id, - None, - *decl_span, + let (kind, move_expr_state) = + self.with_move_expr_bindings(Some(MoveExprState::default()), |this| { + this.make_desugared_coroutine_expr( + *capture_clause, + e.id, + None, + *decl_span, + e.span, + desugaring_kind, + hir::CoroutineSource::Block, + |this| { + this.with_new_scopes(e.span, |this| this.lower_block_expr(block)) + }, + ) + }); + let Some(move_expr_state) = move_expr_state else { + span_bug!( + *decl_span, + "coroutine block lowering did not return `move(...)` state" + ); + }; + let expr = hir::Expr { hir_id: expr_hir_id, kind, span }; + return self.lower_expr_with_move_exprs_in_block( + expr, + move_expr_state, + block, e.span, - desugaring_kind, - hir::CoroutineSource::Block, - |this| { - this.with_new_scopes(e.span, |this| { - let (expr, _) = this - .with_move_expr_bindings(None, |this| this.lower_block_expr(block)); - expr - }) - }, - ) + ); } ExprKind::Block(blk, opt_label) => { // Different from loops, label of block resolves to block id rather than diff --git a/compiler/rustc_ast_lowering/src/expr/closure.rs b/compiler/rustc_ast_lowering/src/expr/closure.rs index 95f54ba281e17..a929ebad3302f 100644 --- a/compiler/rustc_ast_lowering/src/expr/closure.rs +++ b/compiler/rustc_ast_lowering/src/expr/closure.rs @@ -1,11 +1,10 @@ -use rustc_ast::node_id::NodeMap; use rustc_ast::*; use rustc_hir as hir; use rustc_hir::{HirId, Target, find_attr}; use rustc_middle::span_bug; use rustc_span::Span; -use super::{LoweringContext, MoveExprInitializerFinder, MoveExprState}; +use super::{LoweringContext, MoveExprState}; use crate::FnDeclKind; use crate::diagnostics::{ClosureCannotBeStatic, CoroutineTooManyParameters}; @@ -124,59 +123,6 @@ impl<'hir> LoweringContext<'_, 'hir> { self.lower_expr_with_move_exprs(closure_expr, move_expr_state, body, whole_span) } - fn lower_expr_with_move_exprs( - &mut self, - expr: hir::Expr<'hir>, - move_expr_state: MoveExprState<'hir>, - body: &Expr, - whole_span: Span, - ) -> hir::Expr<'hir> { - if move_expr_state.occurrences.is_empty() { - return expr; - } - - let initializers = MoveExprInitializerFinder::collect(body) - .into_iter() - .map(|initializer| (initializer.id, initializer.expr)) - .collect::>(); - let mut stmts = Vec::with_capacity(move_expr_state.occurrences.len()); - let mut initializer_bindings = NodeMap::default(); - for occurrence in &move_expr_state.occurrences { - // Evaluate the expression inside `move(...)` before creating the - // closure and store it in a synthetic local: - // `|| move(foo).bar` becomes roughly - // `let __move_expr_0 = foo; || __move_expr_0.bar`. - let expr = initializers[&occurrence.id]; - let init = if initializer_bindings.is_empty() { - self.lower_expr(expr) - } else { - // Earlier entries cover nested `move(...)` expressions that - // appear inside this initializer, as in - // `move(move(foo.clone()))`. - let (init, _) = self.with_move_expr_bindings( - Some(MoveExprState { - bindings: initializer_bindings.clone(), - occurrences: Vec::new(), - }), - |this| this.lower_expr(expr), - ); - init - }; - stmts.push(self.stmt_let_pat( - None, - expr.span, - Some(init), - occurrence.pat, - hir::LocalSource::Normal, - )); - initializer_bindings.insert(occurrence.id, (occurrence.ident, occurrence.binding)); - } - - let stmts = self.arena.alloc_from_iter(stmts); - let block = self.block_all(whole_span, stmts, Some(self.arena.alloc(expr))); - self.expr(whole_span, hir::ExprKind::Block(block, None)) - } - // Lowers the actual plain closure node and body. The body is lowered while a // `MoveExprState` is active, so `move(...)` occurrences become synthetic // local uses and the caller can later add the matching initializers. From f5c36e81abbcc9b7fb2faf0465908d1cad3ba0be Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Tue, 23 Jun 2026 02:25:28 +0900 Subject: [PATCH 20/32] support move expr in coroutine blocks --- compiler/rustc_ast_lowering/src/diagnostics.rs | 4 ++-- tests/ui/move-expr/outside-plain-closure.rs | 2 +- tests/ui/move-expr/outside-plain-closure.stderr | 2 +- tests/ui/move-expr/parse-ambiguity-errors.rs | 2 +- tests/ui/move-expr/parse-ambiguity-errors.stderr | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/compiler/rustc_ast_lowering/src/diagnostics.rs b/compiler/rustc_ast_lowering/src/diagnostics.rs index aa8550f9b99ed..b18359d9b14da 100644 --- a/compiler/rustc_ast_lowering/src/diagnostics.rs +++ b/compiler/rustc_ast_lowering/src/diagnostics.rs @@ -148,8 +148,8 @@ pub(crate) struct ClosureCannotBeStatic { } #[derive(Diagnostic)] -#[diag("`move(expr)` is only supported in closures")] -pub(crate) struct MoveExprOnlyInPlainClosures { +#[diag("`move(expr)` is only supported in closures, `async`, `gen`, and `async gen` blocks")] +pub(crate) struct MoveExprOnlyInSupportedContexts { #[primary_span] pub span: Span, } diff --git a/tests/ui/move-expr/outside-plain-closure.rs b/tests/ui/move-expr/outside-plain-closure.rs index 64bf8374d92d0..881c00d32aa11 100644 --- a/tests/ui/move-expr/outside-plain-closure.rs +++ b/tests/ui/move-expr/outside-plain-closure.rs @@ -3,5 +3,5 @@ fn main() { let _ = move(String::from("nope")); - //~^ ERROR `move(expr)` is only supported in closures + //~^ ERROR `move(expr)` is only supported in closures, `async`, `gen`, and `async gen` blocks } diff --git a/tests/ui/move-expr/outside-plain-closure.stderr b/tests/ui/move-expr/outside-plain-closure.stderr index c84d71a3579c3..8654f52bf4ac4 100644 --- a/tests/ui/move-expr/outside-plain-closure.stderr +++ b/tests/ui/move-expr/outside-plain-closure.stderr @@ -1,4 +1,4 @@ -error: `move(expr)` is only supported in closures +error: `move(expr)` is only supported in closures, `async`, `gen`, and `async gen` blocks --> $DIR/outside-plain-closure.rs:5:13 | LL | let _ = move(String::from("nope")); diff --git a/tests/ui/move-expr/parse-ambiguity-errors.rs b/tests/ui/move-expr/parse-ambiguity-errors.rs index 68b64d1582ff8..c9428770538f7 100644 --- a/tests/ui/move-expr/parse-ambiguity-errors.rs +++ b/tests/ui/move-expr/parse-ambiguity-errors.rs @@ -5,7 +5,7 @@ fn main() { let x: bool = true; let y: bool = true; let _ = move(x) || y; - //~^ ERROR `move(expr)` is only supported in closures + //~^ ERROR `move(expr)` is only supported in closures, `async`, `gen`, and `async gen` blocks let x: bool = true; let y: bool = true; diff --git a/tests/ui/move-expr/parse-ambiguity-errors.stderr b/tests/ui/move-expr/parse-ambiguity-errors.stderr index 5990bf861f609..17a397cc26900 100644 --- a/tests/ui/move-expr/parse-ambiguity-errors.stderr +++ b/tests/ui/move-expr/parse-ambiguity-errors.stderr @@ -4,7 +4,7 @@ error: expected one of `async`, `|`, or `||`, found `[` LL | let _ = move[x] || y; | ^ expected one of `async`, `|`, or `||` -error: `move(expr)` is only supported in closures +error: `move(expr)` is only supported in closures, `async`, `gen`, and `async gen` blocks --> $DIR/parse-ambiguity-errors.rs:7:13 | LL | let _ = move(x) || y; From 7541f068053593aaca20ae0f02f5d7a2972a0dd0 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Tue, 23 Jun 2026 02:25:54 +0900 Subject: [PATCH 21/32] add coroutine block move expr tests --- tests/ui/move-expr/async-blocks.rs | 41 ++++++++++ tests/ui/move-expr/async-gen-blocks.rs | 100 +++++++++++++++++++++++++ tests/ui/move-expr/gen-blocks.rs | 58 ++++++++++++++ 3 files changed, 199 insertions(+) create mode 100644 tests/ui/move-expr/async-blocks.rs create mode 100644 tests/ui/move-expr/async-gen-blocks.rs create mode 100644 tests/ui/move-expr/gen-blocks.rs diff --git a/tests/ui/move-expr/async-blocks.rs b/tests/ui/move-expr/async-blocks.rs new file mode 100644 index 0000000000000..e6bee3b3c6c06 --- /dev/null +++ b/tests/ui/move-expr/async-blocks.rs @@ -0,0 +1,41 @@ +//@ edition: 2021 +//@ run-pass +#![allow(incomplete_features)] +#![feature(move_expr)] + +use std::cell::Cell; +use std::sync::Arc; + +fn main() { + let created = Cell::new(0); + let fut = async { + let n = move({ + created.set(created.get() + 1); + created.get() + }); + n + }; + assert_eq!(created.get(), 1); + drop(fut); + + let x = Arc::new(String::from("hello")); + assert_eq!(Arc::strong_count(&x), 1); + let fut = async { move(x.clone()) }; + assert_eq!(Arc::strong_count(&x), 2); + drop(fut); + assert_eq!(Arc::strong_count(&x), 1); + + let y = Arc::new(String::from("nested")); + assert_eq!(Arc::strong_count(&y), 1); + let fut = async { move(move(y.clone())) }; + assert_eq!(Arc::strong_count(&y), 2); + drop(fut); + assert_eq!(Arc::strong_count(&y), 1); + + let z = Arc::new(String::from("async move")); + assert_eq!(Arc::strong_count(&z), 1); + let fut = async move { move(z.clone()) }; + assert_eq!(Arc::strong_count(&z), 2); + drop(fut); + assert_eq!(Arc::strong_count(&z), 1); +} diff --git a/tests/ui/move-expr/async-gen-blocks.rs b/tests/ui/move-expr/async-gen-blocks.rs new file mode 100644 index 0000000000000..4037b276ebcb7 --- /dev/null +++ b/tests/ui/move-expr/async-gen-blocks.rs @@ -0,0 +1,100 @@ +//@ edition: 2024 +//@ run-pass +#![allow(incomplete_features)] +#![feature(async_iterator, gen_blocks, move_expr)] + +use std::async_iter::AsyncIterator; +use std::cell::Cell; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll, Waker}; + +struct PendingOnce { + pending: bool, +} + +impl PendingOnce { + fn new() -> Self { + Self { pending: true } + } +} + +impl Future for PendingOnce { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { + if self.pending { + self.pending = false; + cx.waker().wake_by_ref(); + Poll::Pending + } else { + Poll::Ready(()) + } + } +} + +fn poll_next(iter: Pin<&mut I>) -> Poll> { + let cx = &mut Context::from_waker(Waker::noop()); + AsyncIterator::poll_next(iter, cx) +} + +fn ready_next(iter: Pin<&mut I>) -> Option { + match poll_next(iter) { + Poll::Ready(item) => item, + Poll::Pending => panic!("async iterator unexpectedly returned pending"), + } +} + +fn main() { + let created = Cell::new(0); + let mut iter = Box::pin(async gen { + let n = move({ + created.set(created.get() + 1); + created.get() + }); + yield n; + yield n + 1; + }); + assert_eq!(created.get(), 1); + assert_eq!(ready_next(iter.as_mut()), Some(1)); + assert_eq!(ready_next(iter.as_mut()), Some(2)); + assert_eq!(ready_next(iter.as_mut()), None); + + let x = Arc::new(String::from("hello")); + assert_eq!(Arc::strong_count(&x), 1); + let mut iter = Box::pin(async gen { + let value = move(x.clone()); + yield Arc::strong_count(&value); + PendingOnce::new().await; + yield Arc::strong_count(&value); + }); + assert_eq!(Arc::strong_count(&x), 2); + assert_eq!(ready_next(iter.as_mut()), Some(2)); + assert!(matches!(poll_next(iter.as_mut()), Poll::Pending)); + assert_eq!(ready_next(iter.as_mut()), Some(2)); + drop(iter); + assert_eq!(Arc::strong_count(&x), 1); + + let y = Arc::new(String::from("nested")); + assert_eq!(Arc::strong_count(&y), 1); + let mut iter = Box::pin(async gen { + let value = move(move(y.clone())); + yield Arc::strong_count(&value); + }); + assert_eq!(Arc::strong_count(&y), 2); + assert_eq!(ready_next(iter.as_mut()), Some(2)); + drop(iter); + assert_eq!(Arc::strong_count(&y), 1); + + let z = Arc::new(String::from("async gen move")); + assert_eq!(Arc::strong_count(&z), 1); + let mut iter = Box::pin(async gen move { + let value = move(z.clone()); + yield Arc::strong_count(&value); + }); + assert_eq!(Arc::strong_count(&z), 2); + assert_eq!(ready_next(iter.as_mut()), Some(2)); + drop(iter); + assert_eq!(Arc::strong_count(&z), 1); +} diff --git a/tests/ui/move-expr/gen-blocks.rs b/tests/ui/move-expr/gen-blocks.rs new file mode 100644 index 0000000000000..c6cec6491cc6e --- /dev/null +++ b/tests/ui/move-expr/gen-blocks.rs @@ -0,0 +1,58 @@ +//@ edition: 2024 +//@ run-pass +#![allow(incomplete_features)] +#![feature(gen_blocks, move_expr)] + +use std::cell::Cell; +use std::sync::Arc; + +fn main() { + let created = Cell::new(0); + let mut iter = gen { + let n = move({ + created.set(created.get() + 1); + created.get() + }); + yield n; + yield n + 1; + }; + assert_eq!(created.get(), 1); + assert_eq!(iter.next(), Some(1)); + assert_eq!(iter.next(), Some(2)); + assert_eq!(iter.next(), None); + + let x = Arc::new(String::from("hello")); + assert_eq!(Arc::strong_count(&x), 1); + let mut iter = gen { + let value = move(x.clone()); + yield Arc::strong_count(&value); + yield Arc::strong_count(&value); + }; + assert_eq!(Arc::strong_count(&x), 2); + assert_eq!(iter.next(), Some(2)); + assert_eq!(iter.next(), Some(2)); + drop(iter); + assert_eq!(Arc::strong_count(&x), 1); + + let y = Arc::new(String::from("nested")); + assert_eq!(Arc::strong_count(&y), 1); + let mut iter = gen { + let value = move(move(y.clone())); + yield Arc::strong_count(&value); + }; + assert_eq!(Arc::strong_count(&y), 2); + assert_eq!(iter.next(), Some(2)); + drop(iter); + assert_eq!(Arc::strong_count(&y), 1); + + let z = Arc::new(String::from("gen move")); + assert_eq!(Arc::strong_count(&z), 1); + let mut iter = gen move { + let value = move(z.clone()); + yield Arc::strong_count(&value); + }; + assert_eq!(Arc::strong_count(&z), 2); + assert_eq!(iter.next(), Some(2)); + drop(iter); + assert_eq!(Arc::strong_count(&z), 1); +} From e831c5324f3047afc34a35e3093419b1f96e4192 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Wed, 12 Aug 2026 18:56:06 +0900 Subject: [PATCH 22/32] fix nested move expression lowering across capture contexts --- compiler/rustc_ast_lowering/src/expr.rs | 82 +++---------- .../rustc_ast_lowering/src/expr/closure.rs | 113 ++++++++++++------ compiler/rustc_ast_lowering/src/lib.rs | 2 +- 3 files changed, 93 insertions(+), 104 deletions(-) diff --git a/compiler/rustc_ast_lowering/src/expr.rs b/compiler/rustc_ast_lowering/src/expr.rs index 2bf239545bd81..faae4f98ae854 100644 --- a/compiler/rustc_ast_lowering/src/expr.rs +++ b/compiler/rustc_ast_lowering/src/expr.rs @@ -36,30 +36,20 @@ pub(super) struct WillCreateDefIdsVisitor; struct MoveExprInitializer<'a> { /// The `NodeId` of the outer `move(...)` expression. id: NodeId, - /// Span of the `move` token, used for the generated binding name. - move_kw_span: Span, /// The expression inside `move(...)`; e.g. `foo.bar` in `move(foo.bar)`. expr: &'a Expr, } /// State for `move(...)` expressions found while lowering one closure-like body. +#[derive(Default)] pub(super) struct MoveExprState<'hir> { - pub(super) bindings: NodeMap<(Ident, HirId)>, pub(super) occurrences: Vec>, } -impl<'hir> Default for MoveExprState<'hir> { - fn default() -> Self { - Self { bindings: NodeMap::default(), occurrences: Vec::new() } - } -} - pub(super) struct MoveExprOccurrence<'hir> { id: NodeId, - ident: Ident, pat: &'hir hir::Pat<'hir>, binding: HirId, - explicit_capture: bool, } /// Looks up the initializer expression for each `move(...)` occurrence. @@ -84,15 +74,11 @@ impl<'a> MoveExprInitializerFinder<'a> { impl<'a> Visitor<'a> for MoveExprInitializerFinder<'a> { fn visit_expr(&mut self, expr: &'a Expr) { match &expr.kind { - ExprKind::Move(inner, move_kw_span) => { + ExprKind::Move(inner, _) => { self.visit_expr(inner); - self.initializers.push(MoveExprInitializer { - id: expr.id, - move_kw_span: *move_kw_span, - expr: inner, - }); + self.initializers.push(MoveExprInitializer { id: expr.id, expr: inner }); } - ExprKind::Closure(..) | ExprKind::Gen(..) | ExprKind::ConstBlock(..) => {} + ExprKind::ConstBlock(..) => {} _ => walk_expr(self, expr), } } @@ -135,13 +121,7 @@ impl<'hir> LoweringContext<'_, 'hir> { (result, state) } - fn record_move_expr( - &mut self, - id: NodeId, - inner: &Expr, - move_kw_span: Span, - explicit_capture: bool, - ) -> (Ident, HirId) { + fn record_move_expr(&mut self, id: NodeId, inner: &Expr, move_kw_span: Span) -> (Ident, HirId) { let index = self .move_expr_bindings .last() @@ -153,8 +133,7 @@ impl<'hir> LoweringContext<'_, 'hir> { else { span_bug!(move_kw_span, "`move(...)` lowered without a closure-like body state"); }; - state.bindings.insert(id, (ident, binding)); - state.occurrences.push(MoveExprOccurrence { id, ident, pat, binding, explicit_capture }); + state.occurrences.push(MoveExprOccurrence { id, pat, binding }); (ident, binding) } @@ -196,28 +175,16 @@ impl<'hir> LoweringContext<'_, 'hir> { .map(|initializer| (initializer.id, initializer.expr)) .collect::>(); let mut stmts = Vec::with_capacity(move_expr_state.occurrences.len()); - let mut initializer_bindings = NodeMap::default(); for occurrence in &move_expr_state.occurrences { // Evaluate the expression inside `move(...)` before creating the // closure/coroutine and store it in a synthetic local: // `|| move(foo).bar` becomes roughly // `let __move_expr_0 = foo; || __move_expr_0.bar`. let expr = initializers[&occurrence.id]; - let init = if initializer_bindings.is_empty() { - self.lower_expr(expr) - } else { - // Earlier entries cover nested `move(...)` expressions that - // appear inside this initializer, as in - // `move(move(foo.clone()))`. - let (init, _) = self.with_move_expr_bindings( - Some(MoveExprState { - bindings: initializer_bindings.clone(), - occurrences: Vec::new(), - }), - |this| this.lower_expr(expr), - ); - init - }; + // This state has already been popped, so a nested `move(...)` in + // the initializer is recorded by the immediately enclosing + // closure-like body instead of this one. + let init = self.lower_expr(expr); stmts.push(self.stmt_let_pat( None, expr.span, @@ -225,7 +192,6 @@ impl<'hir> LoweringContext<'_, 'hir> { occurrence.pat, hir::LocalSource::Normal, )); - initializer_bindings.insert(occurrence.id, (occurrence.ident, occurrence.binding)); } let stmts = self.arena.alloc_from_iter(stmts); @@ -386,19 +352,8 @@ impl<'hir> LoweringContext<'_, 'hir> { if !self.tcx.features().move_expr() { return self.expr_err(*move_kw_span, self.dcx().has_errors().unwrap()); } - if let Some(state) = self.move_expr_bindings.last().and_then(Option::as_ref) { - let existing = state.bindings.get(&e.id).copied(); - let (ident, binding) = existing.unwrap_or_else(|| { - for nested in MoveExprInitializerFinder::collect(inner) { - self.record_move_expr( - nested.id, - nested.expr, - nested.move_kw_span, - false, - ); - } - self.record_move_expr(e.id, inner, *move_kw_span, true) - }); + if self.move_expr_bindings.last().is_some_and(Option::is_some) { + let (ident, binding) = self.record_move_expr(e.id, inner, *move_kw_span); hir::ExprKind::Path(hir::QPath::Resolved( None, self.arena.alloc(hir::Path { @@ -962,13 +917,12 @@ impl<'hir> LoweringContext<'_, 'hir> { let explicit_captures: &'hir [hir::ExplicitCapture] = if let Some(move_expr_state) = self.move_expr_bindings.last().and_then(Option::as_ref) { - self.arena.alloc_from_iter(move_expr_state.occurrences.iter().filter_map( - |occurrence| { - occurrence - .explicit_capture - .then_some(hir::ExplicitCapture { var_hir_id: occurrence.binding }) - }, - )) + self.arena.alloc_from_iter( + move_expr_state + .occurrences + .iter() + .map(|occurrence| hir::ExplicitCapture { var_hir_id: occurrence.binding }), + ) } else { &[] }; diff --git a/compiler/rustc_ast_lowering/src/expr/closure.rs b/compiler/rustc_ast_lowering/src/expr/closure.rs index a929ebad3302f..3d28b5e92ad3f 100644 --- a/compiler/rustc_ast_lowering/src/expr/closure.rs +++ b/compiler/rustc_ast_lowering/src/expr/closure.rs @@ -22,23 +22,20 @@ impl<'hir> LoweringContext<'_, 'hir> { let attrs = self.lower_attrs(expr_hir_id, &e.attrs, e.span, Target::from_expr(e)); match closure.coroutine_marker { - Some(coroutine_marker) => hir::Expr { - hir_id: expr_hir_id, - kind: self.lower_expr_coroutine_closure( - &closure.binder, - closure.capture_clause, - e.id, - expr_hir_id, - coroutine_marker, - closure.constness, - &closure.fn_decl, - &closure.body, - closure.fn_decl_span, - closure.fn_arg_span, - attrs, - ), - span: self.lower_span(e.span), - }, + Some(coroutine_marker) => self.lower_expr_coroutine_closure_with_move_exprs( + expr_hir_id, + attrs, + &closure.binder, + closure.capture_clause, + e.id, + coroutine_marker, + closure.constness, + &closure.fn_decl, + &closure.body, + closure.fn_decl_span, + closure.fn_arg_span, + e.span, + ), None => self.lower_expr_plain_closure_with_move_exprs( expr_hir_id, attrs, @@ -56,6 +53,46 @@ impl<'hir> LoweringContext<'_, 'hir> { } } + fn lower_expr_coroutine_closure_with_move_exprs( + &mut self, + expr_hir_id: HirId, + attrs: &[hir::Attribute], + binder: &ClosureBinder, + capture_clause: CaptureBy, + closure_id: NodeId, + coroutine_marker: CoroutineMarker, + constness: Const, + decl: &FnDecl, + body: &Expr, + fn_decl_span: Span, + fn_arg_span: Span, + whole_span: Span, + ) -> hir::Expr<'hir> { + let (kind, move_expr_state) = + self.with_move_expr_bindings(Some(MoveExprState::default()), |this| { + this.lower_expr_coroutine_closure( + binder, + capture_clause, + closure_id, + expr_hir_id, + coroutine_marker, + constness, + decl, + body, + fn_decl_span, + fn_arg_span, + attrs, + ) + }); + let Some(move_expr_state) = move_expr_state else { + span_bug!(fn_decl_span, "coroutine closure lowering did not return `move(...)` state"); + }; + let closure_expr = + hir::Expr { hir_id: expr_hir_id, kind, span: self.lower_span(whole_span) }; + + self.lower_expr_with_move_exprs(closure_expr, move_expr_state, body, whole_span) + } + /// Lowers a plain closure expression and wraps it in an outer block if the /// closure body used `move(...)`. /// @@ -64,28 +101,18 @@ impl<'hir> LoweringContext<'_, 'hir> { /// lower each `move(...)` occurrence as a use of the synthetic local that /// will be introduced by that outer block. For example: /// - /// ```ignore (illustrative) - /// || (move(move(foo.clone()))).len() - /// ``` - /// - /// first lowers the closure body roughly as `|| __move_expr_1.len()` while - /// recording two occurrences: - /// - /// ```ignore (illustrative) - /// move(foo.clone()) -> __move_expr_0 - /// move(move(foo.clone())) -> __move_expr_1 - /// ``` - /// - /// This method then lowers the recorded initializers in order and builds the - /// surrounding block: + /// For example, `|| move(foo.clone()).len()` becomes roughly: /// /// ```ignore (illustrative) /// { /// let __move_expr_0 = foo.clone(); - /// let __move_expr_1 = __move_expr_0; - /// || __move_expr_1.len() + /// || __move_expr_0.len() /// } /// ``` + /// + /// If the initializer contains another `move(...)`, it is lowered after + /// this closure's state is popped and therefore belongs to the immediately + /// enclosing closure-like body. fn lower_expr_plain_closure_with_move_exprs( &mut self, expr_hir_id: HirId, @@ -170,11 +197,10 @@ impl<'hir> LoweringContext<'_, 'hir> { span_bug!(fn_decl_span, "plain closure lowering did not return `move(...)` state"); }; let explicit_captures: &'hir [hir::ExplicitCapture] = self.arena.alloc_from_iter( - move_expr_state.occurrences.iter().filter_map(|occurrence| { - occurrence - .explicit_capture - .then_some(hir::ExplicitCapture { var_hir_id: occurrence.binding }) - }), + move_expr_state + .occurrences + .iter() + .map(|occurrence| hir::ExplicitCapture { var_hir_id: occurrence.binding }), ); let bound_generic_params = self.lower_lifetime_binder(closure_id, generic_params); @@ -322,6 +348,15 @@ impl<'hir> LoweringContext<'_, 'hir> { self.dcx().span_err(span, "const coroutines are not supported"); } + let explicit_captures: &'hir [hir::ExplicitCapture] = self.arena.alloc_from_iter( + self.move_expr_bindings + .last() + .and_then(Option::as_ref) + .into_iter() + .flat_map(|state| &state.occurrences) + .map(|occurrence| hir::ExplicitCapture { var_hir_id: occurrence.binding }), + ); + let c = self.arena.alloc(hir::Closure { def_id: closure_def_id, binder: binder_clause, @@ -336,7 +371,7 @@ impl<'hir> LoweringContext<'_, 'hir> { // "coroutine that returns &str", rather than directly returning a `&str`. kind: hir::ClosureKind::CoroutineClosure(coroutine_desugaring), constness: self.lower_constness(attrs, constness), - explicit_captures: &[], + explicit_captures, }); hir::ExprKind::Closure(c) } diff --git a/compiler/rustc_ast_lowering/src/lib.rs b/compiler/rustc_ast_lowering/src/lib.rs index ef6995d9c11d6..f5dc9d5203c13 100644 --- a/compiler/rustc_ast_lowering/src/lib.rs +++ b/compiler/rustc_ast_lowering/src/lib.rs @@ -320,7 +320,7 @@ struct LoweringContext<'a, 'hir> { allow_for_await: Arc<[Symbol]>, allow_async_fn_traits: Arc<[Symbol]>, - /// Stack of `move(...)` collection states. A plain closure body pushes + /// Stack of `move(...)` collection states. A closure-like body pushes /// `Some`, so `move(...)` expressions can record the generated locals they /// should lower to. Nested bodies that cannot use `move(...)` push `None`. move_expr_bindings: Vec>>, From b3d7ec7492f1aa8916328b4f7a37d5315d3cf6f8 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Wed, 12 Aug 2026 18:57:03 +0900 Subject: [PATCH 23/32] improve diagnostics for exhausted nested move expressions --- .../rustc_ast_lowering/src/diagnostics.rs | 9 +++++++++ compiler/rustc_ast_lowering/src/expr.rs | 20 ++++++++++++++++--- compiler/rustc_ast_lowering/src/lib.rs | 4 ++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/compiler/rustc_ast_lowering/src/diagnostics.rs b/compiler/rustc_ast_lowering/src/diagnostics.rs index b18359d9b14da..2a468cb60546d 100644 --- a/compiler/rustc_ast_lowering/src/diagnostics.rs +++ b/compiler/rustc_ast_lowering/src/diagnostics.rs @@ -154,6 +154,15 @@ pub(crate) struct MoveExprOnlyInSupportedContexts { pub span: Span, } +#[derive(Diagnostic)] +#[diag( + "nested `move(expr)` requires another enclosing closure, `async`, `gen`, or `async gen` block" +)] +pub(crate) struct NestedMoveExprWithoutEnclosingContext { + #[primary_span] + pub span: Span, +} + #[derive(Diagnostic)] #[diag("functional record updates are not allowed in destructuring assignments")] pub(crate) struct FunctionalRecordUpdateDestructuringAssignment { diff --git a/compiler/rustc_ast_lowering/src/expr.rs b/compiler/rustc_ast_lowering/src/expr.rs index faae4f98ae854..00176fa9886ee 100644 --- a/compiler/rustc_ast_lowering/src/expr.rs +++ b/compiler/rustc_ast_lowering/src/expr.rs @@ -22,8 +22,8 @@ use crate::diagnostics::{ AsyncCoroutinesNotSupported, AwaitOnlyInAsyncFnAndBlocks, FunctionalRecordUpdateDestructuringAssignment, InclusiveRangeWithNoEnd, InvalidLegacyConstGenericArg, MatchArmWithNoBody, MoveExprOnlyInSupportedContexts, - NeverPatternWithBody, NeverPatternWithGuard, UnderscoreExprLhsAssign, UseConstGenericArg, - YieldInClosure, + NestedMoveExprWithoutEnclosingContext, NeverPatternWithBody, NeverPatternWithGuard, + UnderscoreExprLhsAssign, UseConstGenericArg, YieldInClosure, }; use crate::{ AllowReturnTypeNotation, GenericArgsMode, ImplTraitContext, ImplTraitPosition, LoweringContext, @@ -121,6 +121,14 @@ impl<'hir> LoweringContext<'_, 'hir> { (result, state) } + fn with_move_expr_initializer(&mut self, f: impl FnOnce(&mut Self) -> T) -> T { + let old = self.lowering_move_expr_initializer; + self.lowering_move_expr_initializer = true; + let result = f(self); + self.lowering_move_expr_initializer = old; + result + } + fn record_move_expr(&mut self, id: NodeId, inner: &Expr, move_kw_span: Span) -> (Ident, HirId) { let index = self .move_expr_bindings @@ -184,7 +192,7 @@ impl<'hir> LoweringContext<'_, 'hir> { // This state has already been popped, so a nested `move(...)` in // the initializer is recorded by the immediately enclosing // closure-like body instead of this one. - let init = self.lower_expr(expr); + let init = self.with_move_expr_initializer(|this| this.lower_expr(expr)); stmts.push(self.stmt_let_pat( None, expr.span, @@ -369,6 +377,12 @@ impl<'hir> LoweringContext<'_, 'hir> { ], }), )) + } else if self.lowering_move_expr_initializer && self.move_expr_bindings.is_empty() + { + let guar = self + .dcx() + .emit_err(NestedMoveExprWithoutEnclosingContext { span: *move_kw_span }); + hir::ExprKind::Err(guar) } else { let guar = self .dcx() diff --git a/compiler/rustc_ast_lowering/src/lib.rs b/compiler/rustc_ast_lowering/src/lib.rs index f5dc9d5203c13..43eb398e38e56 100644 --- a/compiler/rustc_ast_lowering/src/lib.rs +++ b/compiler/rustc_ast_lowering/src/lib.rs @@ -325,6 +325,9 @@ struct LoweringContext<'a, 'hir> { /// should lower to. Nested bodies that cannot use `move(...)` push `None`. move_expr_bindings: Vec>>, + /// Whether an initializer for a recorded `move(...)` is currently being lowered. + lowering_move_expr_initializer: bool, + attribute_parser: AttributeParser<'hir>, } @@ -371,6 +374,7 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> { allow_async_iterator: [sym::gen_future, sym::async_iterator].into(), move_expr_bindings: Vec::new(), + lowering_move_expr_initializer: false, attribute_parser: AttributeParser::new( tcx.sess, tcx.features(), From 7fabedc462b3901f4c3147d0c5d2a9da473637d3 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Wed, 12 Aug 2026 18:58:50 +0900 Subject: [PATCH 24/32] add UI coverage for nested move expressions --- tests/ui/move-expr/async-blocks.rs | 48 ++++++++++++++++--- tests/ui/move-expr/async-closures.rs | 11 ++++- tests/ui/move-expr/async-gen-blocks.rs | 14 ++++-- tests/ui/move-expr/gen-blocks.rs | 14 ++++-- .../move-expr/nested-async-block-ownership.rs | 17 +++++++ .../nested-async-block-ownership.stderr | 23 +++++++++ tests/ui/move-expr/nested-move-exhausted.rs | 17 +++++++ .../ui/move-expr/nested-move-exhausted.stderr | 26 ++++++++++ tests/ui/move-expr/nested-move-expr.rs | 18 ++++--- 9 files changed, 165 insertions(+), 23 deletions(-) create mode 100644 tests/ui/move-expr/nested-async-block-ownership.rs create mode 100644 tests/ui/move-expr/nested-async-block-ownership.stderr create mode 100644 tests/ui/move-expr/nested-move-exhausted.rs create mode 100644 tests/ui/move-expr/nested-move-exhausted.stderr diff --git a/tests/ui/move-expr/async-blocks.rs b/tests/ui/move-expr/async-blocks.rs index e6bee3b3c6c06..4f211cc961572 100644 --- a/tests/ui/move-expr/async-blocks.rs +++ b/tests/ui/move-expr/async-blocks.rs @@ -4,7 +4,20 @@ #![feature(move_expr)] use std::cell::Cell; +use std::future::Future; use std::sync::Arc; +use std::task::{Context, Poll, Waker}; + +fn block_on(future: F) -> F::Output { + let mut future = Box::pin(future); + let cx = &mut Context::from_waker(Waker::noop()); + loop { + match future.as_mut().poll(cx) { + Poll::Ready(output) => return output, + Poll::Pending => {} + } + } +} fn main() { let created = Cell::new(0); @@ -25,12 +38,35 @@ fn main() { drop(fut); assert_eq!(Arc::strong_count(&x), 1); - let y = Arc::new(String::from("nested")); - assert_eq!(Arc::strong_count(&y), 1); - let fut = async { move(move(y.clone())) }; - assert_eq!(Arc::strong_count(&y), 2); - drop(fut); - assert_eq!(Arc::strong_count(&y), 1); + let y = Arc::new(String::from("nested once")); + let weak = Arc::downgrade(&y); + let fut = async { + let inner = async { + drop(move(y.clone())); + }; + assert_eq!(weak.strong_count(), 2); + inner.await; + assert_eq!(weak.strong_count(), 1); + drop(y); + }; + assert_eq!(weak.strong_count(), 1); + block_on(fut); + assert_eq!(weak.strong_count(), 0); + + let y = Arc::new(String::from("nested twice")); + let weak = Arc::downgrade(&y); + let fut = async { + let inner = async { + drop(move(move(y.clone()))); + }; + assert_eq!(weak.strong_count(), 2); + inner.await; + assert_eq!(weak.strong_count(), 1); + }; + assert_eq!(weak.strong_count(), 2); + block_on(fut); + assert_eq!(weak.strong_count(), 1); + assert_eq!(&*y, "nested twice"); let z = Arc::new(String::from("async move")); assert_eq!(Arc::strong_count(&z), 1); diff --git a/tests/ui/move-expr/async-closures.rs b/tests/ui/move-expr/async-closures.rs index 5f93915a9f877..0c5d651719dbe 100644 --- a/tests/ui/move-expr/async-closures.rs +++ b/tests/ui/move-expr/async-closures.rs @@ -29,5 +29,14 @@ fn main() { assert_eq!(Arc::strong_count(&x), 2); drop(fut); assert_eq!(Arc::strong_count(&x), 1); - assert_eq!(Arc::strong_count(&x), 1); + + let y = Arc::new(String::from("nested")); + assert_eq!(Arc::strong_count(&y), 1); + let c = async || move(move(y.clone())); + assert_eq!(Arc::strong_count(&y), 2); + let fut = c(); + assert_eq!(Arc::strong_count(&y), 2); + drop(fut); + assert_eq!(Arc::strong_count(&y), 1); + assert_eq!(&*y, "nested"); } diff --git a/tests/ui/move-expr/async-gen-blocks.rs b/tests/ui/move-expr/async-gen-blocks.rs index 4037b276ebcb7..f77123751e1d5 100644 --- a/tests/ui/move-expr/async-gen-blocks.rs +++ b/tests/ui/move-expr/async-gen-blocks.rs @@ -77,15 +77,19 @@ fn main() { assert_eq!(Arc::strong_count(&x), 1); let y = Arc::new(String::from("nested")); - assert_eq!(Arc::strong_count(&y), 1); + let weak = Arc::downgrade(&y); let mut iter = Box::pin(async gen { - let value = move(move(y.clone())); - yield Arc::strong_count(&value); + let mut inner = Box::pin(async gen { + let value = move(move(y.clone())); + yield Arc::strong_count(&value); + }); + yield ready_next(inner.as_mut()).unwrap(); }); - assert_eq!(Arc::strong_count(&y), 2); + assert_eq!(weak.strong_count(), 2); assert_eq!(ready_next(iter.as_mut()), Some(2)); drop(iter); - assert_eq!(Arc::strong_count(&y), 1); + assert_eq!(weak.strong_count(), 1); + assert_eq!(&*y, "nested"); let z = Arc::new(String::from("async gen move")); assert_eq!(Arc::strong_count(&z), 1); diff --git a/tests/ui/move-expr/gen-blocks.rs b/tests/ui/move-expr/gen-blocks.rs index c6cec6491cc6e..b38313b83a8f7 100644 --- a/tests/ui/move-expr/gen-blocks.rs +++ b/tests/ui/move-expr/gen-blocks.rs @@ -35,15 +35,19 @@ fn main() { assert_eq!(Arc::strong_count(&x), 1); let y = Arc::new(String::from("nested")); - assert_eq!(Arc::strong_count(&y), 1); + let weak = Arc::downgrade(&y); let mut iter = gen { - let value = move(move(y.clone())); - yield Arc::strong_count(&value); + let mut inner = gen { + let value = move(move(y.clone())); + yield Arc::strong_count(&value); + }; + yield inner.next().unwrap(); }; - assert_eq!(Arc::strong_count(&y), 2); + assert_eq!(weak.strong_count(), 2); assert_eq!(iter.next(), Some(2)); drop(iter); - assert_eq!(Arc::strong_count(&y), 1); + assert_eq!(weak.strong_count(), 1); + assert_eq!(&*y, "nested"); let z = Arc::new(String::from("gen move")); assert_eq!(Arc::strong_count(&z), 1); diff --git a/tests/ui/move-expr/nested-async-block-ownership.rs b/tests/ui/move-expr/nested-async-block-ownership.rs new file mode 100644 index 0000000000000..32dbd808861f8 --- /dev/null +++ b/tests/ui/move-expr/nested-async-block-ownership.rs @@ -0,0 +1,17 @@ +//@ edition: 2021 +#![allow(incomplete_features)] +#![feature(move_expr)] + +use std::sync::Arc; + +fn main() { + let c = Arc::new(String::new()); + let _future = async { + let f = async { + drop(move(c.clone())); + }; + f.await; + drop(c); + }; + println!("{c}"); //~ ERROR the type `Arc` does not implement `Copy` +} diff --git a/tests/ui/move-expr/nested-async-block-ownership.stderr b/tests/ui/move-expr/nested-async-block-ownership.stderr new file mode 100644 index 0000000000000..fb3030d995230 --- /dev/null +++ b/tests/ui/move-expr/nested-async-block-ownership.stderr @@ -0,0 +1,23 @@ +error[E0382]: the type `Arc` does not implement `Copy` + --> $DIR/nested-async-block-ownership.rs:16:16 + | +LL | let c = Arc::new(String::new()); + | - this move could be avoided by cloning the original `Arc`, which is inexpensive +LL | let _future = async { + | ----- value moved here +... +LL | drop(c); + | - variable moved due to use in coroutine +LL | }; +LL | println!("{c}"); + | ^ value borrowed here after move + | + = note: consider using `Arc::clone` +help: clone the value to increment its reference count + | +LL | drop(c.clone()); + | ++++++++ + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0382`. diff --git a/tests/ui/move-expr/nested-move-exhausted.rs b/tests/ui/move-expr/nested-move-exhausted.rs new file mode 100644 index 0000000000000..d809020d3af6b --- /dev/null +++ b/tests/ui/move-expr/nested-move-exhausted.rs @@ -0,0 +1,17 @@ +//@ edition: 2024 +#![allow(incomplete_features)] +#![feature(async_iterator, gen_blocks, move_expr)] + +fn main() { + let _ = || move(move(0)); + //~^ ERROR nested `move(expr)` requires another enclosing closure + + let _ = async { move(move(0)) }; + //~^ ERROR nested `move(expr)` requires another enclosing closure + + let _ = gen { move(move(0)) }; + //~^ ERROR nested `move(expr)` requires another enclosing closure + + let _ = async gen { move(move(0)) }; + //~^ ERROR nested `move(expr)` requires another enclosing closure +} diff --git a/tests/ui/move-expr/nested-move-exhausted.stderr b/tests/ui/move-expr/nested-move-exhausted.stderr new file mode 100644 index 0000000000000..9743a96b81ef3 --- /dev/null +++ b/tests/ui/move-expr/nested-move-exhausted.stderr @@ -0,0 +1,26 @@ +error: nested `move(expr)` requires another enclosing closure, `async`, `gen`, or `async gen` block + --> $DIR/nested-move-exhausted.rs:6:21 + | +LL | let _ = || move(move(0)); + | ^^^^ + +error: nested `move(expr)` requires another enclosing closure, `async`, `gen`, or `async gen` block + --> $DIR/nested-move-exhausted.rs:9:26 + | +LL | let _ = async { move(move(0)) }; + | ^^^^ + +error: nested `move(expr)` requires another enclosing closure, `async`, `gen`, or `async gen` block + --> $DIR/nested-move-exhausted.rs:12:24 + | +LL | let _ = gen { move(move(0)) }; + | ^^^^ + +error: nested `move(expr)` requires another enclosing closure, `async`, `gen`, or `async gen` block + --> $DIR/nested-move-exhausted.rs:15:30 + | +LL | let _ = async gen { move(move(0)) }; + | ^^^^ + +error: aborting due to 4 previous errors + diff --git a/tests/ui/move-expr/nested-move-expr.rs b/tests/ui/move-expr/nested-move-expr.rs index cf3364c50aad7..f3ca679641238 100644 --- a/tests/ui/move-expr/nested-move-expr.rs +++ b/tests/ui/move-expr/nested-move-expr.rs @@ -1,12 +1,18 @@ -//@ check-pass +//@ run-pass #![allow(incomplete_features)] #![feature(move_expr)] +use std::sync::Arc; + fn main() { - let v = "Hello, Ferris".to_string(); - let r = || { - || (move(move(v.clone()))).len() - }; + let v = Arc::new("Hello, Ferris".to_string()); + let outer = || || (move(move(v.clone()))).len(); + + assert_eq!(Arc::strong_count(&v), 2); + let inner = outer(); + assert_eq!(Arc::strong_count(&v), 2); + assert_eq!(inner(), v.len()); + assert_eq!(Arc::strong_count(&v), 1); - assert_eq!(r()(), v.len()); + println!("{v}"); } From 216cf9e084a7bcae5104aebd41879fd879c0765a Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 20 Aug 2026 22:42:54 +0900 Subject: [PATCH 25/32] evaluate coroutine-closure move expressions at closure creation --- compiler/rustc_ast_lowering/src/expr.rs | 24 ++-- .../rustc_ast_lowering/src/expr/closure.rs | 40 +++---- compiler/rustc_hir_analysis/src/collect.rs | 10 +- compiler/rustc_hir_typeck/src/upvar.rs | 106 ++++++++---------- compiler/rustc_middle/src/ty/closure.rs | 18 ++- compiler/rustc_middle/src/ty/mod.rs | 4 +- .../src/coroutine/by_move_body.rs | 12 +- 7 files changed, 91 insertions(+), 123 deletions(-) diff --git a/compiler/rustc_ast_lowering/src/expr.rs b/compiler/rustc_ast_lowering/src/expr.rs index 00176fa9886ee..1c14c645d474c 100644 --- a/compiler/rustc_ast_lowering/src/expr.rs +++ b/compiler/rustc_ast_lowering/src/expr.rs @@ -928,17 +928,19 @@ impl<'hir> LoweringContext<'_, 'hir> { (params, res) }); - let explicit_captures: &'hir [hir::ExplicitCapture] = if let Some(move_expr_state) = - self.move_expr_bindings.last().and_then(Option::as_ref) - { - self.arena.alloc_from_iter( - move_expr_state - .occurrences - .iter() - .map(|occurrence| hir::ExplicitCapture { var_hir_id: occurrence.binding }), - ) - } else { - &[] + let explicit_captures: &'hir [hir::ExplicitCapture] = match coroutine_source { + hir::CoroutineSource::Block + if let Some(move_expr_state) = + self.move_expr_bindings.last().and_then(Option::as_ref) => + { + self.arena.alloc_from_iter( + move_expr_state + .occurrences + .iter() + .map(|occurrence| hir::ExplicitCapture { var_hir_id: occurrence.binding }), + ) + } + _ => &[], }; // `static |<_task_context?>| -> { }`: diff --git a/compiler/rustc_ast_lowering/src/expr/closure.rs b/compiler/rustc_ast_lowering/src/expr/closure.rs index 3d28b5e92ad3f..8505d39a718c4 100644 --- a/compiler/rustc_ast_lowering/src/expr/closure.rs +++ b/compiler/rustc_ast_lowering/src/expr/closure.rs @@ -99,9 +99,8 @@ impl<'hir> LoweringContext<'_, 'hir> { /// The lowering is split this way because `move(...)` initializers must be /// evaluated before the closure is created, but the closure body must still /// lower each `move(...)` occurrence as a use of the synthetic local that - /// will be introduced by that outer block. For example: - /// - /// For example, `|| move(foo.clone()).len()` becomes roughly: + /// will be introduced by that outer block. For example, + /// `|| move(foo.clone()).len()` becomes roughly: /// /// ```ignore (illustrative) /// { @@ -270,9 +269,9 @@ impl<'hir> LoweringContext<'_, 'hir> { } // Coroutine closures are lowered separately because they build a different - // body shape. The source body is still lowered with `MoveExprState` active, - // so `move(...)` occurrences are collected and then hoisted to the outer - // closure body, immediately before the generated coroutine is created. + // body shape. The source body is lowered with the caller's `MoveExprState` + // active, so `move(...)` occurrences are collected and hoisted into a block + // around the outer closure expression. fn lower_expr_coroutine_closure( &mut self, binder: &ClosureBinder, @@ -308,27 +307,14 @@ impl<'hir> LoweringContext<'_, 'hir> { // Transform `async |x: u8| -> X { ... }` into // `|x: u8| || -> X { ... }`. let body_id = this.lower_body(|this| { - let ((parameters, expr), move_expr_state) = - this.with_move_expr_bindings(Some(MoveExprState::default()), |this| { - this.lower_coroutine_body_with_moved_arguments( - &inner_decl, - |this| { - this.with_new_scopes(fn_decl_span, |this| this.lower_expr_mut(body)) - }, - fn_decl_span, - body.span, - coroutine_marker, - hir::CoroutineSource::Closure, - ) - }); - let Some(move_expr_state) = move_expr_state else { - span_bug!( - fn_decl_span, - "coroutine closure lowering did not return `move(...)` state" - ); - }; - - let expr = this.lower_expr_with_move_exprs(expr, move_expr_state, body, body.span); + let (parameters, expr) = this.lower_coroutine_body_with_moved_arguments( + &inner_decl, + |this| this.with_new_scopes(fn_decl_span, |this| this.lower_expr_mut(body)), + fn_decl_span, + body.span, + coroutine_marker, + hir::CoroutineSource::Closure, + ); this.maybe_forward_track_caller(closure_hir_id, expr.hir_id); diff --git a/compiler/rustc_hir_analysis/src/collect.rs b/compiler/rustc_hir_analysis/src/collect.rs index 2f61ed9be30b8..2e4da8d948f07 100644 --- a/compiler/rustc_hir_analysis/src/collect.rs +++ b/compiler/rustc_hir_analysis/src/collect.rs @@ -1709,14 +1709,6 @@ fn coroutine_for_closure(tcx: TyCtxt<'_>, def_id: LocalDefId) -> DefId { bug!() }; - let body = tcx.hir_body(body).value; - // `move(...)` in coroutine closures wraps the generated coroutine in an - // outer block of synthetic initializer lets. - let body = match body.kind { - hir::ExprKind::Block(block, None) if let Some(tail) = block.expr => tail, - _ => body, - }; - let &hir::Expr { kind: hir::ExprKind::Closure(&rustc_hir::Closure { @@ -1725,7 +1717,7 @@ fn coroutine_for_closure(tcx: TyCtxt<'_>, def_id: LocalDefId) -> DefId { .. }), .. - } = body + } = tcx.hir_body(body).value else { bug!() }; diff --git a/compiler/rustc_hir_typeck/src/upvar.rs b/compiler/rustc_hir_typeck/src/upvar.rs index c9dc0f33da8e2..167cb1f272533 100644 --- a/compiler/rustc_hir_typeck/src/upvar.rs +++ b/compiler/rustc_hir_typeck/src/upvar.rs @@ -290,18 +290,12 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> { // moved, and so on. let _ = euv::ExprUseVisitor::new(&closure_fcx, &mut delegate).consume_body(body); - // `consume_body` only sees how the lowered closure body uses those - // places. For `move(foo).clone()`, the body may only borrow the - // synthetic local for `foo`, but the source `move(...)` still requires - // capturing that local by value. + // Save the captures that must be upgraded to by-value after inferring + // the closure kind from the operations in the body. let explicit_captures = match self.tcx.hir_node(closure_hir_id).expect_expr().kind { hir::ExprKind::Closure(closure) => closure.explicit_captures, _ => bug!("expected closure expr for {:?}", closure_hir_id), }; - for capture in explicit_captures { - let place = closure_fcx.place_for_root_variable(closure_def_id, capture.var_hir_id); - delegate.consume(&PlaceWithHirId { hir_id: capture.var_hir_id, place }, closure_hir_id); - } // There are several curious situations with coroutine-closures where // analysis is too aggressive with borrows when the coroutine-closure is @@ -400,9 +394,25 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> { self.log_capture_analysis_first_pass(closure_def_id, &delegate.capture_information, span); - let (capture_information, closure_kind, origin) = self + let (mut capture_information, closure_kind, origin) = self .process_collected_capture_information(capture_clause, &delegate.capture_information); + // `move(expr)` requires its synthetic local to be captured by value, + // regardless of how the closure body uses it. Apply that requirement + // after closure-kind inference so capturing a value does not by itself + // make the closure `FnOnce`. + for capture in explicit_captures { + let place = closure_fcx.place_for_root_variable(closure_def_id, capture.var_hir_id); + capture_information.push(( + place, + ty::CaptureInfo { + capture_kind_expr_id: Some(closure_hir_id), + path_expr_id: Some(closure_hir_id), + capture_kind: UpvarCapture::ByValue, + }, + )); + } + self.compute_min_captures(closure_def_id, capture_information, span); let closure_hir_id = self.tcx.local_def_id_to_hir_id(closure_def_id); @@ -504,64 +514,44 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> { .tupled_inputs_ty .tuple_fields() .len(); - let coroutine_def_id = - self.tcx.coroutine_for_closure(closure_def_id).expect_local(); - let explicit_captures = self - .tcx - .hir_node_by_def_id(coroutine_def_id) - .expect_closure() - .explicit_captures; let typeck_results = self.typeck_results.borrow(); - let parent_captures = typeck_results - .closure_min_captures_flattened(closure_def_id) - .collect::>(); let tupled_upvars_ty_for_borrow = Ty::new_tup_from_iter( self.tcx, - typeck_results - .closure_min_captures_flattened(coroutine_def_id) - // Skip the captures that are just moving the closure's args - // into the coroutine. These are always by move, and we append - // those later in the `CoroutineClosureSignature` helper functions. - .skip(num_args) - .map(|child_capture| { + ty::analyze_coroutine_closure_captures( + typeck_results.closure_min_captures_flattened(closure_def_id), + typeck_results + .closure_min_captures_flattened( + self.tcx.coroutine_for_closure(closure_def_id).expect_local(), + ) + // Skip the captures that are just moving the closure's args + // into the coroutine. These are always by move, and we append + // those later in the `CoroutineClosureSignature` helper functions. + .skip(num_args), + |(_, parent_capture), (_, child_capture)| { + // This is subtle. See documentation on function. + let needs_ref = should_reborrow_from_env_of_parent_coroutine_closure( + parent_capture, + child_capture, + ); + let upvar_ty = child_capture.place.ty(); let capture = child_capture.info.capture_kind; - let region = if explicit_captures.iter().any(|explicit| { - explicit.var_hir_id == child_capture.get_root_variable() - }) { - // Synthetic move-expression locals are captured by - // value into the generated coroutine. They do not - // reborrow from the parent coroutine-closure env. - self.tcx.lifetimes.re_erased - } else { - let Some(parent_capture) = - parent_captures.iter().copied().find(|parent_capture| { - ty::child_prefix_matches_parent_projections( - parent_capture, - child_capture, - ) - }) - else { - bug!("child capture did not match a parent coroutine capture"); - }; - - // This is subtle. See documentation on function. - if should_reborrow_from_env_of_parent_coroutine_closure( - parent_capture, - child_capture, - ) { - closure_env_region - } else { - self.tcx.lifetimes.re_erased - } - }; - // Not all upvars are captured by ref, so use // `apply_capture_kind_on_capture_ty` to ensure that we // compute the right captured type. - apply_capture_kind_on_capture_ty(self.tcx, upvar_ty, capture, region) - }), + apply_capture_kind_on_capture_ty( + self.tcx, + upvar_ty, + capture, + if needs_ref { + closure_env_region + } else { + self.tcx.lifetimes.re_erased + }, + ) + }, + ), ); let coroutine_captures_by_ref_ty = Ty::new_fn_ptr( self.tcx, diff --git a/compiler/rustc_middle/src/ty/closure.rs b/compiler/rustc_middle/src/ty/closure.rs index 7bf143f625405..c6cdb9b9e2b96 100644 --- a/compiler/rustc_middle/src/ty/closure.rs +++ b/compiler/rustc_middle/src/ty/closure.rs @@ -434,11 +434,11 @@ pub fn analyze_coroutine_closure_captures<'a, 'tcx: 'a, T>( // refining the set of captures via edition-2021 precise captures. We want to // match up any number of child captures with one parent capture, so we keep // peeking off this `Peekable` until the child doesn't match anymore. - // - // Do not require every parent capture to match a child capture. A parent - // capture may be used only while evaluating a coroutine-closure - // `move(expr)` initializer, before the child coroutine is created. for (parent_field_idx, parent_capture) in parent_captures.into_iter().enumerate() { + // Make sure we use every field at least once, b/c why are we capturing something + // if it's not used in the inner coroutine. + let mut field_used_at_least_once = false; + // A parent matches a child if they share the same prefix of projections. // The child may have more, if it is capturing sub-fields out of // something that is captured by-move in the parent closure. @@ -458,13 +458,21 @@ pub fn analyze_coroutine_closure_captures<'a, 'tcx: 'a, T>( (parent_field_idx, parent_capture), (child_field_idx, child_capture), ); + + field_used_at_least_once = true; } + + // Make sure the field was used at least once. + assert!( + field_used_at_least_once, + "we captured {parent_capture:#?} but it was not used in the child coroutine?" + ); } assert_eq!(child_captures.next(), None, "leftover child captures?"); } } -pub fn child_prefix_matches_parent_projections( +fn child_prefix_matches_parent_projections( parent_capture: &ty::CapturedPlace<'_>, child_capture: &ty::CapturedPlace<'_>, ) -> bool { diff --git a/compiler/rustc_middle/src/ty/mod.rs b/compiler/rustc_middle/src/ty/mod.rs index 9328bf44ee04f..cc6a8619e1e74 100644 --- a/compiler/rustc_middle/src/ty/mod.rs +++ b/compiler/rustc_middle/src/ty/mod.rs @@ -64,8 +64,8 @@ pub use vtable::*; pub use self::closure::{ BorrowKind, CAPTURE_STRUCT_LOCAL, CaptureInfo, CapturedPlace, ClosureTypeInfo, MinCaptureInformationMap, MinCaptureList, RootVariableMinCaptureList, UpvarCapture, UpvarId, - UpvarPath, analyze_coroutine_closure_captures, child_prefix_matches_parent_projections, - is_ancestor_or_same_capture, place_to_string_for_capture, + UpvarPath, analyze_coroutine_closure_captures, is_ancestor_or_same_capture, + place_to_string_for_capture, }; pub use self::consts::{ AliasConst, AliasConstKind, AtomicOrdering, Const, ConstInt, ConstKind, ConstToValTreeResult, diff --git a/compiler/rustc_mir_transform/src/coroutine/by_move_body.rs b/compiler/rustc_mir_transform/src/coroutine/by_move_body.rs index 6a66b9c1f9478..88ffe5861a697 100644 --- a/compiler/rustc_mir_transform/src/coroutine/by_move_body.rs +++ b/compiler/rustc_mir_transform/src/coroutine/by_move_body.rs @@ -125,20 +125,10 @@ pub(crate) fn coroutine_by_move_body_def_id<'tcx>( .tupled_inputs_ty .tuple_fields() .len(); - let explicit_captures = - tcx.hir_node_by_def_id(coroutine_def_id).expect_closure().explicit_captures; let field_remapping: UnordMap<_, _> = ty::analyze_coroutine_closure_captures( tcx.closure_captures(parent_def_id).iter().copied(), - tcx.closure_captures(coroutine_def_id) - .iter() - .skip(num_args) - .filter(|capture| { - !explicit_captures - .iter() - .any(|explicit| explicit.var_hir_id == capture.get_root_variable()) - }) - .copied(), + tcx.closure_captures(coroutine_def_id).iter().skip(num_args).copied(), |(parent_field_idx, parent_capture), (child_field_idx, child_capture)| { // Store this set of additional projections (fields and derefs). // We need to re-apply them later. From afff72e3fb2c002da44b06aa363f5736d03e19f0 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 20 Aug 2026 22:44:50 +0900 Subject: [PATCH 26/32] update move-expression closure semantics tests --- tests/ui/move-expr/async-closures.rs | 43 +++++++++++++------ tests/ui/move-expr/nested-move-exhausted.rs | 3 ++ .../ui/move-expr/nested-move-exhausted.stderr | 14 ++++-- tests/ui/move-expr/nested-move-expr.rs | 3 ++ tests/ui/move-expr/plain-closure.rs | 18 +++++++- 5 files changed, 62 insertions(+), 19 deletions(-) diff --git a/tests/ui/move-expr/async-closures.rs b/tests/ui/move-expr/async-closures.rs index 0c5d651719dbe..b467248048367 100644 --- a/tests/ui/move-expr/async-closures.rs +++ b/tests/ui/move-expr/async-closures.rs @@ -4,7 +4,26 @@ #![feature(move_expr)] use std::cell::Cell; +use std::future::Future; +use std::pin::pin; use std::sync::Arc; +use std::task::{Context, Poll, Waker}; + +fn block_on(future: impl Future) -> T { + let mut future = pin!(future); + let context = &mut Context::from_waker(Waker::noop()); + + loop { + match future.as_mut().poll(context) { + Poll::Ready(value) => return value, + Poll::Pending => {} + } + } +} + +async fn call_once(closure: impl AsyncFnOnce() -> T) -> T { + closure().await +} fn main() { let created = Cell::new(0); @@ -15,28 +34,26 @@ fn main() { }); n }; - assert_eq!(created.get(), 0); - let fut = c(); assert_eq!(created.get(), 1); - drop(fut); + assert_eq!(block_on(c()), 1); + assert_eq!(block_on(c()), 1); + assert_eq!(created.get(), 1); let x = Arc::new(String::from("hello")); assert_eq!(Arc::strong_count(&x), 1); let c = async || move(x.clone()); - assert_eq!(Arc::strong_count(&x), 1); + assert_eq!(Arc::strong_count(&x), 2); let fut = c(); assert_eq!(Arc::strong_count(&x), 2); drop(fut); assert_eq!(Arc::strong_count(&x), 1); - let y = Arc::new(String::from("nested")); - assert_eq!(Arc::strong_count(&y), 1); - let c = async || move(move(y.clone())); - assert_eq!(Arc::strong_count(&y), 2); - let fut = c(); - assert_eq!(Arc::strong_count(&y), 2); - drop(fut); - assert_eq!(Arc::strong_count(&y), 1); - assert_eq!(&*y, "nested"); + let a = String::from("a"); + let b = String::from("bbb"); + let c = async || { + let moved = move(a.clone()); + (moved, b.len()) + }; + assert_eq!(block_on(call_once(c)), (String::from("a"), 3)); } diff --git a/tests/ui/move-expr/nested-move-exhausted.rs b/tests/ui/move-expr/nested-move-exhausted.rs index d809020d3af6b..8508cc1c756b5 100644 --- a/tests/ui/move-expr/nested-move-exhausted.rs +++ b/tests/ui/move-expr/nested-move-exhausted.rs @@ -6,6 +6,9 @@ fn main() { let _ = || move(move(0)); //~^ ERROR nested `move(expr)` requires another enclosing closure + let _ = async || move(move(0)); + //~^ ERROR nested `move(expr)` requires another enclosing closure + let _ = async { move(move(0)) }; //~^ ERROR nested `move(expr)` requires another enclosing closure diff --git a/tests/ui/move-expr/nested-move-exhausted.stderr b/tests/ui/move-expr/nested-move-exhausted.stderr index 9743a96b81ef3..2c919666c75ae 100644 --- a/tests/ui/move-expr/nested-move-exhausted.stderr +++ b/tests/ui/move-expr/nested-move-exhausted.stderr @@ -5,22 +5,28 @@ LL | let _ = || move(move(0)); | ^^^^ error: nested `move(expr)` requires another enclosing closure, `async`, `gen`, or `async gen` block - --> $DIR/nested-move-exhausted.rs:9:26 + --> $DIR/nested-move-exhausted.rs:9:27 + | +LL | let _ = async || move(move(0)); + | ^^^^ + +error: nested `move(expr)` requires another enclosing closure, `async`, `gen`, or `async gen` block + --> $DIR/nested-move-exhausted.rs:12:26 | LL | let _ = async { move(move(0)) }; | ^^^^ error: nested `move(expr)` requires another enclosing closure, `async`, `gen`, or `async gen` block - --> $DIR/nested-move-exhausted.rs:12:24 + --> $DIR/nested-move-exhausted.rs:15:24 | LL | let _ = gen { move(move(0)) }; | ^^^^ error: nested `move(expr)` requires another enclosing closure, `async`, `gen`, or `async gen` block - --> $DIR/nested-move-exhausted.rs:15:30 + --> $DIR/nested-move-exhausted.rs:18:30 | LL | let _ = async gen { move(move(0)) }; | ^^^^ -error: aborting due to 4 previous errors +error: aborting due to 5 previous errors diff --git a/tests/ui/move-expr/nested-move-expr.rs b/tests/ui/move-expr/nested-move-expr.rs index f3ca679641238..b6e0f70b355d7 100644 --- a/tests/ui/move-expr/nested-move-expr.rs +++ b/tests/ui/move-expr/nested-move-expr.rs @@ -12,6 +12,9 @@ fn main() { let inner = outer(); assert_eq!(Arc::strong_count(&v), 2); assert_eq!(inner(), v.len()); + assert_eq!(inner(), v.len()); + assert_eq!(Arc::strong_count(&v), 2); + drop(inner); assert_eq!(Arc::strong_count(&v), 1); println!("{v}"); diff --git a/tests/ui/move-expr/plain-closure.rs b/tests/ui/move-expr/plain-closure.rs index 788c631cf5fdf..3f58142f7ea9c 100644 --- a/tests/ui/move-expr/plain-closure.rs +++ b/tests/ui/move-expr/plain-closure.rs @@ -1,8 +1,23 @@ -//@ check-pass +//@ run-pass #![allow(incomplete_features)] #![feature(move_expr)] +use std::cell::Cell; + fn main() { + let created = Cell::new(0); + let c = || { + let n = move({ + created.set(created.get() + 1); + created.get() + }); + n + }; + assert_eq!(created.get(), 1); + assert_eq!(c(), 1); + assert_eq!(c(), 1); + assert_eq!(created.get(), 1); + let s = String::from("hello"); let c = || { let t = move(s); @@ -18,5 +33,4 @@ fn main() { println!("{} {}", x, y); }; c(); - } From 2ed8d0db767789d4b09976777bc4422ffa6ef109 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Thu, 20 Aug 2026 22:45:05 +0900 Subject: [PATCH 27/32] add move-expression tests for generator closures --- tests/ui/move-expr/gen-closures.rs | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 tests/ui/move-expr/gen-closures.rs diff --git a/tests/ui/move-expr/gen-closures.rs b/tests/ui/move-expr/gen-closures.rs new file mode 100644 index 0000000000000..5e74089710adf --- /dev/null +++ b/tests/ui/move-expr/gen-closures.rs @@ -0,0 +1,46 @@ +//@ run-pass + +#![allow(incomplete_features)] +#![feature(iter_macro, move_expr, yield_expr)] + +use std::cell::Cell; +use std::iter::iter; +use std::sync::Arc; + +fn main() { + let created = Cell::new(0); + let closure = iter! { || { + let n = move({ + created.set(created.get() + 1); + created.get() + }); + yield n; + }}; + assert_eq!(created.get(), 1); + assert_eq!(closure().next(), Some(1)); + assert_eq!(closure().next(), Some(1)); + assert_eq!(created.get(), 1); + + let x = Arc::new(String::from("hello")); + assert_eq!(Arc::strong_count(&x), 1); + + let closure = iter! { || { + yield move(x.clone()); + }}; + assert_eq!(Arc::strong_count(&x), 2); + let mut generator = closure(); + assert_eq!(Arc::strong_count(&x), 2); + let yielded = generator.next().unwrap(); + assert_eq!(Arc::strong_count(&x), 2); + assert_eq!(generator.next(), None); + drop(yielded); + assert_eq!(Arc::strong_count(&x), 1); + + let a = String::from("a"); + let b = String::from("bbb"); + let closure = iter! { || { + let moved = move(a.clone()); + yield (moved, b.len()); + }}; + assert_eq!(closure().next(), Some((String::from("a"), 3))); +} From 922fbba38ed69c18aff2cee6349287b87e208956 Mon Sep 17 00:00:00 2001 From: Yukang Date: Wed, 9 Sep 2026 00:42:54 +0800 Subject: [PATCH 28/32] refactoring on upvar_tys --- compiler/rustc_middle/src/ty/sty.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/compiler/rustc_middle/src/ty/sty.rs b/compiler/rustc_middle/src/ty/sty.rs index 8014a52c9b4e1..6f76bc0ff028b 100644 --- a/compiler/rustc_middle/src/ty/sty.rs +++ b/compiler/rustc_middle/src/ty/sty.rs @@ -188,15 +188,9 @@ impl<'tcx> UpvarArgs<'tcx> { /// empty iterator is returned. #[inline] pub fn upvar_tys(self) -> &'tcx List> { - let tupled_tys = match self { - UpvarArgs::Closure(args) => args.as_closure().tupled_upvars_ty(), - UpvarArgs::Coroutine(args) => args.as_coroutine().tupled_upvars_ty(), - UpvarArgs::CoroutineClosure(args) => args.as_coroutine_closure().tupled_upvars_ty(), - }; - - match tupled_tys.kind() { + match self.tupled_upvars_ty().kind() { TyKind::Error(_) => ty::List::empty(), - TyKind::Tuple(..) => self.tupled_upvars_ty().tuple_fields(), + TyKind::Tuple(args) => args, TyKind::Infer(_) => bug!("upvar_tys called before capture types are inferred"), ty => bug!("Unexpected representation of upvar types tuple {:?}", ty), } From 2a61e696449cb8ce3f9813b64fb1c4904eb33888 Mon Sep 17 00:00:00 2001 From: Jonathan Brouwer Date: Tue, 8 Sep 2026 19:26:47 +0200 Subject: [PATCH 29/32] Ignore `self-in-const-generics` test for parallel frontend --- tests/ui/traits/alias/self-in-const-generics.rs | 2 ++ tests/ui/traits/alias/self-in-const-generics.stderr | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/ui/traits/alias/self-in-const-generics.rs b/tests/ui/traits/alias/self-in-const-generics.rs index a7d0ac9cbb4c2..e347e26893741 100644 --- a/tests/ui/traits/alias/self-in-const-generics.rs +++ b/tests/ui/traits/alias/self-in-const-generics.rs @@ -1,3 +1,5 @@ +//@ ignore-parallel-frontend triage https://github.com/rust-lang/rust/issues/162316 + #![allow(incomplete_features)] #![feature(generic_const_exprs)] #![feature(trait_alias)] diff --git a/tests/ui/traits/alias/self-in-const-generics.stderr b/tests/ui/traits/alias/self-in-const-generics.stderr index ea201a2dd977c..7fe45ff0c51b9 100644 --- a/tests/ui/traits/alias/self-in-const-generics.stderr +++ b/tests/ui/traits/alias/self-in-const-generics.stderr @@ -1,12 +1,12 @@ error[E0038]: the trait alias `BB` is not dyn compatible - --> $DIR/self-in-const-generics.rs:9:16 + --> $DIR/self-in-const-generics.rs:11:16 | LL | fn foo(x: &dyn BB) {} | ^^ `BB` is not dyn compatible | note: for a trait to be dyn compatible it needs to allow building a vtable for more information, visit - --> $DIR/self-in-const-generics.rs:7:12 + --> $DIR/self-in-const-generics.rs:9:12 | LL | trait BB = Bar<{ 2 + 1 }>; | -- ^^^^^^^^^^^^^^ ...because it uses `Self` as a type parameter From c6d1849411f2c054cffa3bafa4c3d685e8080fb9 Mon Sep 17 00:00:00 2001 From: Jonathan Brouwer Date: Tue, 8 Sep 2026 20:41:43 +0200 Subject: [PATCH 30/32] Reserve items in `Extend` implementation of `MonoItems` --- compiler/rustc_monomorphize/src/collector.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compiler/rustc_monomorphize/src/collector.rs b/compiler/rustc_monomorphize/src/collector.rs index 4ee1abe4a1ff4..622666a12ef40 100644 --- a/compiler/rustc_monomorphize/src/collector.rs +++ b/compiler/rustc_monomorphize/src/collector.rs @@ -345,6 +345,8 @@ impl<'tcx> Extend>> for MonoItems<'tcx> { where I: IntoIterator>>, { + let iter = iter.into_iter(); + self.items.reserve(iter.size_hint().0); for item in iter { self.push(item) } From 5bd43f35e5ec90ac97e035a70683412a5c35e9f5 Mon Sep 17 00:00:00 2001 From: Jonathan Brouwer Date: Tue, 8 Sep 2026 20:46:38 +0200 Subject: [PATCH 31/32] Do the same optimization in more places --- compiler/rustc_data_structures/src/sso/map.rs | 4 +++- compiler/rustc_data_structures/src/sso/set.rs | 4 +++- compiler/rustc_infer/src/traits/util.rs | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/compiler/rustc_data_structures/src/sso/map.rs b/compiler/rustc_data_structures/src/sso/map.rs index 827c82fa46a11..018cbd96317db 100644 --- a/compiler/rustc_data_structures/src/sso/map.rs +++ b/compiler/rustc_data_structures/src/sso/map.rs @@ -356,7 +356,9 @@ impl Extend<(K, V)> for SsoHashMap { where I: IntoIterator, { - for (key, value) in iter.into_iter() { + let iter = iter.into_iter(); + self.reserve(iter.size_hint().0); + for (key, value) in iter { self.insert(key, value); } } diff --git a/compiler/rustc_data_structures/src/sso/set.rs b/compiler/rustc_data_structures/src/sso/set.rs index e3fa1cbf4cc57..cf75ea4537012 100644 --- a/compiler/rustc_data_structures/src/sso/set.rs +++ b/compiler/rustc_data_structures/src/sso/set.rs @@ -168,7 +168,9 @@ impl Extend for SsoHashSet { where I: IntoIterator, { - for val in iter.into_iter() { + let iter = iter.into_iter(); + self.reserve(iter.size_hint().0); + for val in iter { self.insert(val); } } diff --git a/compiler/rustc_infer/src/traits/util.rs b/compiler/rustc_infer/src/traits/util.rs index cc29546adb880..0380488caf96f 100644 --- a/compiler/rustc_infer/src/traits/util.rs +++ b/compiler/rustc_infer/src/traits/util.rs @@ -47,6 +47,8 @@ impl<'tcx> PredicateSet<'tcx> { impl<'tcx> Extend> for PredicateSet<'tcx> { fn extend>>(&mut self, iter: I) { + let iter = iter.into_iter(); + self.set.reserve(iter.size_hint().0); for pred in iter { self.insert(pred); } From 71a523b7fda4548a2fef2f8a9eaa05d2c9d4e22e Mon Sep 17 00:00:00 2001 From: Jonathan Brouwer Date: Tue, 8 Sep 2026 23:04:10 +0200 Subject: [PATCH 32/32] Move the `expect-item-after-attribute.rs` test to the correct directory --- tests/rustdoc-ui/{ => doctest}/expect-item-after-attribute.rs | 0 tests/rustdoc-ui/{ => doctest}/expect-item-after-attribute.stdout | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/rustdoc-ui/{ => doctest}/expect-item-after-attribute.rs (100%) rename tests/rustdoc-ui/{ => doctest}/expect-item-after-attribute.stdout (100%) diff --git a/tests/rustdoc-ui/expect-item-after-attribute.rs b/tests/rustdoc-ui/doctest/expect-item-after-attribute.rs similarity index 100% rename from tests/rustdoc-ui/expect-item-after-attribute.rs rename to tests/rustdoc-ui/doctest/expect-item-after-attribute.rs diff --git a/tests/rustdoc-ui/expect-item-after-attribute.stdout b/tests/rustdoc-ui/doctest/expect-item-after-attribute.stdout similarity index 100% rename from tests/rustdoc-ui/expect-item-after-attribute.stdout rename to tests/rustdoc-ui/doctest/expect-item-after-attribute.stdout