From 26d7a3a41b2bccdb5324737bca74385cd4859a84 Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Wed, 22 Oct 2025 21:12:16 +0000 Subject: [PATCH 1/3] Add caveats about mutable references in consts As it turns out, the story that "the final value of a constant cannot contain any mutable references" has some nuance to it. Let's describe these caveats. Thanks to theemathas for noticing these and to RalfJ for filling in context. --- src/items/constant-items.md | 89 ++++++++++++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 6 deletions(-) diff --git a/src/items/constant-items.md b/src/items/constant-items.md index 219d4e6ad7..83c18d7a39 100644 --- a/src/items/constant-items.md +++ b/src/items/constant-items.md @@ -49,32 +49,104 @@ const BITS_N_STRINGS: BitsNStrings<'static> = BitsNStrings { ``` r[items.const.no-mut-refs] -The final value of a `const` item cannot contain any mutable references. +The final value of a `const` item, after the initializer is evaluated to a value that has the declared type of the constant, cannot contain any mutable references except as described below. ```rust # #![allow(static_mut_refs)] static mut S: u8 = 0; -const C: &u8 = unsafe { &mut S }; // OK +const _: &u8 = unsafe { &mut S }; // OK. +// ^^^^^^ +// Allowed since this is coerced to `&S`. ``` ```rust # use core::sync::atomic::AtomicU8; static S: AtomicU8 = AtomicU8::new(0); -const C: &AtomicU8 = &S; // OK +const _: &AtomicU8 = &S; // OK. +// ^^ +// Allowed even though the shared reference is to an interior +// mutable value. ``` ```rust,compile_fail,E0080 # #![allow(static_mut_refs)] static mut S: u8 = 0; -const C: &mut u8 = unsafe { &mut S }; // ERROR not allowed +const _: &mut u8 = unsafe { &mut S }; // ERROR. +// ^^^^^^ +// Not allowed as the mutable reference appears in the final value. ``` +```rust,compile_fail,E0080 +# #![allow(static_mut_refs)] +static mut S: u8 = 0; +const _: &dyn Send = &unsafe { &mut S }; // ERROR. +// ^^^^^^ +// Not allowed as the mutable reference appears in the final value, +// even though type erasure occurs. +``` + +Mutable references where the referent is a value of a [zero-sized type] are allowed. + +```rust +# #![allow(static_mut_refs)] +static mut S: () = (); +const _: &mut () = unsafe { &mut S }; // OK. +// ^^ This is a zero-sized type. +``` + +```rust +# #![allow(static_mut_refs)] +static mut S: [u8; 0] = [0; 0]; +const _: &mut [u8; 0] = unsafe { &mut S }; // OK. +// ^^^^^^^ This is a zero-sized type. +``` + +> [!NOTE] +> This is allowed as, for a value of a zero-sized type, no bytes can actually be mutated. We must accept this as `&mut []` is [promoted]. + +Values of [union type] are not considered to contain any references; for this purpose, a value of union type is treated as a sequence of untyped bytes. + +```rust +# #![allow(static_mut_refs)] +union U { f: &'static mut u8 } +static mut S: u8 = 0; +const _: U = unsafe { U { f: &mut S }}; // OK. +// ^^^^^^^^^^^^^^^ +// This is treated as a sequence of untyped bytes. +``` + +Mutable references contained within a [mutable static] may be referenced in the final value of a constant. + +```rust +# #![allow(static_mut_refs)] +static mut S: &mut u8 = unsafe { static mut I: u8 = 0; &mut I }; +const _: &&mut u8 = unsafe { &S }; // OK. +// ^^^^^^^ +// This mutable reference comes from a `static mut`. +``` + +> [!NOTE] +> This is allowed as it's separately not allowed to read from a mutable static during constant evaluation. See [const-eval.const-expr.path-static]. + +Mutable references contained within an [external static] may be referenced in the final value of a constant. + +```rust +# #![allow(static_mut_refs)] +unsafe extern "C" { static S: &'static mut u8; } +const _: &&mut u8 = unsafe { &S }; // OK. +// ^^^^^^^ +// This mutable references comes from an extern static. +``` + +> [!NOTE] +> This is allowed as it's separately not allowed to read from an external static during constant evaluation. See [const-eval.const-expr.path-static]. + > [!NOTE] > We also disallow, in the final value, shared references to mutable statics created in the initializer for a separate reason. Consider: > > ```rust,compile_fail,E0492 > # use core::sync::atomic::AtomicU8; -> const C: &AtomicU8 = &AtomicU8::new(0); // ERROR +> const _: &AtomicU8 = &AtomicU8::new(0); // ERROR. > ``` > > Here, the `AtomicU8` is a temporary that is lifetime extended to `'static` (see [destructors.scope.lifetime-extension.static]), and references to lifetime-extended temporaries with interior mutability are not allowed in the final value of a constant expression (see [const-eval.const-expr.borrows]). @@ -154,10 +226,15 @@ fn unused_generic_function() { [const_eval]: ../const_eval.md [associated constant]: ../items/associated-items.md#associated-constants [constant value]: ../const_eval.md#constant-expressions +[external static]: items.extern.static [free]: ../glossary.md#free-item [static lifetime elision]: ../lifetime-elision.md#const-and-static-elision [trait definition]: traits.md [underscore imports]: use-declarations.md#underscore-imports [`Copy`]: ../special-types-and-traits.md#copy [value namespace]: ../names/namespaces.md -[promotion]: ../destructors.md#constant-promotion +[mutable static]: items.static.mut +[promoted]: destructors.scope.const-promotion +[promotion]: destructors.scope.const-promotion +[union type]: type.union +[zero-sized type]: layout.properties.size From e775322e33190ef1cfe47d8dc4cd4e7c8857d05a Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Thu, 23 Oct 2025 06:11:59 +0000 Subject: [PATCH 2/3] Clarify note about extended temps in initializers We have a note that describes why, perhaps surprisingly, it's OK to have a reference to a static with an interior mutable value in the final value of a constant item but it's not OK if the interior mutable value is a temporary from the initializer. There was room to improve how this was worded, so let's take it. --- src/items/constant-items.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/items/constant-items.md b/src/items/constant-items.md index 83c18d7a39..a1ee37bd95 100644 --- a/src/items/constant-items.md +++ b/src/items/constant-items.md @@ -142,14 +142,22 @@ const _: &&mut u8 = unsafe { &S }; // OK. > This is allowed as it's separately not allowed to read from an external static during constant evaluation. See [const-eval.const-expr.path-static]. > [!NOTE] -> We also disallow, in the final value, shared references to mutable statics created in the initializer for a separate reason. Consider: +> As described above, we accept, in the final value of constant items, shared references to static items whose values have interior mutability. +> +> ```rust +> # use core::sync::atomic::AtomicU8; +> static S: AtomicU8 = AtomicU8::new(0); +> const _: &AtomicU8 = &S; // OK. +> ``` +> +> However, we disallow similar code when the interior mutable value is created in the initializer. > > ```rust,compile_fail,E0492 > # use core::sync::atomic::AtomicU8; > const _: &AtomicU8 = &AtomicU8::new(0); // ERROR. > ``` > -> Here, the `AtomicU8` is a temporary that is lifetime extended to `'static` (see [destructors.scope.lifetime-extension.static]), and references to lifetime-extended temporaries with interior mutability are not allowed in the final value of a constant expression (see [const-eval.const-expr.borrows]). +> Here, the `AtomicU8` is a temporary whose scope is extended to the end of the program (see [destructors.scope.lifetime-extension.static]). Such temporaries with interior mutability cannot be borrowed in constant expressions (see [const-eval.const-expr.borrows]). r[items.const.expr-omission] The constant expression may only be omitted in a [trait definition]. From 53f4392b0287ea2348f8f48b57ea2a8adf44f7eb Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Fri, 24 Oct 2025 21:03:45 +0000 Subject: [PATCH 3/3] Describe why certain constants aren't accepted We had described the design rationale for why certain values of constant items were accepted, but for the values that are not accepted, while we had described mechanically the reasons for this, we had not described the rationale for it. This can be a bit tricky, because often the real rationale is that "we're being conservative and only allowing the cases where we have a good reason to allow them and where we can prove that allowing them is OK". So it is easier to describe why we allow something than why we don't. But still, let's try to describe some reasons why we don't allow some things yet. --- src/items/constant-items.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/items/constant-items.md b/src/items/constant-items.md index a1ee37bd95..321894a944 100644 --- a/src/items/constant-items.md +++ b/src/items/constant-items.md @@ -76,6 +76,24 @@ const _: &mut u8 = unsafe { &mut S }; // ERROR. // Not allowed as the mutable reference appears in the final value. ``` +> [!NOTE] +> Constant initializers can be thought of, in most cases, as being inlined wherever the constant appears. If a constant whose value contains a mutable reference to a mutable static were to appear twice, and this were to be allowed, that would create two mutable references, each having `'static` lifetime, to the same place. This could produce undefined behavior. +> +> Constants that contain mutable references to temporaries whose scopes have been extended to the end of the program have that same problem and an additional one. +> +> ```rust,compile_fail,E0764 +> const _: &mut u8 = &mut 0; // ERROR. +> // ^^^^^^ +> // Not allowed as the mutable reference appears in the final value and +> // because the constant expression contains a mutable borrow of an +> // expression whose temporary scope would be extended to the end of +> // the program. +> ``` +> +> Here, the value `0` is a temporary whose scope is extended to the end of the program (see [destructors.scope.lifetime-extension.static]). Such temporaries cannot be mutably borrowed in constant expressions (see [const-eval.const-expr.borrows]). +> +> To allow this, we'd have to decide whether each use of the constant creates a new `u8` value or whether each use shares the same lifetime-extended temporary. The latter choice, though closer to how `rustc` thinks about this today, would break the conceptual model that, in most cases, the constant initializer can be thought of as being inlined wherever the constant is used. Since we haven't decided, and due to the other problem mentioned, this is not allowed. + ```rust,compile_fail,E0080 # #![allow(static_mut_refs)] static mut S: u8 = 0; @@ -158,6 +176,8 @@ const _: &&mut u8 = unsafe { &S }; // OK. > ``` > > Here, the `AtomicU8` is a temporary whose scope is extended to the end of the program (see [destructors.scope.lifetime-extension.static]). Such temporaries with interior mutability cannot be borrowed in constant expressions (see [const-eval.const-expr.borrows]). +> +> To allow this, we'd have to decide whether each use of the constant creates a new `AtomicU8` or whether each use shares the same lifetime-extended temporary. The latter choice, though closer to how `rustc` thinks about this today, would break the conceptual model that, in most cases, the constant initializer can be thought of as being inlined wherever the constant is used. Since we haven't decided, this is not allowed. r[items.const.expr-omission] The constant expression may only be omitted in a [trait definition].