From 4706577c46b6c664d6522f335a38b48fe970486b Mon Sep 17 00:00:00 2001 From: Waffle Lapkin Date: Fri, 24 Jul 2026 14:52:29 +0200 Subject: [PATCH 1/2] Make `LazyCell` covariant in both `T` and `F` --- .../core/src/cell/covariant_unsafe_cell.rs | 1 + library/core/src/cell/lazy.rs | 59 +++++++++++++++---- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/library/core/src/cell/covariant_unsafe_cell.rs b/library/core/src/cell/covariant_unsafe_cell.rs index 3876cc404cd2e..a9a5889f17832 100644 --- a/library/core/src/cell/covariant_unsafe_cell.rs +++ b/library/core/src/cell/covariant_unsafe_cell.rs @@ -37,6 +37,7 @@ impl CovariantUnsafeCell { /// ``` #[unstable(feature = "covariant_unsafe_cell", issue = "159735")] #[rustc_const_unstable(feature = "covariant_unsafe_cell", issue = "159735")] + #[rustc_const_stable_indirect] #[inline(always)] pub const fn new(value: T) -> CovariantUnsafeCell { CovariantUnsafeCell(UnsafeCell::new(value)) diff --git a/library/core/src/cell/lazy.rs b/library/core/src/cell/lazy.rs index ba32fff2284fd..3ba976dcefbae 100644 --- a/library/core/src/cell/lazy.rs +++ b/library/core/src/cell/lazy.rs @@ -1,4 +1,4 @@ -use super::UnsafeCell; +use crate::cell::CovariantUnsafeCell; use crate::hint::unreachable_unchecked; use crate::ops::{Deref, DerefMut}; use crate::{fmt, mem}; @@ -52,7 +52,44 @@ enum State { /// ``` #[stable(feature = "lazy_cell", since = "1.80.0")] pub struct LazyCell T> { - state: UnsafeCell>, + // It is non-obvious why `LazyCell` can be covariant in both `T` and `F` + // (and thus use `CovariantUnsafeCell`)... + // + // # `F` + // + // `F` is the easier to explain one; The state only ever transitions *out* of `Uninit(F)` + // (to either `Poisoned` or `Init(T)`), and never into it. In other words `F` is only ever + // read, not written. + // + // One could imagine `LazyCell` implemented as + // ``` + // struct { + // state: UnsafeCell, + // f: ManuallyDrop, + // } + // ``` + // which would make it "obviously" covariant in `F`. + // + // **NOTE**: the important invariant here is that we never allow writing `F` through + // `&LazyCell`. + // + // # `T` + // + // Why `LazyCell` can be covariant in `T` is even more subtle. We do allow writing `T` through + // a `&LazyCell`, which would normally force us to make `LazyCell` invariant in `T`. However, + // the only value that we allow writing is one returned by `F`... and we don't allow changing + // `F`. So even if a user gets a `&LazyCell`, they cannot write the + // `LessrestrictedVersionOfT` through it. + // + // **NOTE**: the important invariants here are that + // 1. `T` can only be written through `&LazyCell` by being returned from `F` + // 2. `F` cannot be overwritten after `LazyCell` creation + // + // # Conclusion + // + // `LazyCell` can be covariant in both `T` and `F`... provided the non-local invariants which + // are listed above. + state: CovariantUnsafeCell>, } impl T> LazyCell { @@ -73,7 +110,7 @@ impl T> LazyCell { #[stable(feature = "lazy_cell", since = "1.80.0")] #[rustc_const_stable(feature = "lazy_cell", since = "1.80.0")] pub const fn new(f: F) -> LazyCell { - LazyCell { state: UnsafeCell::new(State::Uninit(f)) } + LazyCell { state: CovariantUnsafeCell::new(State::Uninit(f)) } } /// Consumes this `LazyCell` returning the stored value. @@ -141,7 +178,7 @@ impl T> LazyCell { // reference lives either until the end of the borrow of `this` (in the // initialized case) or is invalidated in `really_init` (in the // uninitialized case; `really_init` will create and return a fresh reference). - let state = unsafe { &*this.state.get() }; + let state = unsafe { this.state.get().as_ref() }; match state { State::Init(data) => data, // SAFETY: The state is uninitialized. @@ -231,7 +268,7 @@ impl T> LazyCell { // This function is only called when the state is uninitialized, // so no references to `state` can exist except for the reference // in `force`, which is invalidated here and not accessed again. - let state = unsafe { &mut *this.state.get() }; + let state = unsafe { this.state.get().as_mut() }; // Temporarily mark the state as poisoned. This prevents reentrant // accesses and correctly poisons the cell if the closure panicked. let State::Uninit(f) = mem::replace(state, State::Poisoned) else { unreachable!() }; @@ -242,15 +279,15 @@ impl T> LazyCell { // If the closure accessed the cell through something like a reentrant // mutex, but caught the panic resulting from the state being poisoned, // the mutable borrow for `state` will be invalidated, so we need to - // go through the `UnsafeCell` pointer here. The state can only be - // poisoned at this point, so using `write` to skip the destructor - // of `State` should help the optimizer. + // go through the `CovariantUnsafeCell` pointer here. The state can + // only be poisoned at this point, so using `write` to skip the + // destructor of `State` should help the optimizer. unsafe { this.state.get().write(State::Init(data)) }; // SAFETY: // The previous references were invalidated by the `write` call above, // so do a new shared borrow of the state instead. - let state = unsafe { &*this.state.get() }; + let state = unsafe { this.state.get().as_ref() }; let State::Init(data) = state else { unreachable!() }; data } @@ -303,7 +340,7 @@ impl LazyCell { // This is sound for the same reason as in `force`: once the state is // initialized, it will not be mutably accessed again, so this reference // will stay valid for the duration of the borrow to `self`. - let state = unsafe { &*this.state.get() }; + let state = unsafe { this.state.get().as_ref() }; match state { State::Init(data) => Some(data), _ => None, @@ -373,7 +410,7 @@ impl From for LazyCell { /// with the provided value. #[inline] fn from(value: T) -> Self { - Self { state: UnsafeCell::new(State::Init(value)) } + Self { state: CovariantUnsafeCell::new(State::Init(value)) } } } From 01615ea32897b53268726629a5308e6af681c9d0 Mon Sep 17 00:00:00 2001 From: Waffle Lapkin Date: Fri, 24 Jul 2026 15:04:24 +0200 Subject: [PATCH 2/2] make `LazyLock` covariant in both `T` and `F` --- library/std/src/lib.rs | 1 + library/std/src/sync/lazy_lock.rs | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/library/std/src/lib.rs b/library/std/src/lib.rs index e1061af1e7d6d..8ed3d7eac720f 100644 --- a/library/std/src/lib.rs +++ b/library/std/src/lib.rs @@ -280,6 +280,7 @@ #![feature(cfg_target_thread_local)] #![feature(cfi_encoding)] #![feature(const_trait_impl)] +#![feature(covariant_unsafe_cell)] #![feature(decl_macro)] #![feature(deprecated_suggestion)] #![feature(diagnostic_on_move)] diff --git a/library/std/src/sync/lazy_lock.rs b/library/std/src/sync/lazy_lock.rs index 9bb25287275b2..25af3d0e25cd8 100644 --- a/library/std/src/sync/lazy_lock.rs +++ b/library/std/src/sync/lazy_lock.rs @@ -1,5 +1,5 @@ use super::once::OnceExclusiveState; -use crate::cell::UnsafeCell; +use crate::cell::CovariantUnsafeCell; use crate::mem::ManuallyDrop; use crate::ops::{Deref, DerefMut}; use crate::panic::{RefUnwindSafe, UnwindSafe}; @@ -81,7 +81,9 @@ union Data { pub struct LazyLock T> { // FIXME(nonpoison_once): if possible, switch to nonpoison version once it is available once: Once, - data: UnsafeCell>, + // See justification on `core::cell::LazyCell::state` for why this can be covariant in both + // `T` and `F`. + data: CovariantUnsafeCell>, } impl T> LazyLock { @@ -102,7 +104,10 @@ impl T> LazyLock { #[stable(feature = "lazy_cell", since = "1.80.0")] #[rustc_const_stable(feature = "lazy_cell", since = "1.80.0")] pub const fn new(f: F) -> LazyLock { - LazyLock { once: Once::new(), data: UnsafeCell::new(Data { f: ManuallyDrop::new(f) }) } + LazyLock { + once: Once::new(), + data: CovariantUnsafeCell::new(Data { f: ManuallyDrop::new(f) }), + } } /// Consumes this `LazyLock` returning the stored value. @@ -245,7 +250,7 @@ impl T> LazyLock { } // SAFETY: `call_once` only runs this closure once, ever. - let data = unsafe { &mut *this.data.get() }; + let data = unsafe { this.data.get().as_mut() }; let f = unsafe { ManuallyDrop::take(&mut data.f) }; let value = f(); data.value = ManuallyDrop::new(value); @@ -258,7 +263,7 @@ impl T> LazyLock { // * the closure was not called, but a previous call initialized `value`. // * the closure was not called because the Once is poisoned, which we handled above. // So `value` has definitely been initialized and will not be modified again. - unsafe { &*(*this.data.get()).value } + unsafe { &this.data.get().as_ref().value } } } @@ -313,7 +318,7 @@ impl LazyLock { // SAFETY: // The closure has been run successfully, so `value` has been initialized // and will not be modified again. - Some(unsafe { &(*this.data.get()).value }) + Some(unsafe { &this.data.get().as_ref().value }) } else { None } @@ -404,7 +409,7 @@ impl From for LazyLock { fn from(value: T) -> Self { LazyLock { once: Once::new_complete(), - data: UnsafeCell::new(Data { value: ManuallyDrop::new(value) }), + data: CovariantUnsafeCell::new(Data { value: ManuallyDrop::new(value) }), } } }