Idea: Shared Mutable References (`&shr mut T`)

Motivation

&T and &mut T literally mean immutable references and mutable references, but more exactly, shared references and exclusive references. &mut T is mutable, also exclusive (assuming that we are not talking about UnsafePinned). That's not bad. However, &T is more complicated because of interior mutability. &T is immutable only when T: Freeze. For T: !Freeze, &T is not only mutable but also shared. What's more, there are no immutable references for T: !Freeze except wrapping T into a new type.

Definition

In this doc, the shared reference &T in current Rust is split up to 3 kinds of references:

  • the immutable reference &const T where T: Freeze, i.e. the referee of an &'a const T can never be mutated during the lifetime 'a.
  • the shared mutable reference &shr mut T, which is (semantically) really similar to &UnsafeCell<T> in current Rust. The referee of an &'a shr mut T can be mutated but cannot be moved or dropped during the lifetime 'a. Besides that, &shr mut T can also be freely copied[1].
  • the shared immutable reference &shr T[2], the immutable counterpart of &shr mut T. The difference between &const T and &shr T is: the referee of &const T is guaranteed to be immutable during its lifetime, but that of &shr T is not. The only guarantee of &shr T is that the referee cannot be mutated by this reference itself, but there may be &shr mut T referring to the same value, and their lifetimes can overlap.

The (exclusively) mutable reference &mut T remains unchanged in this doc.

Here is a table of the total 4 kinds of references. The main idea is: &shr T and &shr mut T cannot coexist with &const T or &mut T. In LLVM's terms, &const T and &mut T are noalias, but &shr T and &shr mut T are not.

Reference Kind Current Rust (Approximate) This Doc
immutable, no alias &T, except T contains UnsafeCell &const T
immutable, may alias &T, and T contains UnsafeCell &shr T
mutable, no alias &mut T, except T is UnsafePinned &mut T
mutable, may alias &mut UnsafePinned<T> &shr mut T

As a result, UnsafeCell and UnsafePinned is no longer needed. UnsafeCell::get and UnsafePinned::get can be replaced by raw (re)borrows of &shr T and &shr mut T.

Here is the coercion/reborrow rules among the 4 kinds of references.

From \ To &const T &shr T &mut T &shr mut T
&const T × ×
&shr T × × ×
&mut T √ (Reborrow) √ (Reborrow) √ (Reborrow) √ (Reborrow)
&shr mut T × ×

The thread safety rules has be slightly changed:

  • Send: the same as current Rust.
  • SyncV2: indicate the &shr T and &shr mut T can be sent across threads.

Note that SyncV2 is kind of different with Sync in current Rust. Many primitive types (integers, bool, str, etc.) become !SyncV2 because &shr mut T (quite similar to &Cell<T> in current Rust) can not be shared across threads.

Type Send where SyncV2 where
&const T T: Send T: Send
&shr T T: SyncV2 T: SyncV2
&mut T T: Send T: Send
&shr mut T T: SyncV2[3] T: SyncV2
Rc<T> never never
Arc<T> T: Send + SyncV2 T: SyncV2
Mutex<T> T: Send always
RwLock<T> T: Send T: SyncV2
ReentrantLock<T> T: Send T: SyncV2
Exclusive<T>[4] T: Send always

Libaray APIs

DerefConst, DerefMut, DerefShr, and DerefShrMut

pub trait DerefTarget {
    type Target;
}

pub trait DerefConst: DerefTarget {
    fn deref_const(&self) -> &const Self::Target;
}

pub trait DerefMutV2: DerefTarget {
    fn deref_mut_v2(&mut self) -> &mut Self::Target;
}

pub trait DerefShr : DerefTarget {
    fn deref_shr(&shr self) -> &shr Self::Target;
}

pub trait DerefShrMut : DerefTarget {
    fn deref_shr_mut(&shr mut self) -> &shr mut Self::Target;
}

Rc<T>, Arc<T>, CloneV2 and CopyMut

Deep copy and shallow copy of Rc<T> and Arc<T> become completely different, as deep copy needs an immutable reference, but shallow copy needs a shared mutable reference.

pub trait Copy {}

pub trait CloneV2 {
    // Deep copy always.
    fn clone_v2(&const self) -> Self;
}
impl<T: Copy> CloneV2 for T {}

/// # Safety
/// The implementation must guarantee that after `on_copy` is called,
/// the newly copied value cannot cause a double-free error together with
/// the original value.
pub unsafe trait CopyMut {
    // Shallow copy only. To copy a `CopyMut` value, first call `on_copy` 
    // on the original value to allow a new duplicated value to be created,
    // then call `memcpy` to create a new duplicated value.
    fn on_copy(this: &shr mut Self);
}
impl<T: Copy> CopyMut for T {}
impl<T> CopyMut for Rc<T> {
    fn on_copy(this: &shr mut Self) {
        unsafe { Rc::increment_strong_count(Rc::as_ptr(this)) }
    }
}

impl<T> DerefTarget for Rc<T> {
    type Target = T;
}

impl<T> DerefConst for Rc<T> { /* ... */ }
impl<T> DerefShr for Rc<T> { /* ... */ }
impl<T> DerefShrMut for Rc<T> { /* ... */ }
// No DerefMutV2 for Rc<T>

impl<T> !Send for Rc<T> {}
impl<T> !SyncV2 for Rc<T> {}
impl<T> CopyMut for Arc<T> { /* ... */ }

impl<T> DerefTarget for Arc<T> {
    type Target = T;
}

impl<T> DerefConst for Arc<T> { /* ... */ }
impl<T: SyncV2> DerefShr for Arc<T> { /* ... */ }
impl<T: SyncV2> DerefShrMut for Arc<T> { /* ... */ }
// No DerefMutV2 for Arc<T>

// Here `T: SyncV2` is unnecessary. Note that
// `DerefShr` and `DerefShrMut` requires `SyncV2`,
// so for `T: !SyncV2`, `Arc<T>` is intended for sharing
// read-only data.
unsafe impl<T: Send> Send for Arc<T> { /* ... */ }
unsafe impl<T: SyncV2> SyncV2 for Arc<T> { /* ... */ }

Vec<T>, and LinkedList<T>

impl<T> Vec<T> {
    // `push` needs an exclusive mutable reference, because it may realloc.
    pub fn push(&mut self, value: T) { /* ... */ }

    // `push_within_capacity` only needs a shared mutable reference,
    //  as it never realloc.
    pub fn push_within_capacity(
         &shr mut self,
         value: T,
    ) -> Result<(), T> { /* ... */ }

    /// `pop` may drop the value inside the `Vec`, so `&shr mut self` is not
    /// enough. It must take an `&mut self`.
    pub fn pop(&mut self) -> Option<T> { /* ... */ }
}

impl<T> LinkedList<T> {
    // Adding new elements to a linked list will not reallocate,
    // so an `&shr mut self` is enough for memory safety.
    pub fn push_back(&shr mut self, value: T) { /* ... */ }
    pub fn push_front(&shr mut self, value: T) { /* ... */ }

    // However, removing elements from a linked list can drop,
    // so an `&mut self` must be required.
    pub fn pop_back(&mut self) -> Option<T> { /* ... */ }
    pub fn pop_front(&mut self) -> Option<T> { /* ... */ }
}

Mutex<T>, ReentrantLock<T>, and RwLock<T> (non-poison versions)

For Mutex<T> and ReentrantLock<T>, their API are almost the same. There is at most one thread can access the data, so T: SyncV2 is not needed for DerefShr and DerefShrMut.

But for RwLock<T>, there can be more than one threads holding shared references (&const T, &shr T, and &shr mut T), so DerefShr and DerefShrMut requires T: SyncV2.

impl<T> Mutex<T> {
    pub fn lock(&shr mut self) -> MutexGuard<'_, T> { /* ... */ }
    // get `&const T` without locking
    pub fn get_ref(&self) -> &const T { /* ... */ }
    // get `&mut T` without locking
    pub fn get_mut(&mut self) -> &mut T { /* ... */ }
}
unsafe impl<T: Send> Send for Mutex<T> {}
unsafe impl<T: Send> SyncV2 for Mutex<T> {}

impl<T> DerefShr for MutexGuard<'_, T> { /* ... */ }
impl<T> DerefShrMut for MutexGuard<'_, T> { /* ... */ }
impl<T> DerefMutV2 for MutexGuard<'_, T> { /* ... */ }
impl<T> ReentrantLock<T> {
    pub fn lock(&shr mut self) -> ReentrantLockGuard<'_, T> { /* ... */ }
    // get `&const T` without locking
    pub fn get_ref(&const self) -> &const T { /* ... */ }
    // get `&mut T` without locking
    pub fn get_mut(&mut self) -> &mut T { /* ... */ }
}
unsafe impl<T: Send> Send for ReentrantLock<T> { /* ... */ }
unsafe impl<T: Send> SyncV2 for ReentrantLock<T> { /* ... */ }

impl<T> DerefShr for ReentrantLockGuard<'_, T> { /* ... */ }
impl<T> DerefShrMut for ReentrantLockGuard<'_, T> { /* ... */ }
impl<T> RwLock<T> {
    pub fn read(&shr mut self) -> RwLockReadGuard<'_, T> { /* ... */ }
    pub fn write(&shr mut self) -> RwLockWriteGuard<'_, T> { /* ... */ }
    // get `&const T` without locking
    pub fn get_ref(&const self) -> &const T { /* ... */ }
    // get `&mut T` without locking
    pub fn get_mut(&mut self) -> &mut T { /* ... */ }
}
unsafe impl<T: Send> Send for RwLock<T> { /* ... */ }
unsafe impl<T: Send> SyncV2 for RwLock<T> { /* ... */ }

impl<T: SyncV2> DerefShr for RwLockReadGuard<'_, T> { /* ... */ }
impl<T: SyncV2> DerefShrMut for RwLockReadGuard<'_, T> { /* ... */ }

impl<T> DerefShr for RwLockWriteGuard<'_, T> { /* ... */ }
impl<T> DerefShrMut for RwLockWriteGuard<'_, T> { /* ... */ }
impl<T> DerefMutV2 for RwLockWriteGuard<'_, T> { /* ... */ }

AtomicI*, AtomicU*, AtomicBool and AtomicPtr

impl Atomic{{T}} {
    // using `&shr self` or `&shr mut self` can distinguish whether the
    // method will mutate the data
    pub fn load(&shr self, ordering: Ordering) -> {{T}} { /* ... */ }
    pub fn store(&shr mut self, value: {{T}}, ordering: Ordering) { /* ... */ }
    // ...
}

  1. but within the limit of Sync ↩︎

  2. &shr T is only for disambiguity with current Rust. It can be abbreviated as &T if no ambiguity ↩︎

  3. no T: Send because &shr mut T cannot move or drop T ↩︎

  4. only &mut T can be obtained ↩︎

First, using types for that instead of reference kinds is much better since it allows encapsulating the unsafety (how does your model handle Cell? RefCell? Mutex? How do you have a type that is only partially interior-mutable?)

But even assuming your model is superior, we already have a working model. Why change it? Your only motivation is the naming confusion. While I may understand advocating for calling & "shared" and not "immutable" (this is indeed its defining property, not mutability), that's far from a satisfying reason for changing such core language feature. Your motivation section is extremely light when it should be extremely heavy.

2 Likes

ofc most of this this is breaking, but notably &const T : Send requiring T : Send is a strong break from the current status quo.

why you did you choose chose to do it this way ?

I would really suggest you to focus on:

  • what problem does this solve/why do we need this
  • what are the concrete benefits compared to the current approach
  • why would this be sound
4 Likes