From 47c9070de56d936b72f68914dfe7b7ebbffaf2c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Le=C3=B3n=20Orell=20Valerian=20Liehr?= Date: Wed, 7 Oct 2026 14:57:00 +0200 Subject: [PATCH 01/16] Parse move exprs as a bottom expr instead of a prefix expr --- compiler/rustc_parse/src/parser/expr.rs | 31 +++++++++---------- .../feature-gate-move_expr.stderr | 2 +- .../left-operand-of-dot-call-index-try.rs | 21 +++++++++++++ 3 files changed, 37 insertions(+), 17 deletions(-) create mode 100644 tests/ui/move-expr/left-operand-of-dot-call-index-try.rs diff --git a/compiler/rustc_parse/src/parser/expr.rs b/compiler/rustc_parse/src/parser/expr.rs index bf99bbe26f087..5577c74850f41 100644 --- a/compiler/rustc_parse/src/parser/expr.rs +++ b/compiler/rustc_parse/src/parser/expr.rs @@ -410,12 +410,6 @@ impl<'a> Parser<'a> { pre_span, )); } - token::Ident(..) - if this.token.is_keyword(kw::Move) - && this.look_ahead(1, |t| *t == token::OpenParen) => - { - make_it!(this, attrs, |this, _| this.parse_expr_move(lo)) - } token::Ident(..) if this.may_recover() && this.is_mistaken_not_ident_negation() => { make_it!(this, attrs, |this, _| this.recover_not_expr(lo)) } @@ -447,16 +441,6 @@ impl<'a> Parser<'a> { self.parse_expr_unary(lo, UnOp::Not) } - fn parse_expr_move(&mut self, move_kw: Span) -> PResult<'a, (Span, ExprKind)> { - self.bump(); - self.psess.gated_spans.gate(sym::move_expr, move_kw); - self.expect(exp!(OpenParen))?; - let expr = self.parse_expr()?; - self.expect(exp!(CloseParen))?; - let span = move_kw.to(self.prev_token.span); - Ok((span, ExprKind::Move(expr, move_kw))) - } - fn is_mistaken_not_ident_negation(&self) -> bool { let token_cannot_continue_expr = |t: &Token| match t.uninterpolate().kind { // These tokens can start an expression after `!`, but @@ -1335,6 +1319,10 @@ impl<'a> Parser<'a> { this.parse_expr_builtin() } else if this.check_path() { this.parse_expr_path_start() + } else if this.token.is_keyword(kw::Move) + && this.look_ahead(1, |t| *t == token::OpenParen) + { + this.parse_expr_move() } else if this.check_keyword(exp!(Move)) || this.check_keyword(exp!(Use)) || this.check_keyword(exp!(Static)) @@ -2216,6 +2204,17 @@ impl<'a> Parser<'a> { Ok(self.mk_expr(blk.span, ExprKind::Block(blk, None))) } + fn parse_expr_move(&mut self) -> PResult<'a, Box> { + let move_kw = self.token.span; + self.expect_keyword(exp!(Move))?; + self.expect(exp!(OpenParen))?; + let expr = self.parse_expr()?; + self.expect(exp!(CloseParen))?; + let span = move_kw.to(self.prev_token.span); + self.psess.gated_spans.gate(sym::move_expr, span); + Ok(self.mk_expr(span, ExprKind::Move(expr, move_kw))) + } + /// Parses a closure expression (e.g., `move |args| expr`). fn parse_expr_closure(&mut self) -> PResult<'a, Box> { let lo = self.token.span; diff --git a/tests/ui/feature-gates/feature-gate-move_expr.stderr b/tests/ui/feature-gates/feature-gate-move_expr.stderr index 8b1da2c06893d..41a2ded842207 100644 --- a/tests/ui/feature-gates/feature-gate-move_expr.stderr +++ b/tests/ui/feature-gates/feature-gate-move_expr.stderr @@ -2,7 +2,7 @@ error[E0658]: `move(expr)` syntax is experimental --> $DIR/feature-gate-move_expr.rs:2:16 | LL | let _ = || move(2); - | ^^^^ + | ^^^^^^^ | = note: see issue #155050 for more information = help: add `#![feature(move_expr)]` to the crate attributes to enable diff --git a/tests/ui/move-expr/left-operand-of-dot-call-index-try.rs b/tests/ui/move-expr/left-operand-of-dot-call-index-try.rs new file mode 100644 index 0000000000000..700101a8a27f2 --- /dev/null +++ b/tests/ui/move-expr/left-operand-of-dot-call-index-try.rs @@ -0,0 +1,21 @@ +// Ensure that move exprs can be the left operand of dot, call, index & try ops. We once used to +// incorrectly parse move exprs as prefix exprs instead of bottom ones which used to prevent that. + +//@ check-pass +//@ edition: 2018.. +#![feature(move_expr)] + +fn main() { + let _ = async || { move(async {}).await }; // Await(Move(_)) + + let _ = || -> Result<(), ()> { + move(((),)).0; // Field(Move(_), ..) + move(Err(()))?; // Try(Move(_)) + move(drop)(()); // Call(Move(_), ..) + move([0])[0]; // Index(Move(_), ..) + + // Previously, we would parse this as Field(Return(Move(_)), ..) (`(return move(…)).0`) + // instead of Return(Field(Move(_), ..)) and thus fail with "no field `0` on type `!`". + return move((Ok(()),)).0; // OK! + }; +} From 604a21cab2edf48dc9b40e1a513273d52aafa167 Mon Sep 17 00:00:00 2001 From: Max Dexheimer Date: Fri, 25 Sep 2026 17:37:42 +0200 Subject: [PATCH 02/16] Document zero sized allocations in `Box` and `Vec` --- library/alloc/src/boxed.rs | 133 ++++++++++++++++---- library/alloc/src/string.rs | 4 + library/alloc/src/vec/mod.rs | 237 ++++++++++++----------------------- 3 files changed, 193 insertions(+), 181 deletions(-) diff --git a/library/alloc/src/boxed.rs b/library/alloc/src/boxed.rs index 7504763120532..9f3a17314c356 100644 --- a/library/alloc/src/boxed.rs +++ b/library/alloc/src/boxed.rs @@ -53,20 +53,26 @@ //! //! # Memory layout //! -//! For non-zero-sized values, a [`Box`] will use the [`Global`] allocator for its allocation. It is -//! valid to convert both ways between a [`Box`] and a raw pointer allocated with the [`Global`] -//! allocator, given that the [`Layout`] used with the allocator is correct for the type and the raw -//! pointer points to a valid value of the right type. More precisely, a `value: *mut T` that has -//! been allocated with the [`Global`] allocator with `Layout::for_value(&*value)` may be converted -//! into a box using [`Box::::from_raw(value)`]. Conversely, the memory backing a `value: *mut T` -//! obtained from [`Box::::into_raw`] may be deallocated using the [`Global`] allocator with -//! [`Layout::for_value(&*value)`]. +//! For non-zero-sized values, a [`Box`] will use its allocator of type `A` for its allocation. +//! It is valid to convert both ways between a `Box` and a raw pointer allocated with `A`, so long as: +//! - the size of the pointee, as reported by [`size_of_val`], is non-zero, +//! - the [`Layout::for_value`] of the pointee [*fits*](Allocator#memory-fitting) the memory block, and +//! - the pointee is a valid instance of `T` (for the given metadata of the pointer). //! -//! For zero-sized values, the `Box` pointer has to be non-null and sufficiently aligned. The -//! recommended way to build a Box to a ZST if `Box::new` cannot be used is to use -//! [`ptr::NonNull::dangling`]. +//! For zero-sized values, a [`Box`] may use any sufficiently aligned non-null pointer. +//! While it is sound to convert a zero-sized allocation from `A` into a `Box`, as +//! long as it is backed by a valid value of `T`, doing so may lead to memory leaks, as +//! the destructor of [`Box`] will not call [`deallocate`](Allocator::deallocate) +//! on zero-sized values. It is recommended to use [`ptr::dangling`] instead. //! -//! On top of these basic layout requirements, a `Box` must point to a valid value of `T`. +//! Likewise, manually deallocating the raw pointer acquired from a `Box` is only sound +//! if the size of the pointee is non-zero, as zero-sized values may be represented by +//! a pointer that is not [*currently allocated*] by that allocator. It is recommended to +//! pair [`Box::into_raw_with_allocator`] with [`Box::from_raw_in`], rather than +//! doing this. +//! +//! Note that even the [`Global`] allocator might not use dangling pointers for +//! zero-sized allocations, even though *most* implementations do this. //! //! So long as `T: Sized`, a `Box` is guaranteed to be represented //! as a single pointer and is also ABI-compatible with C pointers @@ -172,6 +178,7 @@ //! and it's best to avoid relying on this edition-dependent behavior if you wish to preserve //! compatibility with future versions of the compiler. //! +//! [*currently allocated*]: Allocator#currently-allocated-memory //! [ucg#198]: https://github.com/rust-lang/unsafe-code-guidelines/issues/198 //! [ucg#326]: https://github.com/rust-lang/unsafe-code-guidelines/issues/326 //! [dereferencing]: core::ops::Deref @@ -275,12 +282,15 @@ impl Box { /// Allocates memory on the heap and then places `x` into it. /// /// This doesn't actually allocate if `T` is zero-sized. + /// See the [memory layout] section for more details. /// /// # Examples /// /// ``` /// let five = Box::new(5); /// ``` + /// + /// [memory layout]: self#memory-layout #[cfg(not(no_global_oom_handling))] #[inline(always)] #[stable(feature = "rust1", since = "1.0.0")] @@ -367,6 +377,7 @@ impl Box { /// returning an error if the allocation fails /// /// This doesn't actually allocate if `T` is zero-sized. + /// See the [memory layout] section for more details. /// /// # Examples /// @@ -376,6 +387,8 @@ impl Box { /// let five = Box::try_new(5)?; /// # Ok::<(), std::alloc::AllocError>(()) /// ``` + /// + /// [memory layout]: self#memory-layout #[unstable(feature = "allocator_ext", issue = "163177", implied_by = "allocator_api")] #[inline] pub fn try_new(x: T) -> Result { @@ -434,6 +447,7 @@ impl Box { /// Allocates memory in the given allocator then places `x` into it. /// /// This doesn't actually allocate if `T` is zero-sized. + /// See the [memory layout] section for more details. /// /// # Examples /// @@ -442,6 +456,8 @@ impl Box { /// /// let five = Box::new_in(5, System); /// ``` + /// + /// [memory layout]: self#memory-layout #[cfg(not(no_global_oom_handling))] #[stable(feature = "allocator_api", since = "CURRENT_RUSTC_VERSION")] #[must_use] @@ -457,6 +473,7 @@ impl Box { /// returning an error if the allocation fails /// /// This doesn't actually allocate if `T` is zero-sized. + /// See the [memory layout] section for more details. /// /// # Examples /// @@ -468,6 +485,8 @@ impl Box { /// let five = Box::try_new_in(5, System)?; /// # Ok::<(), std::alloc::AllocError>(()) /// ``` + /// + /// [memory layout]: self#memory-layout #[unstable(feature = "allocator_ext", issue = "163177", implied_by = "allocator_api")] #[inline] pub fn try_new_in(x: T, alloc: A) -> Result { @@ -774,6 +793,7 @@ impl Box { /// Allocates memory on the heap then clones `src` into it. /// /// This doesn't actually allocate if `src` is zero-sized. + /// See the [memory layout] section for more details. /// /// # Examples /// @@ -782,6 +802,8 @@ impl Box { /// /// let hello: Box = Box::clone_from_ref("hello"); /// ``` + /// + /// [memory layout]: self#memory-layout #[cfg(not(no_global_oom_handling))] #[unstable(feature = "clone_from_ref", issue = "149075")] #[must_use] @@ -793,6 +815,7 @@ impl Box { /// Allocates memory on the heap then clones `src` into it, returning an error if allocation fails. /// /// This doesn't actually allocate if `src` is zero-sized. + /// See the [memory layout] section for more details. /// /// # Examples /// @@ -802,6 +825,8 @@ impl Box { /// let hello: Box = Box::try_clone_from_ref("hello")?; /// # Ok::<(), std::alloc::AllocError>(()) /// ``` + /// + /// [memory layout]: self#memory-layout #[unstable(feature = "clone_from_ref", issue = "149075")] //#[unstable(feature = "allocator_ext", issue = "163177", implied_by = "allocator_api")] #[inline] @@ -814,6 +839,7 @@ impl Box { /// Allocates memory in the given allocator then clones `src` into it. /// /// This doesn't actually allocate if `src` is zero-sized. + /// See the [memory layout] section for more details. /// /// # Examples /// @@ -824,6 +850,8 @@ impl Box { /// /// let hello: Box = Box::clone_from_ref_in("hello", System); /// ``` + /// + /// [memory layout]: self#memory-layout #[cfg(not(no_global_oom_handling))] #[unstable(feature = "clone_from_ref", issue = "149075")] //#[unstable(feature = "allocator_ext", issue = "163177", implied_by = "allocator_api")] @@ -840,6 +868,7 @@ impl Box { /// Allocates memory in the given allocator then clones `src` into it, returning an error if allocation fails. /// /// This doesn't actually allocate if `src` is zero-sized. + /// See the [memory layout] section for more details. /// /// # Examples /// @@ -851,6 +880,8 @@ impl Box { /// let hello: Box = Box::try_clone_from_ref_in("hello", System)?; /// # Ok::<(), std::alloc::AllocError>(()) /// ``` + /// + /// [memory layout]: self#memory-layout #[unstable(feature = "clone_from_ref", issue = "149075")] //#[unstable(feature = "allocator_ext", issue = "163177", implied_by = "allocator_api")] #[inline] @@ -1300,9 +1331,16 @@ impl Box { /// memory problems. For example, a double-free may occur if the /// function is called twice on the same raw pointer. /// - /// The raw pointer must point to a block of memory allocated by the global allocator. + /// The exact safety requirements are described in the [memory layout] section, together + /// with recommendations about how to handle the special case of zero-sized allocations + /// correctly. + /// + /// In particular, if the size of the pointee, as reported by [`size_of_val`], is + /// nonzero, then the raw pointer must point to a block of memory allocated by + /// [`Global`]. + /// If the size of the pointee is zero, then the raw pointer can be any + /// sufficiently aligned non-null pointer, e.g. [`ptr::dangling`]. /// - /// The safety conditions are described in the [memory layout] section. /// Note that the [considerations for unsafe code] apply to all `Box` values. /// /// # Examples @@ -1352,9 +1390,16 @@ impl Box { /// memory problems. For example, a double-free may occur if the /// function is called twice on the same `NonNull` pointer. /// - /// The non-null pointer must point to a block of memory allocated by the global allocator. + /// The exact safety requirements are described in the [memory layout] section, together + /// with recommendations about how to handle the special case of zero-sized allocations + /// correctly. + /// + /// In particular, if the size of the pointee, as reported by [`size_of_val`], is + /// nonzero, then the raw pointer must point to a block of memory allocated by + /// [`Global`]. + /// If the size of the pointee is zero, then the raw pointer can be any + /// sufficiently aligned pointer, e.g. [`NonNull::dangling`]. /// - /// The safety conditions are described in the [memory layout] section. /// Note that the [considerations for unsafe code] apply to all `Box` values. /// /// # Examples @@ -1403,6 +1448,11 @@ impl Box { /// [`Box::from_raw`] function, allowing the `Box` destructor to perform /// the cleanup. /// + /// While manually deallocating the pointer through [`deallocate`] + /// is possible, you must only do this if the pointee's size, as reported by + /// [`size_of_val`], is non-zero (the `Box` may be a dangling pointer + /// otherwise). See the [memory layout] section for more details. + /// /// Note: this is an associated function, which means that you have /// to call it as `Box::into_raw(b)` instead of `b.into_raw()`. This /// is so that there is no conflict with a method on the inner type. @@ -1438,6 +1488,7 @@ impl Box { /// ``` /// /// [memory layout]: self#memory-layout + /// [`deallocate`]: Allocator::deallocate #[must_use = "losing the pointer will leak memory"] #[stable(feature = "box_raw", since = "1.4.0")] #[inline] @@ -1465,6 +1516,11 @@ impl Box { /// [`Box::from_non_null`] function, allowing the `Box` destructor to /// perform the cleanup. /// + /// While manually deallocating the pointer through [`deallocate`] + /// is possible, you must only do this if the pointee's size, as reported by + /// [`size_of_val`], is non-zero (the `Box` may be a dangling pointer + /// otherwise). See the [memory layout] section for more details. + /// /// Note: this is an associated function, which means that you have /// to call it as `Box::into_non_null(b)` instead of `b.into_non_null()`. /// This is so that there is no conflict with a method on the inner type. @@ -1499,6 +1555,7 @@ impl Box { /// ``` /// /// [memory layout]: self#memory-layout + /// [`deallocate`]: Allocator::deallocate #[must_use = "losing the pointer will leak memory"] #[stable(feature = "box_vec_non_null", since = "1.99.0")] #[inline] @@ -1517,9 +1574,8 @@ impl Box { /// /// After calling this function, the raw pointer is owned by the /// resulting `Box`. Specifically, the `Box` destructor will call - /// the destructor of `T` and free the allocated memory. For this - /// to be safe, the memory must have been allocated in accordance - /// with the [memory layout] used by `Box` . + /// the destructor of `T` and free the allocated memory, except + /// in the cases described in the [memory layout] section. /// /// # Safety /// @@ -1527,9 +1583,16 @@ impl Box { /// memory problems. For example, a double-free may occur if the /// function is called twice on the same raw pointer. /// - /// The raw pointer must point to a block of memory allocated by `alloc`. + /// The exact safety requirements are described in the [memory layout] section, together + /// with recommendations about how to handle the special case of zero-sized allocations + /// correctly. + /// + /// In particular, if the size of the pointee, as reported by [`size_of_val`], is + /// nonzero, then the raw pointer must point to a block of memory allocated by + /// `alloc`. + /// If the size of the pointee is zero, then the raw pointer can be any + /// sufficiently aligned non-null pointer, e.g. [`ptr::dangling`]. /// - /// The safety conditions are described in the [memory layout] section. /// Note that the [considerations for unsafe code] apply to all `Box` values. /// /// # Examples @@ -1573,9 +1636,8 @@ impl Box { /// /// After calling this function, the `NonNull` pointer is owned by /// the resulting `Box`. Specifically, the `Box` destructor will call - /// the destructor of `T` and free the allocated memory. For this - /// to be safe, the memory must have been allocated in accordance - /// with the [memory layout] used by `Box` . + /// the destructor of `T` and free the allocated memory, except + /// in the cases described in the [memory layout] section. /// /// # Safety /// @@ -1583,9 +1645,16 @@ impl Box { /// memory problems. For example, a double-free may occur if the /// function is called twice on the same raw pointer. /// - /// The non-null pointer must point to a block of memory allocated by `alloc`. + /// The exact safety requirements are described in the [memory layout] section, together + /// with recommendations about how to handle the special case of zero-sized allocations + /// correctly. + /// + /// In particular, if the size of the pointee, as reported by [`size_of_val`], is + /// nonzero, then the raw pointer must point to a block of memory allocated by + /// `alloc`. + /// If the size of the pointee is zero, then the raw pointer can be any + /// sufficiently aligned pointer, e.g. [`NonNull::dangling`]. /// - /// The safety conditions are described in the [memory layout] section. /// Note that the [considerations for unsafe code] apply to all `Box` values. /// /// # Examples @@ -1634,6 +1703,11 @@ impl Box { /// [`Box::from_raw_in`] function, allowing the `Box` destructor to perform /// the cleanup. /// + /// While manually deallocating the pointer through [`deallocate`] + /// is possible, you must only do this if the pointee's size, as reported by + /// [`size_of_val`], is non-zero (the `Box` may be a dangling pointer + /// otherwise). See the [memory layout] section for more details. + /// /// Note: this is an associated function, which means that you have /// to call it as `Box::into_raw_with_allocator(b)` instead of `b.into_raw_with_allocator()`. This /// is so that there is no conflict with a method on the inner type. @@ -1664,6 +1738,7 @@ impl Box { /// ``` /// /// [memory layout]: self#memory-layout + /// [`deallocate`]: Allocator::deallocate #[must_use = "losing the pointer will leak memory"] #[stable(feature = "allocator_api", since = "CURRENT_RUSTC_VERSION")] #[rustc_const_unstable(feature = "const_heap", issue = "79597")] @@ -1693,6 +1768,11 @@ impl Box { /// [`Box::from_non_null_in`] function, allowing the `Box` destructor to /// perform the cleanup. /// + /// While manually deallocating the pointer through [`deallocate`] + /// is possible, you must only do this if the pointee's size, as reported by + /// [`size_of_val`], is non-zero (the `Box` may be a dangling pointer + /// otherwise). See the [memory layout] section for more details. + /// /// Note: this is an associated function, which means that you have /// to call it as `Box::into_non_null_with_allocator(b)` instead of /// `b.into_non_null_with_allocator()`. This is so that there is no @@ -1722,6 +1802,7 @@ impl Box { /// ``` /// /// [memory layout]: self#memory-layout + /// [`deallocate`]: Allocator::deallocate #[must_use = "losing the pointer will leak memory"] #[stable(feature = "allocator_api", since = "CURRENT_RUSTC_VERSION")] #[inline] diff --git a/library/alloc/src/string.rs b/library/alloc/src/string.rs index 03c07b15712bb..1d864b061a825 100644 --- a/library/alloc/src/string.rs +++ b/library/alloc/src/string.rs @@ -940,6 +940,10 @@ impl String { /// into a `String` with the [`from_raw_parts`] function, allowing /// the destructor to perform the cleanup. /// + /// If `capacity == 0`, then `pointer` may be dangling. Otherwise, + /// it will always be valid for reads and writes over `capacity` + /// bytes, and point to the start of the string. + /// /// [`from_raw_parts`]: String::from_raw_parts /// /// # Examples diff --git a/library/alloc/src/vec/mod.rs b/library/alloc/src/vec/mod.rs index f837d963a572e..6970235e65e83 100644 --- a/library/alloc/src/vec/mod.rs +++ b/library/alloc/src/vec/mod.rs @@ -51,25 +51,50 @@ //! //! # Memory layout //! -//! When the type is non-zero-sized and the capacity is nonzero, [`Vec`] uses the [`Global`] -//! allocator for its allocation. It is valid to convert both ways between such a [`Vec`] and a raw -//! pointer allocated with the [`Global`] allocator, provided that the [`Layout`] used with the -//! allocator is correct for a sequence of `capacity` elements of the type, and the first `len` -//! values pointed to by the raw pointer are valid. More precisely, a `ptr: *mut T` that has been -//! allocated with the [`Global`] allocator with [`Layout::array::(capacity)`][Layout::array] may -//! be converted into a vec using -//! [`Vec::::from_raw_parts(ptr, len, capacity)`](Vec::from_raw_parts). Conversely, the memory -//! backing a `value: *mut T` obtained from [`Vec::::as_mut_ptr`] may be deallocated using the -//! [`Global`] allocator with the same layout. +//! A [`Vec`] conceptually consists of a raw pointer `ptr` to the start of the allocation, a +//! length `len`, a capacity `cap`, and an allocator `alloc`. +//! While the exact fields and their order are not specified, a [`Vec`] can be converted to and +//! from these parts using [`Vec::into_raw_parts_with_allocator`], [`Vec::from_raw_parts_in`], +//! [`Vec::into_raw_parts`], and [`Vec::from_raw_parts`], as well as their [`NonNull`]-based +//! versions. //! -//! For zero-sized types (ZSTs), or when the capacity is zero, the `Vec` pointer must be non-null -//! and sufficiently aligned. The recommended way to build a `Vec` of ZSTs if [`vec!`] cannot be -//! used is to use [`ptr::NonNull::dangling`]. +//! The parts of a [`Vec`] have the following invariants: //! +//! * If [`size_of::()`](size_of) and `cap` are non-zero, then: +//! - `ptr` must point to the start of a memory block [*currently allocated*] via `alloc`. +//! - [`Layout::array::(cap)`] must [*fit*] the memory block of `ptr`. In particular, +//! * `T` must have the same alignment as the [`Layout`] `ptr` was allocated with, and +//! * `size_of::() * cap` must be at least the size of the [`Layout`] `ptr` was +//! allocated with, and at most the size of the allocation returned the allocator. +//! - `size_of::() * cap` must be at most [`isize::MAX`]. +//! See the safety documentation of [`pointer::offset`]. +//! * If [`size_of::()`](size_of) or `cap` is zero, then `ptr` must be any sufficiently aligned non-null pointer. +//! +//! NOTE: `ptr` will not be passed to the allocator for deallocation by the destructor of [`Vec`] +//! in this case. Thus, it is unwise to use a pointer acquired from the allocator for this, as +//! overallocated memory may end up being leaked. Use [`ptr::dangling`] instead. +//! * The following must always be satisfied: +//! - `len` needs to be less than or equal to `cap`. +//! - At `ptr`, there are at least `len` valid values of type `T`, laid out contiguously +//! (like an array). These values are considered owned by the [`Vec`]. +//! +//! It is sound to create a [`Vec`] from parts that satisfy these invariants. +//! +//! Conversely, the memory backing `ptr` obtained from [`Vec::as_mut_ptr`] or +//! [`Vec::into_raw_parts_with_allocator`]-like methods may be deallocated +//! using the allocator, with layout [`Layout::array::(cap)`], provided +//! that [`size_of::()`](size_of) or `cap` is non-zero. +//! To automatically handle the case where the allocation is zero-sized, +//! it is recommended to use [`Vec::from_raw_parts_in`], instead of manually +//! deallocating. +//! +//! [*fit*]: crate::alloc::Allocator#memory-fitting +//! [*currently allocated*]: crate::alloc::Allocator#currently-allocated-memory +//! [`allocate`]: crate::alloc::Allocator::allocate //! [`push`]: Vec::push //! [`ptr::NonNull::dangling`]: NonNull::dangling //! [`Layout`]: crate::alloc::Layout -//! [Layout::array]: crate::alloc::Layout::array +//! [`Layout::array::(cap)`]: crate::alloc::Layout::array #![stable(feature = "rust1", since = "1.0.0")] @@ -303,35 +328,13 @@ mod sve_retain; /// /// # Guarantees /// +/// The following section is to be read as an addendum of the +/// [memory layout](self#memory-layout) section. +/// /// Due to its incredibly fundamental nature, `Vec` makes a lot of guarantees /// about its design. This ensures that it's as low-overhead as possible in /// the general case, and can be correctly manipulated in primitive ways -/// by unsafe code. Note that these guarantees refer to an unqualified `Vec`. -/// If additional type parameters are added (e.g., to support custom allocators), -/// overriding their defaults may change the behavior. -/// -/// Most fundamentally, `Vec` is and always will be a (pointer, capacity, length) -/// triplet. No more, no less. The order of these fields is completely -/// unspecified, and you should use the appropriate methods to modify these. -/// The pointer will never be null, so this type is null-pointer-optimized. -/// -/// However, the pointer might not actually point to allocated memory. In particular, -/// if you construct a `Vec` with capacity 0 via [`Vec::new`], [`vec![]`][`vec!`], -/// [`Vec::with_capacity(0)`][`Vec::with_capacity`], or by calling [`shrink_to_fit`] -/// on an empty Vec, it will not allocate memory. Similarly, if you store zero-sized -/// types inside a `Vec`, it will not allocate space for them. *Note that in this case -/// the `Vec` might not report a [`capacity`] of 0*. `Vec` will allocate if and only -/// if [size_of::\]\() * [capacity]\() > 0. In general, `Vec`'s allocation -/// details are very subtle --- if you intend to allocate memory using a `Vec` -/// and use it for something else (either to pass to unsafe code, or to build your -/// own memory-backed collection), be sure to deallocate this memory by using -/// `from_raw_parts` to recover the `Vec` and then dropping it. -/// -/// If a `Vec` *has* allocated memory, then the memory it points to is on the heap -/// (as defined by the allocator Rust is configured to use by default), and its -/// pointer points to [`len`] initialized, contiguous elements in order (what -/// you would see if you coerced it to a slice), followed by [capacity] - [len] -/// logically uninitialized, contiguous elements. +/// by unsafe code. /// /// A vector containing the elements `'a'` and `'b'` with capacity 4 can be /// visualized as below. The top part is the `Vec` struct, it contains a @@ -472,7 +475,7 @@ impl Vec { /// /// The vector will be able to hold at least `capacity` elements without /// reallocating. This method is allowed to allocate for more elements than - /// `capacity`. If `capacity` is zero, the vector will not allocate. + /// `capacity`. If `capacity` is zero, the vector [will not allocate]. /// /// It is important to note that although the returned vector has the /// minimum *capacity* specified, the vector will have a zero *length*. For @@ -482,11 +485,12 @@ impl Vec { /// If it is important to know the exact allocated capacity of a `Vec`, /// always use the [`capacity`] method after construction. /// - /// For `Vec` where `T` is a zero-sized type, there will be no allocation - /// and the capacity will always be `usize::MAX`. + /// For `Vec` where `T` is a zero-sized type, the vector + /// [will not allocate], and the capacity will always be `usize::MAX`. /// /// [Capacity and reallocation]: #capacity-and-reallocation /// [`capacity`]: Vec::capacity + /// [will not allocate]: self#memory-layout /// /// # Panics /// @@ -532,12 +536,14 @@ impl Vec { /// /// The vector will be able to hold at least `capacity` elements without /// reallocating. This method is allowed to allocate for more elements than - /// `capacity`. If `capacity` is zero, the vector will not allocate. + /// `capacity`. If `capacity` is zero, the vector [will not allocate]. /// /// # Errors /// /// Returns an error if the capacity exceeds `isize::MAX` _bytes_, /// or if the allocator reports allocation failure. + /// + /// [will not allocate]: self#memory-layout #[inline] #[unstable(feature = "try_with_capacity", issue = "91913")] pub fn try_with_capacity(capacity: usize) -> Result { @@ -548,34 +554,10 @@ impl Vec { /// /// # Safety /// - /// This is highly unsafe, due to the number of invariants that aren't - /// checked: - /// - /// * If `T` is not a zero-sized type and the capacity is nonzero, `ptr` must have - /// been allocated using the global allocator, such as via the [`alloc::alloc`] - /// function. If `T` is a zero-sized type or the capacity is zero, `ptr` need - /// only be non-null and aligned. - /// * `T` needs to have the same alignment as what `ptr` was allocated with, - /// if the pointer is required to be allocated. - /// (`T` having a less strict alignment is not sufficient, the alignment really - /// needs to be equal to satisfy the [`dealloc`] requirement that memory must be - /// allocated and deallocated with the same layout.) - /// * The size of `T` times the `capacity` (i.e. the allocated size in bytes), if - /// nonzero, needs to be the same size as the pointer was allocated with. - /// (Because similar to alignment, [`dealloc`] must be called with the same - /// layout `size`.) - /// * `length` needs to be less than or equal to `capacity`. - /// * The first `length` values must be properly initialized values of type `T`. - /// * `capacity` needs to be the capacity that the pointer was allocated with, - /// if the pointer is required to be allocated. - /// * The allocated size in bytes must be no larger than `isize::MAX`. - /// See the safety documentation of [`pointer::offset`]. - /// - /// These requirements are always upheld by any `ptr` that has been allocated - /// via `Vec`. Other allocation sources are allowed if the invariants are - /// upheld. - /// - /// Violating these may cause problems like corrupting the allocator's + /// The parts must satisfy the invariants described in the [memory layout] section, + /// with allocator [`Global`]. + /// + /// Violating them may cause problems like corrupting the allocator's /// internal data structures. For example it is normally **not** safe /// to build a `Vec` from a pointer to a C `char` array with length /// `size_t`, doing so is only safe if the array was initially allocated by @@ -594,8 +576,7 @@ impl Vec { /// function. /// /// [`String`]: crate::string::String - /// [`alloc::alloc`]: crate::alloc::alloc - /// [`dealloc`]: crate::alloc::GlobalAlloc::dealloc + /// [memory layout]: self#memory-layout /// /// # Examples /// @@ -655,29 +636,10 @@ impl Vec { /// /// # Safety /// - /// This is highly unsafe, due to the number of invariants that aren't - /// checked: - /// - /// * `ptr` must have been allocated using the global allocator, such as via - /// the [`alloc::alloc`] function. - /// * `T` needs to have the same alignment as what `ptr` was allocated with. - /// (`T` having a less strict alignment is not sufficient, the alignment really - /// needs to be equal to satisfy the [`dealloc`] requirement that memory must be - /// allocated and deallocated with the same layout.) - /// * The size of `T` times the `capacity` (i.e. the allocated size in bytes) needs - /// to be the same size as the pointer was allocated with. (Because similar to - /// alignment, [`dealloc`] must be called with the same layout `size`.) - /// * `length` needs to be less than or equal to `capacity`. - /// * The first `length` values must be properly initialized values of type `T`. - /// * `capacity` needs to be the capacity that the pointer was allocated with. - /// * The allocated size in bytes must be no larger than `isize::MAX`. - /// See the safety documentation of [`pointer::offset`]. - /// - /// These requirements are always upheld by any `ptr` that has been allocated - /// via `Vec`. Other allocation sources are allowed if the invariants are - /// upheld. - /// - /// Violating these may cause problems like corrupting the allocator's + /// The parts must satisfy the invariants described in the [memory layout] section, + /// with allocator [`Global`]. + /// + /// Violating them may cause problems like corrupting the allocator's /// internal data structures. For example it is normally **not** safe /// to build a `Vec` from a pointer to a C `char` array with length /// `size_t`, doing so is only safe if the array was initially allocated by @@ -696,8 +658,7 @@ impl Vec { /// function. /// /// [`String`]: crate::string::String - /// [`alloc::alloc`]: crate::alloc::alloc - /// [`dealloc`]: crate::alloc::GlobalAlloc::dealloc + /// [memory layout]: self#memory-layout /// /// # Examples /// @@ -920,7 +881,7 @@ const impl Vec { /// /// The vector will be able to hold at least `capacity` elements without /// reallocating. This method is allowed to allocate for more elements than - /// `capacity`. If `capacity` is zero, the vector will not allocate. + /// `capacity`. If `capacity` is zero, the vector [will not allocate]. /// /// It is important to note that although the returned vector has the /// minimum *capacity* specified, the vector will have a zero *length*. For @@ -930,11 +891,12 @@ const impl Vec { /// If it is important to know the exact allocated capacity of a `Vec`, /// always use the [`capacity`] method after construction. /// - /// For `Vec` where `T` is a zero-sized type, there will be no allocation - /// and the capacity will always be `usize::MAX`. + /// For `Vec` where `T` is a zero-sized type, the vector + /// [will not allocate], and the capacity will always be `usize::MAX`. /// /// [Capacity and reallocation]: #capacity-and-reallocation /// [`capacity`]: Vec::capacity + /// [will not allocate]: self#memory-layout /// /// # Panics /// @@ -1072,12 +1034,14 @@ impl Vec { /// /// The vector will be able to hold at least `capacity` elements without /// reallocating. This method is allowed to allocate for more elements than - /// `capacity`. If `capacity` is zero, the vector will not allocate. + /// `capacity`. If `capacity` is zero, the vector [will not allocate]. /// /// # Errors /// /// Returns an error if the capacity exceeds `isize::MAX` _bytes_, /// or if the allocator reports allocation failure. + /// + /// [will not allocate]: self#memory-layout #[inline] #[unstable(feature = "allocator_ext", issue = "163177", implied_by = "allocator_api")] // #[unstable(feature = "try_with_capacity", issue = "91913")] @@ -1090,28 +1054,9 @@ impl Vec { /// /// # Safety /// - /// This is highly unsafe, due to the number of invariants that aren't - /// checked: - /// - /// * `ptr` must be [*currently allocated*] via the given allocator `alloc`. - /// * `T` needs to have the same alignment as what `ptr` was allocated with. - /// (`T` having a less strict alignment is not sufficient, the alignment really - /// needs to be equal to satisfy the [`dealloc`] requirement that memory must be - /// allocated and deallocated with the same layout.) - /// * The size of `T` times the `capacity` (i.e. the allocated size in bytes) needs - /// to be the same size as the pointer was allocated with. (Because similar to - /// alignment, [`dealloc`] must be called with the same layout `size`.) - /// * `length` needs to be less than or equal to `capacity`. - /// * The first `length` values must be properly initialized values of type `T`. - /// * `capacity` needs to [*fit*] the layout size that the pointer was allocated with. - /// * The allocated size in bytes must be no larger than `isize::MAX`. - /// See the safety documentation of [`pointer::offset`]. - /// - /// These requirements are always upheld by any `ptr` that has been allocated - /// via `Vec`. Other allocation sources are allowed if the invariants are - /// upheld. - /// - /// Violating these may cause problems like corrupting the allocator's + /// The parts must satisfy the invariants described in the [memory layout] section. + /// + /// Violating them may cause problems like corrupting the allocator's /// internal data structures. For example it is **not** safe /// to build a `Vec` from a pointer to a C `char` array with length `size_t`. /// It's also not safe to build one from a `Vec` and its length, because @@ -1126,9 +1071,8 @@ impl Vec { /// function. /// /// [`String`]: crate::string::String - /// [`dealloc`]: crate::alloc::GlobalAlloc::dealloc - /// [*currently allocated*]: crate::alloc::Allocator#currently-allocated-memory - /// [*fit*]: crate::alloc::Allocator#memory-fitting + /// [`allocate`]: crate::alloc::Allocator::allocate + /// [memory layout]: self#memory-layout /// /// # Examples /// @@ -1204,28 +1148,9 @@ impl Vec { /// /// # Safety /// - /// This is highly unsafe, due to the number of invariants that aren't - /// checked: - /// - /// * `ptr` must be [*currently allocated*] via the given allocator `alloc`. - /// * `T` needs to have the same alignment as what `ptr` was allocated with. - /// (`T` having a less strict alignment is not sufficient, the alignment really - /// needs to be equal to satisfy the [`dealloc`] requirement that memory must be - /// allocated and deallocated with the same layout.) - /// * The size of `T` times the `capacity` (i.e. the allocated size in bytes) needs - /// to be the same size as the pointer was allocated with. (Because similar to - /// alignment, [`dealloc`] must be called with the same layout `size`.) - /// * `length` needs to be less than or equal to `capacity`. - /// * The first `length` values must be properly initialized values of type `T`. - /// * `capacity` needs to [*fit*] the layout size that the pointer was allocated with. - /// * The allocated size in bytes must be no larger than `isize::MAX`. - /// See the safety documentation of [`pointer::offset`]. - /// - /// These requirements are always upheld by any `ptr` that has been allocated - /// via `Vec`. Other allocation sources are allowed if the invariants are - /// upheld. - /// - /// Violating these may cause problems like corrupting the allocator's + /// The parts must satisfy the invariants described in the [memory layout] section. + /// + /// Violating them may cause problems like corrupting the allocator's /// internal data structures. For example it is **not** safe /// to build a `Vec` from a pointer to a C `char` array with length `size_t`. /// It's also not safe to build one from a `Vec` and its length, because @@ -1241,8 +1166,7 @@ impl Vec { /// /// [`String`]: crate::string::String /// [`dealloc`]: crate::alloc::GlobalAlloc::dealloc - /// [*currently allocated*]: crate::alloc::Allocator#currently-allocated-memory - /// [*fit*]: crate::alloc::Allocator#memory-fitting + /// [memory layout]: self#memory-layout /// /// # Examples /// @@ -1902,7 +1826,7 @@ impl Vec { } /// Returns a raw pointer to the vector's buffer, or a dangling raw pointer - /// valid for zero sized reads if the vector didn't allocate. + /// valid for zero sized reads if the vector [didn't allocate]. /// /// The caller must ensure that the vector outlives the pointer this /// function returns, or else it will end up dangling. @@ -1954,6 +1878,7 @@ impl Vec { /// [`as_mut_ptr`]: Vec::as_mut_ptr /// [`as_ptr`]: Vec::as_ptr /// [`as_non_null`]: Vec::as_non_null + /// [didn't allocate]: self#memory-layout #[stable(feature = "vec_as_ptr", since = "1.37.0")] #[rustc_const_stable(feature = "const_vec_string_slice", since = "1.87.0")] #[rustc_never_returns_null_ptr] @@ -1966,7 +1891,7 @@ impl Vec { } /// Returns a raw mutable pointer to the vector's buffer, or a dangling - /// raw pointer valid for zero sized reads if the vector didn't allocate. + /// raw pointer valid for zero sized reads if the vector [didn't allocate]. /// /// The caller must ensure that the vector outlives the pointer this /// function returns, or else it will end up dangling. @@ -2038,6 +1963,7 @@ impl Vec { /// [`as_non_null`]: Vec::as_non_null /// [`dealloc`]: crate::alloc::GlobalAlloc::dealloc /// [`ManuallyDrop`]: core::mem::ManuallyDrop + /// [didn't allocate]: self#memory-layout #[stable(feature = "vec_as_ptr", since = "1.37.0")] #[rustc_const_stable(feature = "const_vec_string_slice", since = "1.87.0")] #[rustc_never_returns_null_ptr] @@ -2050,7 +1976,7 @@ impl Vec { } /// Returns a `NonNull` pointer to the vector's buffer, or a dangling - /// `NonNull` pointer valid for zero sized reads if the vector didn't allocate. + /// `NonNull` pointer valid for zero sized reads if the vector [didn't allocate]. /// /// The caller must ensure that the vector outlives the pointer this /// function returns, or else it will end up dangling. @@ -2105,6 +2031,7 @@ impl Vec { /// [`as_mut_ptr`]: Vec::as_mut_ptr /// [`as_ptr`]: Vec::as_ptr /// [`as_non_null`]: Vec::as_non_null + /// [didn't allocate]: self#memory-layout #[unstable(feature = "vec_as_non_null", issue = "157843")] #[rustc_const_unstable(feature = "vec_as_non_null", issue = "157843")] #[rustc_as_ptr] From 311c6e39ad6481a30ac3ef87735299fed86b6127 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Sun, 11 Oct 2026 20:42:55 +0200 Subject: [PATCH 03/16] Revert "Rollup merge of #159562 - Joel-Wwalker:154071-stdio-handle-cache, r=aapoalas" This reverts commit 556c0e625b9f24942a6ee2cc3ef1683c60868d6a, reversing changes made to 2cb8b181007fd50c394fbbe16b115e942c5b62ab. --- library/std/src/io/stdio.rs | 45 +--------------- library/std/src/sys/stdio/windows.rs | 77 ++++------------------------ 2 files changed, 13 insertions(+), 109 deletions(-) diff --git a/library/std/src/io/stdio.rs b/library/std/src/io/stdio.rs index 0848f97e69d32..527671442fb72 100644 --- a/library/std/src/io/stdio.rs +++ b/library/std/src/io/stdio.rs @@ -95,27 +95,6 @@ const fn stderr_raw() -> StderrRaw { StderrRaw(stdio::Stderr::new()) } -#[cfg(windows)] -impl StdoutRaw { - /// Starts a new lock session: stream state cached per lock session (the - /// handle and its console mode) is re-queried at the next write, so that - /// changing the process stdio handles (e.g. with `SetStdHandle`) between - /// lock sessions keeps working. - #[inline] - fn refresh(&mut self) { - self.0.refresh(); - } -} - -#[cfg(windows)] -impl StderrRaw { - /// See `StdoutRaw::refresh`. - #[inline] - fn refresh(&mut self) { - self.0.refresh(); - } -} - impl Read for StdinRaw { fn read(&mut self, buf: &mut [u8]) -> io::Result { handle_ebadf(self.0.read(buf), || Ok(0)) @@ -790,19 +769,7 @@ impl Stdout { // Locks this handle with 'static lifetime. This depends on the // implementation detail that the underlying `ReentrantMutex` is // static. - let lock = StdoutLock { inner: self.inner.lock() }; - // A new lock session begins, so let Windows re-query cached stream - // state at the next write; other platforms cache nothing, and skip - // this entirely so their `lock()` is unchanged. Skipped also if the - // cell is already borrowed (a nested lock during an ongoing write); - // such a lock is part of the outer session anyway. - #[cfg(windows)] - { - if let Ok(mut w) = lock.inner.try_borrow_mut() { - w.get_mut().refresh(); - } - } - lock + StdoutLock { inner: self.inner.lock() } } } @@ -1034,15 +1001,7 @@ impl Stderr { // Locks this handle with 'static lifetime. This depends on the // implementation detail that the underlying `ReentrantMutex` is // static. - let lock = StderrLock { inner: self.inner.lock() }; - // See `Stdout::lock`: a new lock session begins (Windows only). - #[cfg(windows)] - { - if let Ok(mut w) = lock.inner.try_borrow_mut() { - w.refresh(); - } - } - lock + StderrLock { inner: self.inner.lock() } } } diff --git a/library/std/src/sys/stdio/windows.rs b/library/std/src/sys/stdio/windows.rs index d9c9290ed21a2..1339048a9c039 100644 --- a/library/std/src/sys/stdio/windows.rs +++ b/library/std/src/sys/stdio/windows.rs @@ -22,35 +22,12 @@ pub struct Stdin { pub struct Stdout { incomplete_utf8: IncompleteUtf8, - write_mode: Option, } pub struct Stderr { incomplete_utf8: IncompleteUtf8, - write_mode: Option, } -/// How to write to a standard stream, cached for the duration of a lock -/// session (see `Stdout::refresh`). Re-querying this on every lock -/// acquisition keeps `SetStdHandle` calls made between lock sessions working -/// (see #40490), while a held lock skips the per-write console queries that -/// otherwise dominate bulk writes (see #154071). -#[derive(Clone, Copy)] -enum WriteMode { - /// The stream is a pipe or file, or a console using the UTF-8 code page: - /// bytes are written out unchanged. - Passthrough(c::HANDLE), - /// The stream is a console using a non-UTF-8 code page: data is converted - /// to UTF-16 and written with `WriteConsoleW`. - Utf16Console(c::HANDLE), -} - -// SAFETY: a `HANDLE` stored here is only an OS handle value for a standard -// stream (it is never dereferenced), and handle values are valid -// process-wide, on any thread. -unsafe impl Send for WriteMode {} -unsafe impl Sync for WriteMode {} - struct IncompleteUtf8 { bytes: [u8; 4], len: u8, @@ -122,40 +99,21 @@ fn is_utf8_console() -> bool { false } -fn write( - handle_id: u32, - data: &[u8], - incomplete_utf8: &mut IncompleteUtf8, - write_mode: &mut Option, -) -> io::Result { +fn write(handle_id: u32, data: &[u8], incomplete_utf8: &mut IncompleteUtf8) -> io::Result { if data.is_empty() { return Ok(0); } - let mode = match *write_mode { - Some(mode) => mode, - None => { - let handle = get_handle(handle_id)?; - let mode = if !is_console(handle) || is_utf8_console() { - WriteMode::Passthrough(handle) - } else { - WriteMode::Utf16Console(handle) - }; - // Only cache success; if there is no handle, keep erroring on - // every write like before. - *write_mode = Some(mode); - mode - } - }; - - match mode { - WriteMode::Passthrough(handle) => unsafe { + let handle = get_handle(handle_id)?; + if !is_console(handle) || is_utf8_console() { + unsafe { let handle = Handle::from_raw_handle(handle); let ret = handle.write(data); let _ = handle.into_raw_handle(); // Don't close the handle ret - }, - WriteMode::Utf16Console(handle) => write_console_utf16(data, incomplete_utf8, handle), + } + } else { + write_console_utf16(data, incomplete_utf8, handle) } } @@ -481,21 +439,13 @@ impl IncompleteUtf8 { impl Stdout { pub const fn new() -> Stdout { - Stdout { incomplete_utf8: IncompleteUtf8::new(), write_mode: None } - } - - /// Forgets the cached stream state, so the next write re-queries the OS. - /// Called when a new lock session begins, so that changing the process - /// stdio handles (e.g. `SetStdHandle`) between lock sessions keeps - /// working (see #40490). - pub fn refresh(&mut self) { - self.write_mode = None; + Stdout { incomplete_utf8: IncompleteUtf8::new() } } } impl io::Write for Stdout { fn write(&mut self, buf: &[u8]) -> io::Result { - write(c::STD_OUTPUT_HANDLE, buf, &mut self.incomplete_utf8, &mut self.write_mode) + write(c::STD_OUTPUT_HANDLE, buf, &mut self.incomplete_utf8) } fn flush(&mut self) -> io::Result<()> { @@ -505,18 +455,13 @@ impl io::Write for Stdout { impl Stderr { pub const fn new() -> Stderr { - Stderr { incomplete_utf8: IncompleteUtf8::new(), write_mode: None } - } - - /// See `Stdout::refresh`. - pub fn refresh(&mut self) { - self.write_mode = None; + Stderr { incomplete_utf8: IncompleteUtf8::new() } } } impl io::Write for Stderr { fn write(&mut self, buf: &[u8]) -> io::Result { - write(c::STD_ERROR_HANDLE, buf, &mut self.incomplete_utf8, &mut self.write_mode) + write(c::STD_ERROR_HANDLE, buf, &mut self.incomplete_utf8) } fn flush(&mut self) -> io::Result<()> { From 9a20a071c20fe5bd68004b5d1aee909c21c46e33 Mon Sep 17 00:00:00 2001 From: Raushan singh Date: Sat, 12 Sep 2026 13:53:31 +0000 Subject: [PATCH 04/16] test(parser): add baseline for missing turbofish with lifetime in struct expr --- .../recover/missing-turbofish-lifetime.rs | 30 ++++++++++ .../recover/missing-turbofish-lifetime.stderr | 57 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 tests/ui/parser/recover/missing-turbofish-lifetime.rs create mode 100644 tests/ui/parser/recover/missing-turbofish-lifetime.stderr diff --git a/tests/ui/parser/recover/missing-turbofish-lifetime.rs b/tests/ui/parser/recover/missing-turbofish-lifetime.rs new file mode 100644 index 0000000000000..596b197a317da --- /dev/null +++ b/tests/ui/parser/recover/missing-turbofish-lifetime.rs @@ -0,0 +1,30 @@ +struct Struct<'a> { + string: &'a str, +} + +fn struct_with_reserved_lifetime() { + let _ = Struct<'_> { + //~^ ERROR labels cannot use keyword names + //~| ERROR expected `while`, `for`, `loop` or `{` after a label + //~| ERROR comparison operators cannot be chained + string: "", + }; +} + +fn struct_with_named_lifetime() { + let _ = Struct<'a> { + //~^ ERROR expected `while`, `for`, `loop` or `{` after a label + //~| ERROR comparison operators cannot be chained + string: "", + }; +} + +fn struct_with_multichar_lifetime() { + let _ = Struct<'abc> { + //~^ ERROR expected `while`, `for`, `loop` or `{` after a label + //~| ERROR comparison operators cannot be chained + string: "", + }; +} + +fn main() {} diff --git a/tests/ui/parser/recover/missing-turbofish-lifetime.stderr b/tests/ui/parser/recover/missing-turbofish-lifetime.stderr new file mode 100644 index 0000000000000..e14d9d6aa464c --- /dev/null +++ b/tests/ui/parser/recover/missing-turbofish-lifetime.stderr @@ -0,0 +1,57 @@ +error: labels cannot use keyword names + --> $DIR/missing-turbofish-lifetime.rs:6:20 + | +LL | let _ = Struct<'_> { + | ^^ + +error: expected `while`, `for`, `loop` or `{` after a label + --> $DIR/missing-turbofish-lifetime.rs:6:22 + | +LL | let _ = Struct<'_> { + | ^ expected `while`, `for`, `loop` or `{` after a label + | +help: add `'` to close the char literal + | +LL | let _ = Struct<'_'> { + | + + +error: comparison operators cannot be chained + --> $DIR/missing-turbofish-lifetime.rs:6:19 + | +LL | let _ = Struct<'_> { + | ^ ^ + +error: expected `while`, `for`, `loop` or `{` after a label + --> $DIR/missing-turbofish-lifetime.rs:15:22 + | +LL | let _ = Struct<'a> { + | ^ expected `while`, `for`, `loop` or `{` after a label + | +help: add `'` to close the char literal + | +LL | let _ = Struct<'a'> { + | + + +error: comparison operators cannot be chained + --> $DIR/missing-turbofish-lifetime.rs:15:19 + | +LL | let _ = Struct<'a> { + | ^ ^ + +error: expected `while`, `for`, `loop` or `{` after a label + --> $DIR/missing-turbofish-lifetime.rs:23:24 + | +LL | let _ = Struct<'abc> { + | ^ expected `while`, `for`, `loop` or `{` after a label + +error: comparison operators cannot be chained + --> $DIR/missing-turbofish-lifetime.rs:23:19 + | +LL | let _ = Struct<'abc> { + | ^ ^ + | + = help: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + = help: or use `(...)` if you meant to specify fn arguments + +error: aborting due to 7 previous errors + From 508d2f42744343a6096b0e642565c9c919dadb58 Mon Sep 17 00:00:00 2001 From: Robin <1187338299@qq.com> Date: Tue, 13 Oct 2026 18:00:21 +0800 Subject: [PATCH 05/16] test DSL eagerly handle false constraints --- compiler/rustc_ast/src/ast.rs | 1 + compiler/rustc_ast_lowering/src/item.rs | 3 ++- compiler/rustc_hir/src/hir.rs | 1 + compiler/rustc_hir/src/intravisit.rs | 2 +- .../rustc_hir_analysis/src/check/wfcheck.rs | 17 ++++++++++++++++- compiler/rustc_hir_analysis/src/collect.rs | 4 ++-- compiler/rustc_parse/src/parser/item.rs | 4 +++- .../test-forall-evaluates-to-false.rs | 15 +++++++++++++++ .../test-forall-evaluates-to-false.stderr | 8 ++++++++ .../test-top-level-false-constraint.rs | 11 +++++++++++ .../test-top-level-false-constraint.stderr | 8 ++++++++ 11 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 tests/ui/assumptions_on_binders/test-forall-evaluates-to-false.rs create mode 100644 tests/ui/assumptions_on_binders/test-forall-evaluates-to-false.stderr create mode 100644 tests/ui/assumptions_on_binders/test-top-level-false-constraint.rs create mode 100644 tests/ui/assumptions_on_binders/test-top-level-false-constraint.stderr diff --git a/compiler/rustc_ast/src/ast.rs b/compiler/rustc_ast/src/ast.rs index 4c7ae87556f76..4effdd42f347a 100644 --- a/compiler/rustc_ast/src/ast.rs +++ b/compiler/rustc_ast/src/ast.rs @@ -4073,6 +4073,7 @@ pub struct TestBinderConstraints { #[derive(Clone, Encodable, Decodable, Debug, Walkable)] pub struct TestBinderBody { + pub span: Span, pub foralls: ThinVec, pub exists: ThinVec, pub constraints: Vec, diff --git a/compiler/rustc_ast_lowering/src/item.rs b/compiler/rustc_ast_lowering/src/item.rs index babca94628891..8203e6f98bd4f 100644 --- a/compiler/rustc_ast_lowering/src/item.rs +++ b/compiler/rustc_ast_lowering/src/item.rs @@ -2005,7 +2005,8 @@ impl<'hir> LoweringContext<'_, 'hir> { .iter() .map(|predicate| self.lower_where_predicate(predicate, &[], &mut dedup_map)), ); - hir::TestBinderBody { foralls, exists, constraints, predicates } + let span = self.lower_span(body.span); + hir::TestBinderBody { foralls, exists, constraints, predicates, span } } fn lower_test_binder_forall( diff --git a/compiler/rustc_hir/src/hir.rs b/compiler/rustc_hir/src/hir.rs index dfcb8871fe982..70702c5d42b59 100644 --- a/compiler/rustc_hir/src/hir.rs +++ b/compiler/rustc_hir/src/hir.rs @@ -4527,6 +4527,7 @@ impl FnHeader { #[derive(Debug, Clone, Copy, StableHash)] pub struct TestBinderBody<'hir> { + pub span: Span, pub foralls: &'hir [TestBinderForall<'hir>], pub exists: &'hir [TestBinderExists<'hir>], /// Constraints to be inserted directly into constraint storage to be proven diff --git a/compiler/rustc_hir/src/intravisit.rs b/compiler/rustc_hir/src/intravisit.rs index ab81862b74e32..ba82a1adc7d48 100644 --- a/compiler/rustc_hir/src/intravisit.rs +++ b/compiler/rustc_hir/src/intravisit.rs @@ -1600,7 +1600,7 @@ pub fn walk_test_binder_body<'v, V: Visitor<'v>>( visitor: &mut V, body: &'v TestBinderBody<'v>, ) -> V::Result { - let TestBinderBody { foralls, exists, constraints, predicates } = body; + let TestBinderBody { span: _, foralls, exists, constraints, predicates } = body; walk_list!(visitor, visit_test_binder_forall, *foralls); walk_list!(visitor, visit_test_binder_exists, *exists); try_visit!(visitor.visit_test_binder_constraint(&constraints)); diff --git a/compiler/rustc_hir_analysis/src/check/wfcheck.rs b/compiler/rustc_hir_analysis/src/check/wfcheck.rs index 0599f036848c5..1aa194d7e0013 100644 --- a/compiler/rustc_hir_analysis/src/check/wfcheck.rs +++ b/compiler/rustc_hir_analysis/src/check/wfcheck.rs @@ -2379,7 +2379,7 @@ impl<'tcx> WfCheckingCtxt<'_, 'tcx> { #[instrument(level = "debug", skip(self))] pub(super) fn check_test_binder_body(&self, body: TestBinderBody<'tcx>) { - let TestBinderBody { foralls, exists, constraints, predicates } = body; + let TestBinderBody { span, foralls, exists, constraints, predicates } = body; if !predicates.is_empty() { for (predicate, span) in predicates { let cause = traits::ObligationCause::misc(span, self.body_def_id); @@ -2399,6 +2399,13 @@ impl<'tcx> WfCheckingCtxt<'_, 'tcx> { Ok(()) => constraints, Err(_guar) => ty::region_constraint::RegionConstraint::new_true(), }; + if constraints.is_false() { + self.tcx() + .dcx() + .struct_span_err(span, "top level false constraints are not supported") + .emit(); + return; + } self.infcx.register_solver_region_constraint(constraints); @@ -2478,6 +2485,13 @@ impl<'tcx> WfCheckingCtxt<'_, 'tcx> { u, ) .with_spans(forall.span); + if constraint.is_false() { + self.tcx().dcx().struct_span_err(forall.span, "forall evaluated to false").emit(); + self.infcx.overwrite_solver_region_constraint( + ty::region_constraint::RegionConstraint::new_true(), + ); + return; + } if let Some(assert_on_exit) = &forall.assert_on_exit { self.check_test_binder_region_constraints(forall.span, assert_on_exit, &constraint); } @@ -2797,6 +2811,7 @@ struct RedundantLifetimeArgsLint<'tcx> { #[derive(Clone, Debug, TypeFoldable, TypeVisitable)] pub(crate) struct TestBinderBody<'tcx> { + pub span: Span, pub foralls: Vec>, pub exists: Vec>, /// Constraints to be inserted directly into constraint storage to be proven diff --git a/compiler/rustc_hir_analysis/src/collect.rs b/compiler/rustc_hir_analysis/src/collect.rs index 1965ade38204e..7f181ef2e7e04 100644 --- a/compiler/rustc_hir_analysis/src/collect.rs +++ b/compiler/rustc_hir_analysis/src/collect.rs @@ -328,7 +328,7 @@ impl<'tcx> ItemCtxt<'tcx> { &self, item: &hir::TestBinderBody<'tcx>, ) -> TestBinderBody<'tcx> { - let hir::TestBinderBody { foralls, exists, constraints, predicates } = item; + let hir::TestBinderBody { span, foralls, exists, constraints, predicates } = item; let foralls = foralls.iter().map(|forall| self.lower_test_binder_forall(forall)).collect(); let exists = exists.iter().map(|exists| self.lower_test_binder_exists(exists)).collect(); let constraints = self.lower_test_binder_constraint(&constraints); @@ -337,7 +337,7 @@ impl<'tcx> ItemCtxt<'tcx> { clauses_of::where_predicate_clauses(self, predicate, &mut clauses); } let predicates = clauses.into_iter().map(|(c, span)| (c.kind(), span)).collect(); - TestBinderBody { foralls, exists, constraints, predicates } + TestBinderBody { span: *span, foralls, exists, constraints, predicates } } #[instrument(level = "debug", skip(self), ret)] diff --git a/compiler/rustc_parse/src/parser/item.rs b/compiler/rustc_parse/src/parser/item.rs index 6d3d79bbe2b5c..e7e0434c2fa2e 100644 --- a/compiler/rustc_parse/src/parser/item.rs +++ b/compiler/rustc_parse/src/parser/item.rs @@ -2717,6 +2717,7 @@ impl<'a> Parser<'a> { let mut exists = ThinVec::new(); let mut constraints = Vec::new(); let mut predicates = Vec::new(); + let mut span = self.token.span; self.parse_delim_comma_seq(exp!(OpenBrace), exp!(CloseBrace), |this| { match this.token.ident() { Some((Ident { name: sym::predicates, .. }, IdentKind::Normal)) => { @@ -2735,7 +2736,8 @@ impl<'a> Parser<'a> { } Ok(()) })?; - Ok(TestBinderBody { foralls, exists, constraints, predicates }) + span = span.to(self.prev_token.span); + Ok(TestBinderBody { foralls, exists, constraints, predicates, span }) } pub fn parse_test_binder_forall(&mut self) -> PResult<'a, TestBinderForall> { diff --git a/tests/ui/assumptions_on_binders/test-forall-evaluates-to-false.rs b/tests/ui/assumptions_on_binders/test-forall-evaluates-to-false.rs new file mode 100644 index 0000000000000..c98afb8474448 --- /dev/null +++ b/tests/ui/assumptions_on_binders/test-forall-evaluates-to-false.rs @@ -0,0 +1,15 @@ +//@ compile-flags: -Zassumptions-on-binders -Znext-solver=globally + +#![feature(test_binder_constraints, non_lifetime_binders)] +#![expect(incomplete_features)] + +core::test_binder_constraints! { + impl<'a> { + forall<'b> { + //~^ ERROR forall evaluated to false + 'b: 'a + } + } +} + +fn main() {} diff --git a/tests/ui/assumptions_on_binders/test-forall-evaluates-to-false.stderr b/tests/ui/assumptions_on_binders/test-forall-evaluates-to-false.stderr new file mode 100644 index 0000000000000..995363d521621 --- /dev/null +++ b/tests/ui/assumptions_on_binders/test-forall-evaluates-to-false.stderr @@ -0,0 +1,8 @@ +error: forall evaluated to false + --> $DIR/test-forall-evaluates-to-false.rs:8:9 + | +LL | forall<'b> { + | ^^^^^^ + +error: aborting due to 1 previous error + diff --git a/tests/ui/assumptions_on_binders/test-top-level-false-constraint.rs b/tests/ui/assumptions_on_binders/test-top-level-false-constraint.rs new file mode 100644 index 0000000000000..cdd7eb7a321bc --- /dev/null +++ b/tests/ui/assumptions_on_binders/test-top-level-false-constraint.rs @@ -0,0 +1,11 @@ +//@ compile-flags: -Zassumptions-on-binders -Znext-solver=globally + +#![feature(test_binder_constraints, non_lifetime_binders)] +#![expect(incomplete_features)] + +core::test_binder_constraints! { + impl { or {} } + //~^ ERROR top level false constraints are not supported +} + +fn main() {} diff --git a/tests/ui/assumptions_on_binders/test-top-level-false-constraint.stderr b/tests/ui/assumptions_on_binders/test-top-level-false-constraint.stderr new file mode 100644 index 0000000000000..fb5380879fe3d --- /dev/null +++ b/tests/ui/assumptions_on_binders/test-top-level-false-constraint.stderr @@ -0,0 +1,8 @@ +error: top level false constraints are not supported + --> $DIR/test-top-level-false-constraint.rs:7:10 + | +LL | impl { or {} } + | ^^^^^^^^^ + +error: aborting due to 1 previous error + From ebca0756e0812a8c971788d2df71bcec70f9aa66 Mon Sep 17 00:00:00 2001 From: Raushan singh Date: Sat, 3 Oct 2026 03:43:17 +0000 Subject: [PATCH 06/16] fix(parser): recover missing turbofish for lifetimes in exprs --- compiler/rustc_parse/src/diagnostics.rs | 9 ++ .../rustc_parse/src/parser/diagnostics.rs | 7 + compiler/rustc_parse/src/parser/path.rs | 17 +++ tests/ui/parser/issues/issue-93282.rs | 8 +- tests/ui/parser/issues/issue-93282.stderr | 47 ++++--- .../missing-turbofish-lifetime-macro.rs | 16 +++ .../missing-turbofish-lifetime-macro.stderr | 20 +++ .../recover/missing-turbofish-lifetime.fixed | 87 ++++++++++++ .../recover/missing-turbofish-lifetime.rs | 75 +++++++++-- .../recover/missing-turbofish-lifetime.stderr | 127 +++++++++++++----- .../require-parens-for-chained-comparison.rs | 16 +-- ...quire-parens-for-chained-comparison.stderr | 67 ++++----- 12 files changed, 375 insertions(+), 121 deletions(-) create mode 100644 tests/ui/parser/recover/missing-turbofish-lifetime-macro.rs create mode 100644 tests/ui/parser/recover/missing-turbofish-lifetime-macro.stderr create mode 100644 tests/ui/parser/recover/missing-turbofish-lifetime.fixed diff --git a/compiler/rustc_parse/src/diagnostics.rs b/compiler/rustc_parse/src/diagnostics.rs index 54d91eca01327..a942d6b060dab 100644 --- a/compiler/rustc_parse/src/diagnostics.rs +++ b/compiler/rustc_parse/src/diagnostics.rs @@ -1712,6 +1712,15 @@ pub(crate) struct GenericParamsWithoutAngleBrackets { pub sugg: GenericParamsWithoutAngleBracketsSugg, } +#[derive(Diagnostic)] +#[diag("use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments")] +pub(crate) struct MissingTurbofishForLifetime { + #[primary_span] + pub span: Span, + #[suggestion("add `::`", style = "verbose", code = "::", applicability = "machine-applicable")] + pub suggest_turbofish: Span, +} + #[derive(Subdiagnostic)] #[multipart_suggestion( "surround the type parameters with angle brackets", diff --git a/compiler/rustc_parse/src/parser/diagnostics.rs b/compiler/rustc_parse/src/parser/diagnostics.rs index 14ea328601a02..18221f2fb8d7e 100644 --- a/compiler/rustc_parse/src/parser/diagnostics.rs +++ b/compiler/rustc_parse/src/parser/diagnostics.rs @@ -1143,6 +1143,13 @@ impl<'a> Parser<'a> { } } + pub(super) fn recover_missing_turbofish_for_lifetimes(&self, lt_span: Span) { + self.dcx().emit_err(crate::diagnostics::MissingTurbofishForLifetime { + span: lt_span, + suggest_turbofish: lt_span.shrink_to_lo(), + }); + } + /// When writing a turbofish with multiple type parameters missing the leading `::`, we will /// encounter a parse error when encountering the first `,`. pub(super) fn check_mistyped_turbofish_with_multiple_type_params( diff --git a/compiler/rustc_parse/src/parser/path.rs b/compiler/rustc_parse/src/parser/path.rs index 99bf592c859fb..b9ba5532f5708 100644 --- a/compiler/rustc_parse/src/parser/path.rs +++ b/compiler/rustc_parse/src/parser/path.rs @@ -303,9 +303,20 @@ impl<'a> Parser<'a> { is_args_start(&this.token) }; + // A lifetime followed by `>` or `,` can never start a valid expression, so `<` here + // must be the start of generic arguments with a missing `::`. + let is_missing_turbofish_for_lifetime = |this: &mut Self| { + this.token == token::Lt + && this.may_recover() + && style == PathStyle::Expr + && this.look_ahead(1, |t| t.is_lifetime()) + && this.look_ahead(2, |t| matches!(t.kind, token::Gt | token::Comma)) + }; + Ok( if style == PathStyle::Type && check_args_start(self) || style != PathStyle::Mod && self.check_path_sep_and_look_ahead(is_args_start) + || is_missing_turbofish_for_lifetime(self) { // We use `style == PathStyle::Expr` to check if this is in a recursion or not. If // it isn't, then we reset the unmatched angle bracket count as we're about to start @@ -314,12 +325,18 @@ impl<'a> Parser<'a> { self.unmatched_angle_bracket_count = 0; } + let missing_turbofish = + style == PathStyle::Expr && !self.check_path_sep_and_look_ahead(is_args_start); + // Generic arguments are found - `<`, `(`, `::<` or `::(`. // First, eat `::` if it exists. let _ = self.eat_path_sep(); let lo = self.token.span; let args = if self.eat_lt() { + if missing_turbofish { + self.recover_missing_turbofish_for_lifetimes(lo); + } // `<'a, T, A = U>` let args = self.parse_angle_args_with_leading_angle_bracket_recovery( style, diff --git a/tests/ui/parser/issues/issue-93282.rs b/tests/ui/parser/issues/issue-93282.rs index 274245f1a465f..5b96969f5b971 100644 --- a/tests/ui/parser/issues/issue-93282.rs +++ b/tests/ui/parser/issues/issue-93282.rs @@ -1,7 +1,10 @@ fn main() { + //~^ HELP consider introducing lifetime `'a` here f<'a,> - //~^ ERROR expected - //~| ERROR expected + //~^ ERROR use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + //~| HELP add `::` + //~| ERROR use of undeclared lifetime name `'a` + //~| ERROR cannot find value `f` in this scope } fn bar(a: usize, b: usize) -> usize { @@ -12,5 +15,6 @@ fn foo() { let x = 1; bar('y, x); //~^ ERROR expected + //~| HELP add `'` to close the char literal //~| ERROR mismatched types } diff --git a/tests/ui/parser/issues/issue-93282.stderr b/tests/ui/parser/issues/issue-93282.stderr index c6140bb821e48..1daacff52f066 100644 --- a/tests/ui/parser/issues/issue-93282.stderr +++ b/tests/ui/parser/issues/issue-93282.stderr @@ -1,27 +1,16 @@ -error: expected `while`, `for`, `loop` or `{` after a label - --> $DIR/issue-93282.rs:2:9 - | -LL | f<'a,> - | ^ expected `while`, `for`, `loop` or `{` after a label - | -help: add `'` to close the char literal - | -LL | f<'a',> - | + - -error: expected one of `.`, `:`, `;`, `?`, `for`, `loop`, `while`, `}`, or an operator, found `,` - --> $DIR/issue-93282.rs:2:9 +error: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + --> $DIR/issue-93282.rs:3:6 | LL | f<'a,> - | ^ expected one of 9 possible tokens + | ^ | -help: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments +help: add `::` | LL | f::<'a,> | ++ error: expected `while`, `for`, `loop` or `{` after a label - --> $DIR/issue-93282.rs:13:11 + --> $DIR/issue-93282.rs:16:11 | LL | bar('y, x); | ^ expected `while`, `for`, `loop` or `{` after a label @@ -31,8 +20,25 @@ help: add `'` to close the char literal LL | bar('y', x); | + +error[E0261]: use of undeclared lifetime name `'a` + --> $DIR/issue-93282.rs:3:7 + | +LL | f<'a,> + | ^^ undeclared lifetime + | +help: consider introducing lifetime `'a` here + | +LL | fn main<'a>() { + | ++++ + +error[E0425]: cannot find value `f` in this scope + --> $DIR/issue-93282.rs:3:5 + | +LL | f<'a,> + | ^ not found in this scope + error[E0308]: mismatched types - --> $DIR/issue-93282.rs:13:9 + --> $DIR/issue-93282.rs:16:9 | LL | bar('y, x); | --- ^^ expected `usize`, found `char` @@ -40,11 +46,12 @@ LL | bar('y, x); | arguments to this function are incorrect | note: function defined here - --> $DIR/issue-93282.rs:7:4 + --> $DIR/issue-93282.rs:10:4 | LL | fn bar(a: usize, b: usize) -> usize { | ^^^ -------- -error: aborting due to 4 previous errors +error: aborting due to 5 previous errors -For more information about this error, try `rustc --explain E0308`. +Some errors have detailed explanations: E0261, E0308, E0425. +For more information about an error, try `rustc --explain E0261`. diff --git a/tests/ui/parser/recover/missing-turbofish-lifetime-macro.rs b/tests/ui/parser/recover/missing-turbofish-lifetime-macro.rs new file mode 100644 index 0000000000000..366fc8ddfd982 --- /dev/null +++ b/tests/ui/parser/recover/missing-turbofish-lifetime-macro.rs @@ -0,0 +1,16 @@ +macro_rules! m { + ($e:expr) => { "expr" }; + (a < $l:lifetime >) => { "lifetime" }; +} + +fn test_macro() { + // Ensure turbofish recovery is disabled during macro matching to avoid spurious diagnostics. + // The second arm does NOT match; the expr-fragment error is a hard error. + let _ = m!(a < 'x >); + //~^ ERROR comparison operators cannot be chained + //~| HELP use `::<...>` instead of `<...>` + //~| HELP or use `(...)` + //~| ERROR expected `while`, `for`, `loop` or `{` after a label +} + +fn main() {} diff --git a/tests/ui/parser/recover/missing-turbofish-lifetime-macro.stderr b/tests/ui/parser/recover/missing-turbofish-lifetime-macro.stderr new file mode 100644 index 0000000000000..a05d7e668a521 --- /dev/null +++ b/tests/ui/parser/recover/missing-turbofish-lifetime-macro.stderr @@ -0,0 +1,20 @@ +error: expected `while`, `for`, `loop` or `{` after a label + --> $DIR/missing-turbofish-lifetime-macro.rs:9:23 + | +LL | let _ = m!(a < 'x >); + | ^ expected `while`, `for`, `loop` or `{` after a label + +error: comparison operators cannot be chained + --> $DIR/missing-turbofish-lifetime-macro.rs:9:18 + | +LL | ($e:expr) => { "expr" }; + | ------- while parsing argument for this `expr` macro fragment +... +LL | let _ = m!(a < 'x >); + | ^ ^ + | + = help: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + = help: or use `(...)` if you meant to specify fn arguments + +error: aborting due to 2 previous errors + diff --git a/tests/ui/parser/recover/missing-turbofish-lifetime.fixed b/tests/ui/parser/recover/missing-turbofish-lifetime.fixed new file mode 100644 index 0000000000000..f5d8e2e63e6d5 --- /dev/null +++ b/tests/ui/parser/recover/missing-turbofish-lifetime.fixed @@ -0,0 +1,87 @@ +//! Regression test for +//! +//! Test that omitting the turbofish when passing a lifetime to an expression +//! emits a targeted suggestion rather than a cascading syntax error. + +#![allow(dead_code)] + +//@ run-rustfix + +struct Struct<'a> { + string: &'a str, +} + +fn struct_with_reserved_lifetime() { + let _ = Struct::<'_> { + //~^ ERROR use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + //~| HELP add `::` + string: "", + }; +} + +fn struct_with_named_lifetime<'a>() { + let _ = Struct::<'a> { + //~^ ERROR use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + //~| HELP add `::` + string: "", + }; +} + +fn struct_with_multichar_lifetime<'abc>() { + let _ = Struct::<'abc> { + //~^ ERROR use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + //~| HELP add `::` + string: "", + }; +} + +struct TupleStruct<'a>(&'a str); + +fn tuple_struct_with_reserved_lifetime() { + let _ = TupleStruct::<'_>(""); + //~^ ERROR use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + //~| HELP add `::` +} + +struct Wrapper<'a>(&'a str); +impl<'a> Wrapper<'a> { fn new(s: &'a str) -> Self { Self(s) } } + +struct Struct2<'a> { x: i32, _p: std::marker::PhantomData<&'a ()> } +impl<'a> Struct2<'a> { fn method(&self) -> Option<()> { Some(()) } } + +fn f<'a: 'a>() {} + +struct StructT<'a, T> { a: &'a str, b: T } +struct Struct3<'a, 'b> { a: &'a str, b: &'b str } + +fn chaining_cases<'a, 'b, T>() -> Option<()> { + let _ = Wrapper::<'a>::new("hi"); + //~^ ERROR use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + //~| HELP add `::` + + let _ = Struct2::<'a> { x: 1, _p: std::marker::PhantomData }.method(); + //~^ ERROR use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + //~| HELP add `::` + + let _ = Struct2::<'a> { x: 1, _p: std::marker::PhantomData }.method()?; + //~^ ERROR use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + //~| HELP add `::` + + f::<'_>(); + //~^ ERROR use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + //~| HELP add `::` + + // struct with two lifetimes + let _ = Struct3::<'a, 'b> { a: "hi", b: "hi" }; + //~^ ERROR use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + //~| HELP add `::` + + // struct with lifetime and type + let _ = StructT::<'a, &str> { a: "hi", b: "hi" }; + //~^ ERROR use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + //~| HELP add `::` + + Some(()) +} + +fn main() {} diff --git a/tests/ui/parser/recover/missing-turbofish-lifetime.rs b/tests/ui/parser/recover/missing-turbofish-lifetime.rs index 596b197a317da..1bc9e97878739 100644 --- a/tests/ui/parser/recover/missing-turbofish-lifetime.rs +++ b/tests/ui/parser/recover/missing-turbofish-lifetime.rs @@ -1,30 +1,87 @@ +//! Regression test for +//! +//! Test that omitting the turbofish when passing a lifetime to an expression +//! emits a targeted suggestion rather than a cascading syntax error. + +#![allow(dead_code)] + +//@ run-rustfix + struct Struct<'a> { string: &'a str, } fn struct_with_reserved_lifetime() { let _ = Struct<'_> { - //~^ ERROR labels cannot use keyword names - //~| ERROR expected `while`, `for`, `loop` or `{` after a label - //~| ERROR comparison operators cannot be chained + //~^ ERROR use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + //~| HELP add `::` string: "", }; } -fn struct_with_named_lifetime() { +fn struct_with_named_lifetime<'a>() { let _ = Struct<'a> { - //~^ ERROR expected `while`, `for`, `loop` or `{` after a label - //~| ERROR comparison operators cannot be chained + //~^ ERROR use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + //~| HELP add `::` string: "", }; } -fn struct_with_multichar_lifetime() { +fn struct_with_multichar_lifetime<'abc>() { let _ = Struct<'abc> { - //~^ ERROR expected `while`, `for`, `loop` or `{` after a label - //~| ERROR comparison operators cannot be chained + //~^ ERROR use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + //~| HELP add `::` string: "", }; } +struct TupleStruct<'a>(&'a str); + +fn tuple_struct_with_reserved_lifetime() { + let _ = TupleStruct<'_>(""); + //~^ ERROR use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + //~| HELP add `::` +} + +struct Wrapper<'a>(&'a str); +impl<'a> Wrapper<'a> { fn new(s: &'a str) -> Self { Self(s) } } + +struct Struct2<'a> { x: i32, _p: std::marker::PhantomData<&'a ()> } +impl<'a> Struct2<'a> { fn method(&self) -> Option<()> { Some(()) } } + +fn f<'a: 'a>() {} + +struct StructT<'a, T> { a: &'a str, b: T } +struct Struct3<'a, 'b> { a: &'a str, b: &'b str } + +fn chaining_cases<'a, 'b, T>() -> Option<()> { + let _ = Wrapper<'a>::new("hi"); + //~^ ERROR use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + //~| HELP add `::` + + let _ = Struct2<'a> { x: 1, _p: std::marker::PhantomData }.method(); + //~^ ERROR use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + //~| HELP add `::` + + let _ = Struct2<'a> { x: 1, _p: std::marker::PhantomData }.method()?; + //~^ ERROR use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + //~| HELP add `::` + + f<'_>(); + //~^ ERROR use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + //~| HELP add `::` + + // struct with two lifetimes + let _ = Struct3<'a, 'b> { a: "hi", b: "hi" }; + //~^ ERROR use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + //~| HELP add `::` + + // struct with lifetime and type + let _ = StructT<'a, &str> { a: "hi", b: "hi" }; + //~^ ERROR use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + //~| HELP add `::` + + Some(()) +} + fn main() {} diff --git a/tests/ui/parser/recover/missing-turbofish-lifetime.stderr b/tests/ui/parser/recover/missing-turbofish-lifetime.stderr index e14d9d6aa464c..faea5463bdfd4 100644 --- a/tests/ui/parser/recover/missing-turbofish-lifetime.stderr +++ b/tests/ui/parser/recover/missing-turbofish-lifetime.stderr @@ -1,57 +1,112 @@ -error: labels cannot use keyword names - --> $DIR/missing-turbofish-lifetime.rs:6:20 +error: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + --> $DIR/missing-turbofish-lifetime.rs:15:19 | LL | let _ = Struct<'_> { - | ^^ + | ^ + | +help: add `::` + | +LL | let _ = Struct::<'_> { + | ++ -error: expected `while`, `for`, `loop` or `{` after a label - --> $DIR/missing-turbofish-lifetime.rs:6:22 +error: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + --> $DIR/missing-turbofish-lifetime.rs:23:19 | -LL | let _ = Struct<'_> { - | ^ expected `while`, `for`, `loop` or `{` after a label +LL | let _ = Struct<'a> { + | ^ | -help: add `'` to close the char literal +help: add `::` | -LL | let _ = Struct<'_'> { - | + +LL | let _ = Struct::<'a> { + | ++ -error: comparison operators cannot be chained - --> $DIR/missing-turbofish-lifetime.rs:6:19 +error: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + --> $DIR/missing-turbofish-lifetime.rs:31:19 | -LL | let _ = Struct<'_> { - | ^ ^ +LL | let _ = Struct<'abc> { + | ^ + | +help: add `::` + | +LL | let _ = Struct::<'abc> { + | ++ -error: expected `while`, `for`, `loop` or `{` after a label - --> $DIR/missing-turbofish-lifetime.rs:15:22 +error: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + --> $DIR/missing-turbofish-lifetime.rs:41:24 | -LL | let _ = Struct<'a> { - | ^ expected `while`, `for`, `loop` or `{` after a label +LL | let _ = TupleStruct<'_>(""); + | ^ | -help: add `'` to close the char literal +help: add `::` | -LL | let _ = Struct<'a'> { - | + +LL | let _ = TupleStruct::<'_>(""); + | ++ -error: comparison operators cannot be chained - --> $DIR/missing-turbofish-lifetime.rs:15:19 +error: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + --> $DIR/missing-turbofish-lifetime.rs:58:20 | -LL | let _ = Struct<'a> { - | ^ ^ +LL | let _ = Wrapper<'a>::new("hi"); + | ^ + | +help: add `::` + | +LL | let _ = Wrapper::<'a>::new("hi"); + | ++ -error: expected `while`, `for`, `loop` or `{` after a label - --> $DIR/missing-turbofish-lifetime.rs:23:24 +error: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + --> $DIR/missing-turbofish-lifetime.rs:62:20 | -LL | let _ = Struct<'abc> { - | ^ expected `while`, `for`, `loop` or `{` after a label +LL | let _ = Struct2<'a> { x: 1, _p: std::marker::PhantomData }.method(); + | ^ + | +help: add `::` + | +LL | let _ = Struct2::<'a> { x: 1, _p: std::marker::PhantomData }.method(); + | ++ -error: comparison operators cannot be chained - --> $DIR/missing-turbofish-lifetime.rs:23:19 +error: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + --> $DIR/missing-turbofish-lifetime.rs:66:20 | -LL | let _ = Struct<'abc> { - | ^ ^ +LL | let _ = Struct2<'a> { x: 1, _p: std::marker::PhantomData }.method()?; + | ^ + | +help: add `::` + | +LL | let _ = Struct2::<'a> { x: 1, _p: std::marker::PhantomData }.method()?; + | ++ + +error: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + --> $DIR/missing-turbofish-lifetime.rs:70:6 + | +LL | f<'_>(); + | ^ + | +help: add `::` + | +LL | f::<'_>(); + | ++ + +error: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + --> $DIR/missing-turbofish-lifetime.rs:75:20 + | +LL | let _ = Struct3<'a, 'b> { a: "hi", b: "hi" }; + | ^ + | +help: add `::` + | +LL | let _ = Struct3::<'a, 'b> { a: "hi", b: "hi" }; + | ++ + +error: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + --> $DIR/missing-turbofish-lifetime.rs:80:20 + | +LL | let _ = StructT<'a, &str> { a: "hi", b: "hi" }; + | ^ + | +help: add `::` | - = help: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments - = help: or use `(...)` if you meant to specify fn arguments +LL | let _ = StructT::<'a, &str> { a: "hi", b: "hi" }; + | ++ -error: aborting due to 7 previous errors +error: aborting due to 10 previous errors diff --git a/tests/ui/parser/require-parens-for-chained-comparison.rs b/tests/ui/parser/require-parens-for-chained-comparison.rs index 6152fff6c0365..4162e9f27f36d 100644 --- a/tests/ui/parser/require-parens-for-chained-comparison.rs +++ b/tests/ui/parser/require-parens-for-chained-comparison.rs @@ -20,18 +20,14 @@ fn main() { //~| HELP use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments let _ = f<'_, i8>(); - //~^ ERROR expected one of - //~| HELP use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments - //~| ERROR expected - //~| HELP add `'` to close the char literal - //~| ERROR labels cannot use keyword names + //~^ ERROR cannot find function `f` in this scope + //~| ERROR use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + //~| HELP add `::` f<'_>(); - //~^ ERROR comparison operators cannot be chained - //~| HELP use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments - //~| ERROR expected - //~| HELP add `'` to close the char literal - //~| ERROR labels cannot use keyword names + //~^ ERROR cannot find function `f` in this scope + //~| ERROR use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + //~| HELP add `::` let _ = f; //~^ ERROR comparison operators cannot be chained diff --git a/tests/ui/parser/require-parens-for-chained-comparison.stderr b/tests/ui/parser/require-parens-for-chained-comparison.stderr index 9edfae3625027..50f66927db118 100644 --- a/tests/ui/parser/require-parens-for-chained-comparison.stderr +++ b/tests/ui/parser/require-parens-for-chained-comparison.stderr @@ -53,64 +53,30 @@ help: use `::<...>` instead of `<...>` to specify lifetime, type, or const argum LL | let _ = f::(); | ++ -error: labels cannot use keyword names - --> $DIR/require-parens-for-chained-comparison.rs:22:15 +error: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + --> $DIR/require-parens-for-chained-comparison.rs:22:14 | LL | let _ = f<'_, i8>(); - | ^^ - -error: expected `while`, `for`, `loop` or `{` after a label - --> $DIR/require-parens-for-chained-comparison.rs:22:17 - | -LL | let _ = f<'_, i8>(); - | ^ expected `while`, `for`, `loop` or `{` after a label - | -help: add `'` to close the char literal + | ^ | -LL | let _ = f<'_', i8>(); - | + - -error: expected one of `.`, `:`, `;`, `?`, `else`, `for`, `loop`, `while`, or an operator, found `,` - --> $DIR/require-parens-for-chained-comparison.rs:22:17 - | -LL | let _ = f<'_, i8>(); - | ^ expected one of 9 possible tokens - | -help: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments +help: add `::` | LL | let _ = f::<'_, i8>(); | ++ -error: labels cannot use keyword names - --> $DIR/require-parens-for-chained-comparison.rs:29:7 - | -LL | f<'_>(); - | ^^ - -error: expected `while`, `for`, `loop` or `{` after a label - --> $DIR/require-parens-for-chained-comparison.rs:29:9 +error: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + --> $DIR/require-parens-for-chained-comparison.rs:27:6 | LL | f<'_>(); - | ^ expected `while`, `for`, `loop` or `{` after a label + | ^ | -help: add `'` to close the char literal - | -LL | f<'_'>(); - | + - -error: comparison operators cannot be chained - --> $DIR/require-parens-for-chained-comparison.rs:29:6 - | -LL | f<'_>(); - | ^ ^ - | -help: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments +help: add `::` | LL | f::<'_>(); | ++ error: comparison operators cannot be chained - --> $DIR/require-parens-for-chained-comparison.rs:36:14 + --> $DIR/require-parens-for-chained-comparison.rs:32:14 | LL | let _ = f; | ^ ^ @@ -118,5 +84,18 @@ LL | let _ = f; = help: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments = help: or use `(...)` if you meant to specify fn arguments -error: aborting due to 12 previous errors +error[E0425]: cannot find function `f` in this scope + --> $DIR/require-parens-for-chained-comparison.rs:22:13 + | +LL | let _ = f<'_, i8>(); + | ^ not found in this scope + +error[E0425]: cannot find function `f` in this scope + --> $DIR/require-parens-for-chained-comparison.rs:27:5 + | +LL | f<'_>(); + | ^ not found in this scope + +error: aborting due to 10 previous errors +For more information about this error, try `rustc --explain E0425`. From eb85bf4a4e9e6fc4fb968ffb0f494b8ccec20328 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 13 Oct 2026 16:12:38 +0200 Subject: [PATCH 07/16] [rustdoc] Improve display of `rustc_must_implement_one_of` note and generate links to the mentioned methods --- src/librustdoc/html/render/print_item.rs | 14 ++++++++++---- tests/rustdoc-html/impl/must_implement_one_of.rs | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/librustdoc/html/render/print_item.rs b/src/librustdoc/html/render/print_item.rs index f6d5ada07f364..544d57d92613c 100644 --- a/src/librustdoc/html/render/print_item.rs +++ b/src/librustdoc/html/render/print_item.rs @@ -960,11 +960,17 @@ fn item_trait(cx: &Context<'_>, it: &clean::Item, t: &clean::Trait) -> impl fmt: )?; if let Some(list) = must_implement_one_of_functions.as_deref() { - write!( - w, - "
At least one of the `{}` methods is required.
", - fmt::from_fn(|f| list.iter().joined("`, `", f)), + w.write_str( + "\ +
At least one of the ", )?; + for (pos, method) in list.iter().enumerate() { + if pos > 0 { + w.write_str(", ")?; + } + write!(w, "{method}")?; + } + w.write_str(" methods is required.
")?; } for m in required_methods { diff --git a/tests/rustdoc-html/impl/must_implement_one_of.rs b/tests/rustdoc-html/impl/must_implement_one_of.rs index ab965bab35667..2d905df0a4566 100644 --- a/tests/rustdoc-html/impl/must_implement_one_of.rs +++ b/tests/rustdoc-html/impl/must_implement_one_of.rs @@ -3,7 +3,7 @@ #[rustc_must_implement_one_of(a, b)] //@ matches c/trait.Trait.html '//*[@class="stab must_implement"]' \ -// 'At least one of the `a`, `b` methods is required.$' +// 'At least one of the a, b methods is required.$' pub trait Trait { fn a() {} fn b() {} From fe974855d46941397c22c5901457bd8d7fff1296 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 13 Oct 2026 16:13:22 +0200 Subject: [PATCH 08/16] [rustdoc] Generate sidebar link for "required methods" even if there are none if the `rustc_must_implement_one_of` attribute is present --- src/librustdoc/html/render/sidebar.rs | 31 +++++++++++++++++---------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/librustdoc/html/render/sidebar.rs b/src/librustdoc/html/render/sidebar.rs index 3befbf2cff8ff..e0bd1fc9792e6 100644 --- a/src/librustdoc/html/render/sidebar.rs +++ b/src/librustdoc/html/render/sidebar.rs @@ -337,30 +337,39 @@ fn sidebar_trait<'a>( ("provided-associated-consts", "Provided Associated Constants", prov_assoc_const), ("required-associated-types", "Required Associated Types", req_assoc), ("provided-associated-types", "Provided Associated Types", prov_assoc), - ("required-methods", "Required Methods", req_method), - ("provided-methods", "Provided Methods", prov_method), - ("foreign-impls", "Implementations on Foreign Types", foreign_impls), ] .into_iter() .map(|(id, title, items)| LinkBlock::new(Link::new(id, title), "", items)), ); + + let tcx = cx.tcx(); + // Because of `rustc_must_implement_one_of`, we might have a trait with no "required methods" + // but still want the link in the sidebar, so we handle it on its own. + let mut req_method_link = + LinkBlock::new(Link::new("required-methods", "Required Methods"), "", req_method); + let rustc_middle::ty::TraitDef { must_implement_one_of, .. } = tcx.trait_def(t.def_id); + req_method_link.force_render = must_implement_one_of.is_some(); + blocks.push(req_method_link); + + blocks.push(LinkBlock::new(Link::new("provided-methods", "Provided Methods"), "", prov_method)); + sidebar_assoc_items(cx, it, blocks, deref_id_map); - // Move the foreign impls block after dyn compatibility note to match the order of the headings - // in the main content. - let foreign_impls_block = blocks.pop_if(|b| b.heading.href == "foreign-impls"); - if !t.is_dyn_compatible(cx.tcx()) { + if !t.is_dyn_compatible(tcx) { blocks.push(LinkBlock::forced( Link::new("dyn-compatibility", "Dyn Compatibility"), "dyn-compatibility-note", )); } - if let Some(foreign_impls_block) = foreign_impls_block { - blocks.push(foreign_impls_block); - } + + blocks.push(LinkBlock::new( + Link::new("foreign-impls", "Implementations on Foreign Types"), + "", + foreign_impls, + )); blocks.push(LinkBlock::forced(Link::new("implementors", "Implementors"), "impl")); - if t.is_auto(cx.tcx()) { + if t.is_auto(tcx) { blocks.push(LinkBlock::forced( Link::new("synthetic-implementors", "Auto Implementors"), "impl-auto", From 9b08cd0a18c01f87433654d6f0f49cb97fc63189 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 13 Oct 2026 09:21:07 -0600 Subject: [PATCH 09/16] Windows FileExt trait doesn't need to handle ErrorKind::Interrupted; test improvements for same --- library/std/src/fs/tests.rs | 250 +++++++++++++++++++++---------- library/std/src/os/windows/fs.rs | 9 +- 2 files changed, 174 insertions(+), 85 deletions(-) diff --git a/library/std/src/fs/tests.rs b/library/std/src/fs/tests.rs index 7fb976d5ef3da..605ad25270a63 100644 --- a/library/std/src/fs/tests.rs +++ b/library/std/src/fs/tests.rs @@ -852,102 +852,175 @@ fn file_test_windows_fileext_trait_case_1() { #[test] #[cfg(windows)] fn file_test_windows_fileext_trait_case_2() { + use crate::cell::RefCell; use crate::os::windows::fs::FileExt; // Test when seek_read(), seek_write() return Ok(0) struct MockFile { expected_offset: u64, + read_calls: RefCell, + write_calls: RefCell, + } + + impl MockFile { + fn new(expected_offset: u64) -> Self { + Self { expected_offset, read_calls: RefCell::new(0), write_calls: RefCell::new(0) } + } } impl FileExt for MockFile { fn seek_read(&self, _buf: &mut [u8], offset: u64) -> io::Result { + *self.read_calls.borrow_mut() += 1; assert_eq!(offset, self.expected_offset); Ok(0) } fn seek_write(&self, _buf: &[u8], offset: u64) -> io::Result { + *self.write_calls.borrow_mut() += 1; assert_eq!(offset, self.expected_offset); Ok(0) } } - { - let mock_file = MockFile { expected_offset: 0 }; - let mut buf = [0; 256]; - assert_eq!( - mock_file.seek_read_exact(&mut buf, 0).unwrap_err().kind(), - io::ErrorKind::UnexpectedEof - ); - assert_eq!(mock_file.seek_write_all(&buf, 0).unwrap_err().kind(), io::ErrorKind::WriteZero); - } - - { - let mock_file = MockFile { expected_offset: 420 }; - let mut buf = [0; 256]; - assert_eq!( - mock_file.seek_read_exact(&mut buf, 420).unwrap_err().kind(), - io::ErrorKind::UnexpectedEof - ); - assert_eq!( - mock_file.seek_write_all(&buf, 420).unwrap_err().kind(), - io::ErrorKind::WriteZero - ); + let mut buf = [0; 256]; + for offset in [0, 1, 420] { + { + let mock_file = MockFile::new(offset); + assert_eq!( + mock_file.seek_read_exact(&mut buf, offset).unwrap_err().kind(), + ErrorKind::UnexpectedEof + ); + assert_eq!(*mock_file.read_calls.borrow(), 1); + assert_eq!(*mock_file.write_calls.borrow(), 0); + } + { + let mock_file = MockFile::new(offset); + assert_eq!( + mock_file.seek_write_all(&buf, offset).unwrap_err().kind(), + ErrorKind::WriteZero + ); + assert_eq!(*mock_file.read_calls.borrow(), 0); + assert_eq!(*mock_file.write_calls.borrow(), 1); + } } } #[test] #[cfg(windows)] fn file_test_windows_fileext_trait_case_3() { + use crate::cell::RefCell; use crate::os::windows::fs::FileExt; - // Test that Err other than io::ErrorKind::Interrupted are propagated up. + // Test that Err are propagated up. struct MockFile { expected_offset: u64, + read_calls: RefCell, + write_calls: RefCell, + } + + impl MockFile { + fn new(expected_offset: u64) -> Self { + Self { expected_offset, read_calls: RefCell::new(0), write_calls: RefCell::new(0) } + } } impl FileExt for MockFile { fn seek_read(&self, _buf: &mut [u8], offset: u64) -> io::Result { + *self.read_calls.borrow_mut() += 1; assert_eq!(offset, self.expected_offset); - Err(io::Error::new(io::ErrorKind::PermissionDenied, "seek_read")) + Err(io::Error::new(ErrorKind::PermissionDenied, "seek_read")) } fn seek_write(&self, _buf: &[u8], offset: u64) -> io::Result { + *self.write_calls.borrow_mut() += 1; assert_eq!(offset, self.expected_offset); - Err(io::Error::new(io::ErrorKind::ConnectionRefused, "seek_write")) + Err(io::Error::new(ErrorKind::ConnectionRefused, "seek_write")) } } - { - let mock_file = MockFile { expected_offset: 0 }; - let mut buf = [0; 256]; - assert_eq!( - mock_file.seek_read_exact(&mut buf, 0).unwrap_err().kind(), - io::ErrorKind::PermissionDenied - ); - assert_eq!( - mock_file.seek_write_all(&buf, 0).unwrap_err().kind(), - io::ErrorKind::ConnectionRefused - ); + let mut buf = [0; 256]; + for offset in [0, 1, 420] { + { + let mock_file = MockFile::new(offset); + assert_eq!( + mock_file.seek_read_exact(&mut buf, offset).unwrap_err().kind(), + ErrorKind::PermissionDenied + ); + assert_eq!(*mock_file.read_calls.borrow(), 1); + assert_eq!(*mock_file.write_calls.borrow(), 0); + } + { + let mock_file = MockFile::new(offset); + assert_eq!( + mock_file.seek_write_all(&buf, offset).unwrap_err().kind(), + ErrorKind::ConnectionRefused + ); + assert_eq!(*mock_file.read_calls.borrow(), 0); + assert_eq!(*mock_file.write_calls.borrow(), 1); + } } +} - { - let mock_file = MockFile { expected_offset: 420 }; - let mut buf = [0; 256]; - assert_eq!( - mock_file.seek_read_exact(&mut buf, 420).unwrap_err().kind(), - io::ErrorKind::PermissionDenied - ); - assert_eq!( - mock_file.seek_write_all(&buf, 420).unwrap_err().kind(), - io::ErrorKind::ConnectionRefused - ); +#[test] +#[cfg(windows)] +fn file_test_windows_fileext_trait_case_4() { + use crate::cell::RefCell; + use crate::os::windows::fs::FileExt; + + // Test that Err ErrorKind::Interrupted are propagated up (unlike Unix versions) + struct MockFile { + expected_offset: u64, + read_calls: RefCell, + write_calls: RefCell, + } + + impl MockFile { + fn new(expected_offset: u64) -> Self { + Self { expected_offset, read_calls: RefCell::new(0), write_calls: RefCell::new(0) } + } + } + + impl FileExt for MockFile { + fn seek_read(&self, _buf: &mut [u8], offset: u64) -> io::Result { + *self.read_calls.borrow_mut() += 1; + assert_eq!(offset, self.expected_offset); + Err(io::Error::new(ErrorKind::Interrupted, "seek_read")) + } + + fn seek_write(&self, _buf: &[u8], offset: u64) -> io::Result { + *self.write_calls.borrow_mut() += 1; + assert_eq!(offset, self.expected_offset); + Err(io::Error::new(ErrorKind::Interrupted, "seek_write")) + } + } + + let mut buf = [0; 256]; + for offset in [0, 1, 420] { + { + let mock_file = MockFile::new(offset); + assert_eq!( + mock_file.seek_read_exact(&mut buf, offset).unwrap_err().kind(), + ErrorKind::Interrupted + ); + assert_eq!(*mock_file.read_calls.borrow(), 1); + assert_eq!(*mock_file.write_calls.borrow(), 0); + } + { + let mock_file = MockFile::new(offset); + assert_eq!( + mock_file.seek_write_all(&buf, offset).unwrap_err().kind(), + ErrorKind::Interrupted + ); + assert_eq!(*mock_file.read_calls.borrow(), 0); + assert_eq!(*mock_file.write_calls.borrow(), 1); + } } - // FIXME: Cover io::ErrorKind::Interrupted, but don't infinite loop ;) } #[test] #[cfg(windows)] -fn file_test_windows_fileext_trait_case_4() { +fn file_test_windows_fileext_trait_case_5() { + use crate::cell::RefCell; use crate::os::windows::fs::FileExt; const MSG: &[u8] = @@ -957,10 +1030,19 @@ fn file_test_windows_fileext_trait_case_4() { // seek_write(), respectively. struct MockFile { expected_offset: u64, + read_calls: RefCell, + write_calls: RefCell, + } + + impl MockFile { + fn new(expected_offset: u64) -> Self { + Self { expected_offset, read_calls: RefCell::new(0), write_calls: RefCell::new(0) } + } } impl FileExt for MockFile { fn seek_read(&self, buf: &mut [u8], offset: u64) -> io::Result { + *self.read_calls.borrow_mut() += 1; assert_eq!(offset, self.expected_offset); assert_eq!(buf.len(), MSG.len()); assert_eq!(buf, &[0; MSG.len()]); @@ -969,6 +1051,7 @@ fn file_test_windows_fileext_trait_case_4() { } fn seek_write(&self, buf: &[u8], offset: u64) -> io::Result { + *self.write_calls.borrow_mut() += 1; assert_eq!(offset, self.expected_offset); assert_eq!(buf.len(), MSG.len()); assert_eq!(buf, MSG); @@ -976,26 +1059,28 @@ fn file_test_windows_fileext_trait_case_4() { } } - { - let mock_file = MockFile { expected_offset: 0 }; - let mut buf = [0; MSG.len()]; - check!(mock_file.seek_read_exact(&mut buf, 0)); - assert_eq!(&buf, MSG); - check!(mock_file.seek_write_all(&buf, 0)); - } - - { - let mock_file = MockFile { expected_offset: 420 }; - let mut buf = [0; MSG.len()]; - check!(mock_file.seek_read_exact(&mut buf, 420)); - assert_eq!(&buf, MSG); - check!(mock_file.seek_write_all(&buf, 420)); + for offset in [0, 1, 420] { + { + let mock_file = MockFile::new(offset); + let mut buf = [0; MSG.len()]; + check!(mock_file.seek_read_exact(&mut buf, offset)); + assert_eq!(&buf, MSG); + assert_eq!(*mock_file.read_calls.borrow(), 1); + assert_eq!(*mock_file.write_calls.borrow(), 0); + } + { + let mock_file = MockFile::new(offset); + check!(mock_file.seek_write_all(MSG, offset)); + assert_eq!(*mock_file.read_calls.borrow(), 0); + assert_eq!(*mock_file.write_calls.borrow(), 1); + } } } #[test] #[cfg(windows)] -fn file_test_windows_fileext_trait_case_5() { +fn file_test_windows_fileext_trait_case_6() { + use crate::cell::RefCell; use crate::os::windows::fs::FileExt; const MSG: &[u8] = @@ -1004,36 +1089,47 @@ fn file_test_windows_fileext_trait_case_5() { // Test pathological case where seek_read(), seek_write() only do 1 byte per call, return Ok(1) struct MockFile { base_offset: u64, + read_calls: RefCell, + write_calls: RefCell, + } + + impl MockFile { + fn new(base_offset: u64) -> Self { + Self { base_offset, read_calls: RefCell::new(0), write_calls: RefCell::new(0) } + } } impl FileExt for MockFile { fn seek_read(&self, buf: &mut [u8], offset: u64) -> io::Result { + *self.read_calls.borrow_mut() += 1; let offset = (offset - self.base_offset) as usize; buf[0..1].copy_from_slice(&MSG[offset..offset + 1]); Ok(1) } fn seek_write(&self, buf: &[u8], offset: u64) -> io::Result { + *self.write_calls.borrow_mut() += 1; let offset = (offset - self.base_offset) as usize; assert_eq!(buf[0..1], MSG[offset..offset + 1]); Ok(1) } } - { - let mock_file = MockFile { base_offset: 0 }; - let mut buf = [0; MSG.len()]; - check!(mock_file.seek_read_exact(&mut buf, 0)); - assert_eq!(&buf, MSG); - check!(mock_file.seek_write_all(&buf, 0)); - } - - { - let mock_file = MockFile { base_offset: 420 }; - let mut buf = [0; MSG.len()]; - check!(mock_file.seek_read_exact(&mut buf, 420)); - assert_eq!(&buf, MSG); - check!(mock_file.seek_write_all(&buf, 420)); + for offset in [0, 1, 420] { + { + let mock_file = MockFile::new(offset); + let mut buf = [0; MSG.len()]; + check!(mock_file.seek_read_exact(&mut buf, offset)); + assert_eq!(&buf, MSG); + assert_eq!(*mock_file.read_calls.borrow(), MSG.len()); + assert_eq!(*mock_file.write_calls.borrow(), 0); + } + { + let mock_file = MockFile::new(offset); + check!(mock_file.seek_write_all(MSG, offset)); + assert_eq!(*mock_file.read_calls.borrow(), 0); + assert_eq!(*mock_file.write_calls.borrow(), MSG.len()); + } } } diff --git a/library/std/src/os/windows/fs.rs b/library/std/src/os/windows/fs.rs index c10766470e873..4c2506ffae66c 100644 --- a/library/std/src/os/windows/fs.rs +++ b/library/std/src/os/windows/fs.rs @@ -69,9 +69,6 @@ pub trait FileExt { /// /// # Errors /// - /// If this function encounters an error of the kind - /// [`io::ErrorKind::Interrupted`] then the error is ignored and the operation - /// will continue. /// /// If this function encounters an "end of file" before completely filling /// the buffer, it returns an error of the kind [`io::ErrorKind::UnexpectedEof`]. @@ -113,7 +110,6 @@ pub trait FileExt { buf = &mut buf[n..]; offset += n as u64; } - Err(ref e) if e.is_interrupted() => {} Err(e) => return Err(e), } } @@ -203,7 +199,6 @@ pub trait FileExt { let prev_written = buf.written(); match self.seek_read_buf(buf.reborrow(), offset) { Ok(()) => {} - Err(e) if e.is_interrupted() => {} Err(e) => return Err(e), } let n = buf.written() - prev_written; @@ -264,8 +259,7 @@ pub trait FileExt { /// /// # Errors /// - /// This function will return the first error of - /// non-[`io::ErrorKind::Interrupted`] kind that [`seek_write`] returns. + /// This function will return the first error that [`seek_write`] returns. /// /// [`seek_write`]: FileExt::seek_write /// @@ -298,7 +292,6 @@ pub trait FileExt { buf = &buf[n..]; offset += n as u64 } - Err(ref e) if e.is_interrupted() => {} Err(e) => return Err(e), } } From fbf53be6d470bbf291d3e1b757c40e88469a1dba Mon Sep 17 00:00:00 2001 From: Urgau Date: Mon, 12 Oct 2026 19:15:26 +0200 Subject: [PATCH 10/16] Test the interaction between `--remap-path-prefix` and doctests --- .../remap-path-prefix-doctest-panic/rmake.rs | 52 +++++++++++++++++++ .../src/doctest.rs | 4 ++ 2 files changed, 56 insertions(+) create mode 100644 tests/run-make/remap-path-prefix-doctest-panic/rmake.rs create mode 100644 tests/run-make/remap-path-prefix-doctest-panic/src/doctest.rs diff --git a/tests/run-make/remap-path-prefix-doctest-panic/rmake.rs b/tests/run-make/remap-path-prefix-doctest-panic/rmake.rs new file mode 100644 index 0000000000000..b184eda0b37bb --- /dev/null +++ b/tests/run-make/remap-path-prefix-doctest-panic/rmake.rs @@ -0,0 +1,52 @@ +// Checks that the location printed by a panicking doctest is remapped +// according to `--remap-path-scope`. +// +// Only the lines between the start of the stack backtrace and the end are +// are inspected, so timings, temp dirs and backtraces can't leak into the check. + +use run_make_support::assertion_helpers::{assert_contains, assert_not_contains}; +use run_make_support::rustdoc; + +/// Runs the failing doctest and returns stdout. +fn panic_file(scope: Option<&str>) -> String { + let mut cmd = rustdoc(); + cmd.input("src/doctest.rs") + .arg("--test") + .arg("--remap-path-prefix=src=/REMAPPED") + .arg("-Cdebuginfo=1") + .arg("--edition=2015") + .env("RUST_BACKTRACE", "1"); + + if let Some(scope) = scope { + cmd.arg(format!("--remap-path-scope={scope}")).arg("-Zunstable-options"); + } + + let out = cmd.run_fail(); + out.assert_stdout_contains("explicit panic"); + + let stdout = out.stdout_utf8(); + let section: Vec<&str> = stdout + .lines() + .skip_while(|l| l.trim() != "stack backtrace:") + .skip(1) + .take_while(|l| l.trim() != "failures:") + .collect(); + + assert!(!section.is_empty(), "no backtrace found in output:\n{stdout}"); + section.join("\n") +} + +fn main() { + // Check that with debuginfo (and related) we get the remapped path + for scope in [Some("debuginfo"), Some("object"), Some("all"), None] { + let stdout = panic_file(scope); + assert_contains(&stdout, "/REMAPPED/doctest.rs"); + assert_not_contains(&stdout, "src/doctest.rs"); + } + + for scope in ["diagnostics", "documentation", "macro"] { + let stdout = panic_file(Some(scope)); + assert_contains(&stdout, "src/doctest.rs"); + assert_not_contains(&stdout, "/REMAPPED/doctest.rs"); + } +} diff --git a/tests/run-make/remap-path-prefix-doctest-panic/src/doctest.rs b/tests/run-make/remap-path-prefix-doctest-panic/src/doctest.rs new file mode 100644 index 0000000000000..2160cc21f6b42 --- /dev/null +++ b/tests/run-make/remap-path-prefix-doctest-panic/src/doctest.rs @@ -0,0 +1,4 @@ +/// ``` +/// panic!() +/// ``` +pub fn f() {} From cad2ab9ffe4878410152581c90db2b42b4a1339c Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 13 Oct 2026 16:26:46 +0200 Subject: [PATCH 11/16] Add regression test for `rustc_must_implement_one_of` changes --- .../trait-rustc_must_implement_one_of.rs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/rustdoc-html/trait-rustc_must_implement_one_of.rs diff --git a/tests/rustdoc-html/trait-rustc_must_implement_one_of.rs b/tests/rustdoc-html/trait-rustc_must_implement_one_of.rs new file mode 100644 index 0000000000000..57accd9fa0791 --- /dev/null +++ b/tests/rustdoc-html/trait-rustc_must_implement_one_of.rs @@ -0,0 +1,50 @@ +// This test ensures a few things around the `rustc_must_implement_one_of` attribute: +// 1. The listed methods are links +// 2. It gets a heading in the sidebar even if there is no "required methods" +// Regression test for . + +#![allow(internal_features)] +#![feature(rustc_attrs)] + +#![crate_name = "foo"] + +//@ has 'foo/trait.Read.html' +//@ has - '//*[@class="stab must_implement"]/a[@href="#method.read"]' 'read' +//@ has - '//*[@class="stab must_implement"]/a[@href="#method.read_buf"]' 'read_buf' + +// There should be 3 headings in the sidebar: Required/Provided methods and "Implementors". +//@ count - '//*[@id="rustdoc-toc"]/h3' 3 +//@ has - '//*[@id="rustdoc-toc"]/h3/a[@href="#required-methods"]' 'Required Methods' +//@ has - '//*[@id="rustdoc-toc"]/h3/a[@href="#provided-methods"]' 'Provided Methods' +//@ has - '//*[@id="rustdoc-toc"]/h3/a[@href="#implementors"]' 'Implementors' + +#[rustc_must_implement_one_of(read_buf, read)] +pub trait Read { + fn read_buf(&self) {} + fn read(&self) {} +} + +//@ has 'foo/trait.Tadam.html' +// There should still be 3 headings in the sidebar: Required/Provided methods and "Implementors". +//@ count - '//*[@id="rustdoc-toc"]/h3' 3 +//@ has - '//*[@id="rustdoc-toc"]/h3/a[@href="#required-methods"]' 'Required Methods' +//@ has - '//*[@id="rustdoc-toc"]/h3/a[@href="#provided-methods"]' 'Provided Methods' +//@ has - '//*[@id="rustdoc-toc"]/h3/a[@href="#implementors"]' 'Implementors' + +#[rustc_must_implement_one_of(read_buf, read)] +pub trait Tadam { + fn read_buf(&self) {} + fn read(&self) {} + fn foo(&self) {} +} + +//@ has 'foo/trait.Other.html' +// There should be 2 headings in the sidebar: Provided methods and "Implementors". +//@ count - '//*[@id="rustdoc-toc"]/h3' 2 +//@ has - '//*[@id="rustdoc-toc"]/h3/a[@href="#provided-methods"]' 'Provided Methods' +//@ has - '//*[@id="rustdoc-toc"]/h3/a[@href="#implementors"]' 'Implementors' + +pub trait Other { + fn read_buf(&self) {} + fn read(&self) {} +} From f8d4856c08ecbdc05e7766b12e809a90ee7ede22 Mon Sep 17 00:00:00 2001 From: Urgau Date: Tue, 13 Oct 2026 20:47:57 +0200 Subject: [PATCH 12/16] Output remapped paths in rustdoc JSON with the `DOCUMENTATION` scope --- src/librustdoc/json/conversions.rs | 21 ++++++++----------- .../remap-path-prefix/debuginfo.rs | 5 +++++ tests/rustdoc-json/remap-path-prefix/diag.rs | 5 +++++ tests/rustdoc-json/remap-path-prefix/doc.rs | 5 +++++ .../rustdoc-json/remap-path-prefix/object.rs | 5 +++++ .../remap-path-prefix/remap-path-prefix.rs | 4 ++++ 6 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 tests/rustdoc-json/remap-path-prefix/debuginfo.rs create mode 100644 tests/rustdoc-json/remap-path-prefix/diag.rs create mode 100644 tests/rustdoc-json/remap-path-prefix/doc.rs create mode 100644 tests/rustdoc-json/remap-path-prefix/object.rs create mode 100644 tests/rustdoc-json/remap-path-prefix/remap-path-prefix.rs diff --git a/src/librustdoc/json/conversions.rs b/src/librustdoc/json/conversions.rs index 556a922f7044c..d23e9a8ca456a 100644 --- a/src/librustdoc/json/conversions.rs +++ b/src/librustdoc/json/conversions.rs @@ -17,7 +17,7 @@ use rustc_hir_pretty::PpAnn; use rustc_metadata::rendered_const; use rustc_middle::ty::{self, TyCtxt}; use rustc_span::def_id::ModId; -use rustc_span::{Pos, Symbol, bug, kw, sym}; +use rustc_span::{Pos, RemapPathScopeComponents, Symbol, bug, kw, sym}; use rustdoc_json_types::*; use crate::clean::{self, ItemId}; @@ -191,17 +191,14 @@ impl FromClean for Option { fn from_clean(span: &clean::Span, renderer: &JsonRenderer<'_>) -> Self { match span.filename(renderer.sess()) { rustc_span::FileName::Real(name) => { - if let Some(local_path) = name.into_local_path() { - let hi = span.hi(renderer.sess()); - let lo = span.lo(renderer.sess()); - Some(Span { - filename: local_path, - begin: (lo.line, lo.col.to_usize() + 1), - end: (hi.line, hi.col.to_usize() + 1), - }) - } else { - None - } + let filename = name.path(RemapPathScopeComponents::DOCUMENTATION); + let hi = span.hi(renderer.sess()); + let lo = span.lo(renderer.sess()); + Some(Span { + filename: filename.to_path_buf(), + begin: (lo.line, lo.col.to_usize() + 1), + end: (hi.line, hi.col.to_usize() + 1), + }) } _ => None, } diff --git a/tests/rustdoc-json/remap-path-prefix/debuginfo.rs b/tests/rustdoc-json/remap-path-prefix/debuginfo.rs new file mode 100644 index 0000000000000..d378dca22be58 --- /dev/null +++ b/tests/rustdoc-json/remap-path-prefix/debuginfo.rs @@ -0,0 +1,5 @@ +//@ compile-flags: --remap-path-prefix={{src-base}}=/remapped +//@ compile-flags: --remap-path-scope=debuginfo -Zunstable-options + +//@ is "$.index[?(@.name=='StrLike')].span.filename" $FILE +pub trait StrLike {} diff --git a/tests/rustdoc-json/remap-path-prefix/diag.rs b/tests/rustdoc-json/remap-path-prefix/diag.rs new file mode 100644 index 0000000000000..79b469c40084e --- /dev/null +++ b/tests/rustdoc-json/remap-path-prefix/diag.rs @@ -0,0 +1,5 @@ +//@ compile-flags: --remap-path-prefix={{src-base}}=/remapped +//@ compile-flags: --remap-path-scope=diagnostics -Zunstable-options + +//@ is "$.index[?(@.name=='StrLike')].span.filename" $FILE +pub trait StrLike {} diff --git a/tests/rustdoc-json/remap-path-prefix/doc.rs b/tests/rustdoc-json/remap-path-prefix/doc.rs new file mode 100644 index 0000000000000..11fa449983754 --- /dev/null +++ b/tests/rustdoc-json/remap-path-prefix/doc.rs @@ -0,0 +1,5 @@ +//@ compile-flags: --remap-path-prefix={{src-base}}=/remapped +//@ compile-flags: --remap-path-scope=documentation -Zunstable-options + +//@ is "$.index[?(@.name=='StrLike')].span.filename" \"/remapped/remap-path-prefix/doc.rs\" +pub trait StrLike {} diff --git a/tests/rustdoc-json/remap-path-prefix/object.rs b/tests/rustdoc-json/remap-path-prefix/object.rs new file mode 100644 index 0000000000000..052ad7651eeab --- /dev/null +++ b/tests/rustdoc-json/remap-path-prefix/object.rs @@ -0,0 +1,5 @@ +//@ compile-flags: --remap-path-prefix={{src-base}}=/remapped +//@ compile-flags: --remap-path-scope=object -Zunstable-options + +//@ is "$.index[?(@.name=='StrLike')].span.filename" $FILE +pub trait StrLike {} diff --git a/tests/rustdoc-json/remap-path-prefix/remap-path-prefix.rs b/tests/rustdoc-json/remap-path-prefix/remap-path-prefix.rs new file mode 100644 index 0000000000000..1d0a1fa65fad0 --- /dev/null +++ b/tests/rustdoc-json/remap-path-prefix/remap-path-prefix.rs @@ -0,0 +1,4 @@ +//@ compile-flags: --remap-path-prefix={{src-base}}=/remapped + +//@ is "$.index[?(@.name=='StrLike')].span.filename" \"/remapped/remap-path-prefix/remap-path-prefix.rs\" +pub trait StrLike {} From c2a249de8feb848954f2e99d28d78139672b0265 Mon Sep 17 00:00:00 2001 From: Urgau Date: Tue, 13 Oct 2026 21:57:52 +0200 Subject: [PATCH 13/16] Ignore cross-compile as doctests don't always work when cross-compiled --- tests/run-make/remap-path-prefix-doctest-panic/rmake.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/run-make/remap-path-prefix-doctest-panic/rmake.rs b/tests/run-make/remap-path-prefix-doctest-panic/rmake.rs index b184eda0b37bb..383955a270ab3 100644 --- a/tests/run-make/remap-path-prefix-doctest-panic/rmake.rs +++ b/tests/run-make/remap-path-prefix-doctest-panic/rmake.rs @@ -4,6 +4,8 @@ // Only the lines between the start of the stack backtrace and the end are // are inspected, so timings, temp dirs and backtraces can't leak into the check. +//@ ignore-cross-compile + use run_make_support::assertion_helpers::{assert_contains, assert_not_contains}; use run_make_support::rustdoc; From 0d0f40400243a926ab26953a5b089252c06035fc Mon Sep 17 00:00:00 2001 From: Urgau Date: Tue, 13 Oct 2026 21:54:04 +0200 Subject: [PATCH 14/16] Add README.md and descriptions for the remap-path-prefix JSON tests --- tests/rustdoc-json/README.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 tests/rustdoc-json/README.md diff --git a/tests/rustdoc-json/README.md b/tests/rustdoc-json/README.md new file mode 100644 index 0000000000000..875a149585010 --- /dev/null +++ b/tests/rustdoc-json/README.md @@ -0,0 +1,7 @@ +## `tests/rustdoc-json/remap-path-prefix` + +Tests for `--remap-path-prefix` and `--remap-path-scope` with rustdoc JSON: + - `remap-path-prefix`: only uses bare `--remap-path-prefix`, expects remapped paths + - `doc`: pass `--remap-path-prefix` and the `documentation` scope, expects remapped paths + - `object`: pass `--remap-path-prefix` and the `object` scope, expects un-remapped paths + - `debuginfo`: pass `--remap-path-prefix` and the `debuginfo` scope, expects un-remapped paths From 9c1eb804990d9cbfe3d763a275958aab1bb0dd0f Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 14 Oct 2026 02:30:42 +0200 Subject: [PATCH 15/16] Prefer WriteConsoleW for stdout on Windows This drops the `GetConsoleOutputCP` call, which would otherwise do an RPC to the console server (= slow). This leaves us with `is_console` which was using another RPC via `GetConsoleMode` (= doubly slow). On Windows 8+ we can just use `NtQueryVolumeInformationFile()` to test for `FILE_DEVICE_CONSOLE`, resulting in a ~2x console stdout speedup. --- library/std/src/sys/io/is_terminal/windows.rs | 48 +++++++++++++---- library/std/src/sys/pal/windows/c.rs | 7 +-- .../std/src/sys/pal/windows/c/bindings.txt | 4 +- .../std/src/sys/pal/windows/c/windows_sys.rs | 10 +++- library/std/src/sys/stdio/windows.rs | 53 ++++++++++++------- .../miri/src/shims/windows/foreign_items.rs | 28 ++++------ 6 files changed, 98 insertions(+), 52 deletions(-) diff --git a/library/std/src/sys/io/is_terminal/windows.rs b/library/std/src/sys/io/is_terminal/windows.rs index b0c718d71f9f3..6d66e93087911 100644 --- a/library/std/src/sys/io/is_terminal/windows.rs +++ b/library/std/src/sys/io/is_terminal/windows.rs @@ -1,4 +1,5 @@ use crate::ffi::c_void; +use crate::mem::MaybeUninit; use crate::os::windows::io::{AsHandle, AsRawHandle, BorrowedHandle}; use crate::sys::c; @@ -12,10 +13,44 @@ fn handle_is_console(handle: BorrowedHandle<'_>) -> bool { return false; } - let mut out = 0; - if unsafe { c::GetConsoleMode(handle.as_raw_handle(), &mut out) != 0 } { - // False positives aren't possible. If we got a console then we definitely have a console. - return true; + cfg_select! { + target_vendor = "win7" => { + let mut out = 0; + if unsafe { c::GetConsoleMode(handle.as_raw_handle(), &mut out) != 0 } { + // False positives aren't possible. If we got a console then we definitely have a console. + return true; + } + + // Early return if the handle is not FILE_TYPE_PIPE, implying it cannot be an msys PTY. + if unsafe { c::GetFileType(handle.as_raw_handle()) != 0x0003 } { + return false; + } + } + _ => unsafe { + // `GetConsoleMode` incurs a remote RPC round-trip (= bad), so it's best avoided. + // `NtQueryVolumeInformationFile` lets us test the handle directly. + // The required `FILE_DEVICE_CONSOLE` type was introduced in Windows 8. + // Fun fact: `GetFileType` uses `NtQueryVolumeInformationFile` internally. + let mut io_status = MaybeUninit::::uninit(); + let mut device_info = MaybeUninit::::uninit(); + let status = c::NtQueryVolumeInformationFile( + handle.as_raw_handle(), + io_status.as_mut_ptr(), + device_info.as_mut_ptr().cast(), + size_of_val(&device_info) as u32, + c::FileFsDeviceInformation, + ); + if status < 0 { + return false; + } + + match device_info.assume_init_ref().DeviceType { + c::FILE_DEVICE_CONSOLE => return true, + // If it's a named pipe, it may be an msys PTY. + c::FILE_DEVICE_NAMED_PIPE => {} // aka FILE_TYPE_PIPE + _ => return false, + } + }, } // Otherwise, we fall back to an msys hack to see if we can detect the presence of a pty. @@ -23,11 +58,6 @@ fn handle_is_console(handle: BorrowedHandle<'_>) -> bool { } fn msys_tty_on(handle: BorrowedHandle<'_>) -> bool { - // Early return if the handle is not a pipe. - if unsafe { c::GetFileType(handle.as_raw_handle()) != c::FILE_TYPE_PIPE } { - return false; - } - /// Mirrors [`FILE_NAME_INFO`], giving it a fixed length that we can stack /// allocate /// diff --git a/library/std/src/sys/pal/windows/c.rs b/library/std/src/sys/pal/windows/c.rs index 25031968a7e02..67345d183a2d2 100644 --- a/library/std/src/sys/pal/windows/c.rs +++ b/library/std/src/sys/pal/windows/c.rs @@ -20,6 +20,9 @@ pub type WCHAR = u16; pub const INVALID_HANDLE_VALUE: HANDLE = ::core::ptr::without_provenance_mut(-1i32 as _); +pub const FILE_DEVICE_NAMED_PIPE: u32 = 0x00000011; +pub const FILE_DEVICE_CONSOLE: u32 = 0x00000050; + // https://learn.microsoft.com/en-us/cpp/c-runtime-library/exit-success-exit-failure?view=msvc-170 pub const EXIT_SUCCESS: u32 = 0; pub const EXIT_FAILURE: u32 = 1; @@ -188,9 +191,6 @@ pub const FILE_CURRENT: u32 = cu(windows_sys::FILE_CURRENT); pub const FSCTL_GET_REPARSE_POINT: u32 = cu(windows_sys::FSCTL_GET_REPARSE_POINT); pub const FSCTL_SET_REPARSE_POINT: u32 = cu(windows_sys::FSCTL_SET_REPARSE_POINT); -// GetFileType return value (DWORD) -pub const FILE_TYPE_PIPE: u32 = cu(windows_sys::FILE_TYPE_PIPE); - // GetACP return value (UINT) pub const CP_UTF8: u32 = windows_sys::CP_UTF8.cast_unsigned(); @@ -473,6 +473,7 @@ cfg_select! { target_vendor = "uwp" => { windows_link::link_raw_dylib!("ntdll.dll" "system" fn NtCreateFile(filehandle : *mut HANDLE, desiredaccess : ACCESS_MASK, objectattributes : *const OBJECT_ATTRIBUTES, iostatusblock : *mut IO_STATUS_BLOCK, allocationsize : *const i64, fileattributes : u32, shareaccess : u32, createdisposition : u32, createoptions : u32, eabuffer : *const core::ffi::c_void, ealength : u32) -> NTSTATUS); windows_link::link_raw_dylib!("ntdll.dll" "system" fn NtOpenFile(filehandle : *mut HANDLE, desiredaccess : ACCESS_MASK, objectattributes : *const OBJECT_ATTRIBUTES, iostatusblock : *mut IO_STATUS_BLOCK, shareaccess : u32, openoptions : u32) -> NTSTATUS); + windows_link::link_raw_dylib!("ntdll.dll" "system" fn NtQueryVolumeInformationFile(filehandle : HANDLE, iostatusblock : *mut IO_STATUS_BLOCK, fsinformation : *mut core::ffi::c_void, length : u32, fsinformationclass : FS_INFORMATION_CLASS) -> NTSTATUS); windows_link::link_raw_dylib!("ntdll.dll" "system" fn NtReadFile(filehandle : HANDLE, event : HANDLE, apcroutine : PIO_APC_ROUTINE, apccontext : *const core::ffi::c_void, iostatusblock : *mut IO_STATUS_BLOCK, buffer : *mut core::ffi::c_void, length : u32, byteoffset : *const i64, key : *const u32) -> NTSTATUS); windows_link::link_raw_dylib!("ntdll.dll" "system" fn NtWriteFile(filehandle : HANDLE, event : HANDLE, apcroutine : PIO_APC_ROUTINE, apccontext : *const core::ffi::c_void, iostatusblock : *mut IO_STATUS_BLOCK, buffer : *const core::ffi::c_void, length : u32, byteoffset : *const i64, key : *const u32) -> NTSTATUS); windows_link::link_raw_dylib!("ntdll.dll" "system" fn RtlNtStatusToDosError(status : NTSTATUS) -> u32); diff --git a/library/std/src/sys/pal/windows/c/bindings.txt b/library/std/src/sys/pal/windows/c/bindings.txt index 70af36f27907e..71f94954b1356 100644 --- a/library/std/src/sys/pal/windows/c/bindings.txt +++ b/library/std/src/sys/pal/windows/c/bindings.txt @@ -2032,6 +2032,7 @@ FILE_FLAG_RANDOM_ACCESS FILE_FLAG_SEQUENTIAL_SCAN FILE_FLAG_SESSION_AWARE FILE_FLAG_WRITE_THROUGH +FILE_FS_DEVICE_INFORMATION FILE_GENERIC_EXECUTE FILE_GENERIC_READ FILE_GENERIC_WRITE @@ -2084,7 +2085,6 @@ FILE_SYNCHRONOUS_IO_NONALERT FILE_TRAVERSE FILE_TYPE_CHAR FILE_TYPE_DISK -FILE_TYPE_PIPE FILE_TYPE_REMOTE FILE_TYPE_UNKNOWN FILE_WRITE_ATTRIBUTES @@ -2100,6 +2100,7 @@ FileCompressionInfo FileDispositionInfo FileDispositionInfoEx FileEndOfFileInfo +FileFsDeviceInformation FileFullDirectoryInfo FileFullDirectoryRestartInfo FileIdBothDirectoryInfo @@ -2303,6 +2304,7 @@ NO_ERROR NORMAL_PRIORITY_CLASS NtCreateFile NtOpenFile +NtQueryVolumeInformationFile NtReadFile NtSetInformationFile NtWriteFile diff --git a/library/std/src/sys/pal/windows/c/windows_sys.rs b/library/std/src/sys/pal/windows/c/windows_sys.rs index 0c865494a6e7c..ae08f790bb40a 100644 --- a/library/std/src/sys/pal/windows/c/windows_sys.rs +++ b/library/std/src/sys/pal/windows/c/windows_sys.rs @@ -78,6 +78,7 @@ windows_link::link!("kernel32.dll" "system" fn MoveFileExW(lpexistingfilename : windows_link::link!("kernel32.dll" "system" fn MultiByteToWideChar(codepage : u32, dwflags : u32, lpmultibytestr : *const i8, cbmultibyte : i32, lpwidecharstr : PWSTR, cchwidechar : i32) -> i32); windows_link::link!("ntdll.dll" "system" fn NtCreateFile(filehandle : *mut HANDLE, desiredaccess : ACCESS_MASK, objectattributes : *const OBJECT_ATTRIBUTES, iostatusblock : *mut IO_STATUS_BLOCK, allocationsize : *const i64, fileattributes : u32, shareaccess : u32, createdisposition : u32, createoptions : u32, eabuffer : *const core::ffi::c_void, ealength : u32) -> NTSTATUS); windows_link::link!("ntdll.dll" "system" fn NtOpenFile(filehandle : *mut HANDLE, desiredaccess : ACCESS_MASK, objectattributes : *const OBJECT_ATTRIBUTES, iostatusblock : *mut IO_STATUS_BLOCK, shareaccess : u32, openoptions : u32) -> NTSTATUS); +windows_link::link!("ntdll.dll" "system" fn NtQueryVolumeInformationFile(filehandle : HANDLE, iostatusblock : *mut IO_STATUS_BLOCK, fsinformation : *mut core::ffi::c_void, length : u32, fsinformationclass : FS_INFORMATION_CLASS) -> NTSTATUS); windows_link::link!("ntdll.dll" "system" fn NtReadFile(filehandle : HANDLE, event : HANDLE, apcroutine : PIO_APC_ROUTINE, apccontext : *const core::ffi::c_void, iostatusblock : *mut IO_STATUS_BLOCK, buffer : *mut core::ffi::c_void, length : u32, byteoffset : *const i64, key : *const u32) -> NTSTATUS); windows_link::link!("ntdll.dll" "system" fn NtSetInformationFile(filehandle : HANDLE, iostatusblock : *mut IO_STATUS_BLOCK, fileinformation : *const core::ffi::c_void, length : u32, fileinformationclass : FILE_INFORMATION_CLASS) -> NTSTATUS); windows_link::link!("ntdll.dll" "system" fn NtWriteFile(filehandle : HANDLE, event : HANDLE, apcroutine : PIO_APC_ROUTINE, apccontext : *const core::ffi::c_void, iostatusblock : *mut IO_STATUS_BLOCK, buffer : *const core::ffi::c_void, length : u32, byteoffset : *const i64, key : *const u32) -> NTSTATUS); @@ -2544,6 +2545,12 @@ pub const FILE_FLAG_RANDOM_ACCESS: i32 = 268435456; pub const FILE_FLAG_SEQUENTIAL_SCAN: i32 = 134217728; pub const FILE_FLAG_SESSION_AWARE: i32 = 8388608; pub const FILE_FLAG_WRITE_THROUGH: u32 = 2147483648; +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct FILE_FS_DEVICE_INFORMATION { + pub DeviceType: u32, + pub Characteristics: u32, +} pub const FILE_GENERIC_EXECUTE: i32 = 1179808; pub const FILE_GENERIC_READ: i32 = 1179785; pub const FILE_GENERIC_WRITE: i32 = 1179926; @@ -2678,7 +2685,6 @@ pub const FILE_SYNCHRONOUS_IO_NONALERT: i32 = 32; pub const FILE_TRAVERSE: i32 = 32; pub const FILE_TYPE_CHAR: i32 = 2; pub const FILE_TYPE_DISK: i32 = 1; -pub const FILE_TYPE_PIPE: i32 = 3; pub const FILE_TYPE_REMOTE: i32 = 32768; pub const FILE_TYPE_UNKNOWN: i32 = 0; pub const FILE_WRITE_ATTRIBUTES: i32 = 256; @@ -2718,6 +2724,7 @@ pub const FORMAT_MESSAGE_IGNORE_INSERTS: i32 = 512; pub const FRS_ERR_SYSVOL_POPULATE_TIMEOUT: i32 = 8014; pub const FSCTL_GET_REPARSE_POINT: i32 = 589992; pub const FSCTL_SET_REPARSE_POINT: i32 = 589988; +pub type FS_INFORMATION_CLASS = i32; pub const FileAlignmentInfo: FILE_INFO_BY_HANDLE_CLASS = 17; pub const FileAllocationInfo: FILE_INFO_BY_HANDLE_CLASS = 5; pub const FileAttributeTagInfo: FILE_INFO_BY_HANDLE_CLASS = 9; @@ -2727,6 +2734,7 @@ pub const FileCompressionInfo: FILE_INFO_BY_HANDLE_CLASS = 8; pub const FileDispositionInfo: FILE_INFO_BY_HANDLE_CLASS = 4; pub const FileDispositionInfoEx: FILE_INFO_BY_HANDLE_CLASS = 21; pub const FileEndOfFileInfo: FILE_INFO_BY_HANDLE_CLASS = 6; +pub const FileFsDeviceInformation: FS_INFORMATION_CLASS = 4; pub const FileFullDirectoryInfo: FILE_INFO_BY_HANDLE_CLASS = 14; pub const FileFullDirectoryRestartInfo: FILE_INFO_BY_HANDLE_CLASS = 15; pub const FileIdBothDirectoryInfo: FILE_INFO_BY_HANDLE_CLASS = 10; diff --git a/library/std/src/sys/stdio/windows.rs b/library/std/src/sys/stdio/windows.rs index 1339048a9c039..82a4424d46989 100644 --- a/library/std/src/sys/stdio/windows.rs +++ b/library/std/src/sys/stdio/windows.rs @@ -77,26 +77,34 @@ pub fn get_handle(handle_id: u32) -> io::Result { } } -fn is_console(handle: c::HANDLE) -> bool { - // `GetConsoleMode` will return false (0) if this is a pipe (we don't care about the reported - // mode). This will only detect Windows Console, not other terminals connected to a pipe like - // MSYS. Which is exactly what we need, as only Windows Console needs a conversion to UTF-16. - let mut mode = 0; - unsafe { c::GetConsoleMode(handle, &mut mode) != 0 } -} - -/// Returns true if the attached console's code page is currently UTF-8. +/// Returns true if `handle` refers to a proper console (aka conhost). #[cfg(not(target_vendor = "win7"))] -fn is_utf8_console() -> bool { - unsafe { c::GetConsoleOutputCP() == c::CP_UTF8 } -} - -#[cfg(target_vendor = "win7")] -fn is_utf8_console() -> bool { - // Windows 7 has a fun "feature" where WriteFile on a console handle will return - // the number of UTF-16 code units written and not the number of bytes from the input string. - // So we always claim the console isn't UTF-8 to trigger the WriteConsole fallback code. - false +fn is_console(handle: c::HANDLE) -> bool { + cfg_select! { + target_vendor = "win7" => { + // `GetConsoleMode` will return false (0) if this is a pipe (we don't care about the reported + // mode). This will only detect Windows Console, not other terminals connected to a pipe like + // MSYS. Which is exactly what we need, as only Windows Console needs a conversion to UTF-16. + let mut mode = 0; + unsafe { c::GetConsoleMode(handle, &mut mode) != 0 } + } + _ => unsafe { + // `GetConsoleMode` incurs a remote RPC round-trip (= bad), so it's best avoided. + // `NtQueryVolumeInformationFile` lets us test the handle directly. + // The required `FILE_DEVICE_CONSOLE` type was introduced in Windows 8. + // Fun fact: `GetFileType` uses `NtQueryVolumeInformationFile` internally. + let mut io_status = MaybeUninit::::uninit(); + let mut device_info = MaybeUninit::::uninit(); + let status = c::NtQueryVolumeInformationFile( + handle, + io_status.as_mut_ptr(), + device_info.as_mut_ptr().cast(), + size_of_val(&device_info) as u32, + c::FileFsDeviceInformation, + ); + status >= 0 && device_info.assume_init_ref().DeviceType == c::FILE_DEVICE_CONSOLE + }, + } } fn write(handle_id: u32, data: &[u8], incomplete_utf8: &mut IncompleteUtf8) -> io::Result { @@ -105,7 +113,12 @@ fn write(handle_id: u32, data: &[u8], incomplete_utf8: &mut IncompleteUtf8) -> i } let handle = get_handle(handle_id)?; - if !is_console(handle) || is_utf8_console() { + + // At the time of writing, console IO on Windows is high latency: + // Windows has no TTY buffers, making every call a fully synchronous RPC. + // So, while always deferring to `write_console_utf16` wastes valuable heap for + // our UTF16 conversion, it handily beats doing 2 (or more) costly RPC round-trips. + if !is_console(handle) { unsafe { let handle = Handle::from_raw_handle(handle); let ret = handle.write(data); diff --git a/src/tools/miri/src/shims/windows/foreign_items.rs b/src/tools/miri/src/shims/windows/foreign_items.rs index 28fe4e86bb7b5..2d31c6809da87 100644 --- a/src/tools/miri/src/shims/windows/foreign_items.rs +++ b/src/tools/miri/src/shims/windows/foreign_items.rs @@ -1190,6 +1190,16 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { // Incomplete shims that we "stub out" just to get pre-main initialization code to work. // These shims are enabled only when the caller is in the standard library. + "NtQueryVolumeInformationFile" if this.frame_in_std() => { + let [_handle, _iosb, _info, _length, _class] = this.check_shim_sig( + shim_sig!(extern "system" fn(winapi::HANDLE, *_, *_, u32, i32) -> i32), + (link_name, abi, args), + )?; + // Return an error to make std think that nothing is a console. + // This means the normal NtWriteFile is used for stdout/stderr, greatly simplifying our lives. + let not_implemented = this.eval_windows("c", "STATUS_NOT_IMPLEMENTED"); + this.write_scalar(not_implemented, dest)?; + } "GetProcessHeap" if this.frame_in_std() => { let [] = this.check_shim_sig( shim_sig!(extern "system" fn() -> winapi::HANDLE), @@ -1215,24 +1225,6 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { // Pretend these does not exist / nothing happened, by returning zero. this.write_null(dest)?; } - "GetConsoleMode" if this.frame_in_std() => { - let [console, mode] = this.check_shim_sig( - shim_sig!(extern "system" fn(winapi::HANDLE, *_) -> winapi::BOOL), - (link_name, abi, args), - )?; - this.read_target_isize(console)?; - this.deref_pointer_as(mode, this.machine.layouts.u32)?; - // Indicate an error. - this.write_null(dest)?; - } - "GetFileType" if this.frame_in_std() => { - let [_file] = this.check_shim_sig( - shim_sig!(extern "system" fn(winapi::HANDLE) -> u32), - (link_name, abi, args), - )?; - // Return unknown file type. - this.write_null(dest)?; - } "AddVectoredExceptionHandler" if this.frame_in_std() => { let [_first, _handler] = this.check_shim_sig( shim_sig!(extern "system" fn(u32, *_) -> *_), From 6c150feb0fded2947740033883c718fe644749b6 Mon Sep 17 00:00:00 2001 From: Robin <1187338299@qq.com> Date: Wed, 14 Oct 2026 09:56:48 +0800 Subject: [PATCH 16/16] fix some nits --- compiler/rustc_hir_analysis/src/check/wfcheck.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/compiler/rustc_hir_analysis/src/check/wfcheck.rs b/compiler/rustc_hir_analysis/src/check/wfcheck.rs index 1aa194d7e0013..6401854f5dcfd 100644 --- a/compiler/rustc_hir_analysis/src/check/wfcheck.rs +++ b/compiler/rustc_hir_analysis/src/check/wfcheck.rs @@ -2400,10 +2400,7 @@ impl<'tcx> WfCheckingCtxt<'_, 'tcx> { Err(_guar) => ty::region_constraint::RegionConstraint::new_true(), }; if constraints.is_false() { - self.tcx() - .dcx() - .struct_span_err(span, "top level false constraints are not supported") - .emit(); + self.tcx().dcx().span_err(span, "top level false constraints are not supported"); return; } @@ -2486,7 +2483,7 @@ impl<'tcx> WfCheckingCtxt<'_, 'tcx> { ) .with_spans(forall.span); if constraint.is_false() { - self.tcx().dcx().struct_span_err(forall.span, "forall evaluated to false").emit(); + self.tcx().dcx().span_err(forall.span, "forall evaluated to false"); self.infcx.overwrite_solver_region_constraint( ty::region_constraint::RegionConstraint::new_true(), );