[Idea] A third kind of reference between "mutable" and "immutable" in Rust

Hi, everyone. This is my first post here. This post is not a feature proposal; rather, it is a theoretical exploration that reinterprets Rust’s existing mutability model.

Rust already has two reference types, &T and &mut T. Conceptually, however, introducing a third kind between "mutable" and "immutable" might make the mutability model feel more complete -- in the sense of Rust's mutability XOR aliasing requirement.

TL;DR

  • This post is a thought experiment, not a proposal, exploring a third form of mutability in Rust, distinct from both &T and &mut T, modeled as &in T.
  • The core idea is to treat &in T as a restricted form of &mut T, supported by factoring out a notion of "pre-mutability" from immutability for variables and bindings, purely by reframing existing semantics.
  • By reinterpreting dereferencing, place expressions, and interior mutability, the post argues that such a reference could fit naturally into Rust’s existing mutability and aliasing model, without proposing any concrete language change.
  • The post is intentionally long, as it examines existing Rust semantics from an alternative perspective, with a comprehensive and explicit focus on compatibility.

Terminology and conventions

This post introduces several hypothetical concepts and adopts some terminology that differs slightly from current Rust usage.

  • Pre-mutable refers to the mutability of bindings and variables not declared with mut, rather than calling them immutable.
  • Transitively immutable is used to describe the immutability enforced by shared references (&T), when the distinction matters.
  • Non-mutable refers to place expressions that are not mutable, encompassing both pre-mutable and transitively immutable cases.
  • As a result, non-mutable and immutable may carry slightly different nuances in this post.
  • PE is used as an abbreviation for "place expression".
  • non-mut refers to being declared without the mut keyword.

Introduction

&in T: a third kind of reference type

In this post, I introduce a hypothetical reference syntax &in T (standing for "indirect", "inactive" or "internal"; notably, in is already a keyword). The core idea is to allow creating a unique reference from a so-called "immutable" binding using &in, like this:

    let mut a = 42;
    let x = &mut a; // "immutable binding" of a mutable reference
    *x = 29; // (1)

    let y = &in x; // a hypothetical `&in T`
    **y = 17; // (2)

There is another key idea: dereferencing &in T restores the mutability of the original "immutable" variable it was created from. In this sense, the mutability of &in T is indirect. As a result, &in T behaves like a restricted &mut T: it cannot be "immutable" via a single dereference, but may be assigned to when dereferenced more than once.

In the example above, in current Rust an "immutable" variable x of type &mut i32 can still produce a mutable place expression when dereferenced, so assignment through *x is permitted in (1). Similarly, in (2), y of type &in &mut i32 could be assigned to when dereferenced twice (**y).

On the other hand, x itself is not assignable: it can be read but cannot be written to: assigning to x directly like let mut b = 0; x = &mut b; would cause an error, while a read access like println!("{:p}", x) would compile. Similarly, println!("{:p}", *y) would compile but *y = &mut b; would cause an error.

A shared reference &T cannot express this behavior, because shared references enforce transitive immutability: the result of dereferencing a &&mut T is an immutable place expression. Unlike variables, the immutability of shared references propagates.

Note that, ignoring system-level differences, C++ allows declaring a pointer which behaves like y of type &in T above (a const pointer to a non-const int):

    // C++                 // Rust
    int a = 42;            // let mut a = 42;
    int* const x = &a;     // let x = &mut a;
    int* const* y = &x;    // let y = &in x;
    **y = 29;              // **y = 29;

(Full code: cpp.sh)

"pre-mutable": rather than "unique immutable"

To support the new syntax, I introduce a hypothetical notion of "pre-mutable" place expressions -- meaning not currently mutable, but with the potential to become mutable: its mutability is just inactive. This would restate some part of the semantics of "immutable" variables.

These semantics are simple and not entirely new: Rust already has a similar capability as unique immutable borrows in captures, where implicit borrows could be made explicit with &in:

    let mut a = 42;
    let x = &mut a;
    let mut c = || {    // let mut c = |y: &in i32| {
        *x = 29;        //     **y = 29;
    };                  // };
    c();                // c(&in x);

This suggests that a third kind of mutability already exists in Rust in some form. But it cannot currently be used anywhere else in the language and cannot be written out explicitly. Therefore, it might be useful to show how the Rust Reference would change, as I discuss below, even though this is not a proposal.

But why this feature should be presented as a dormant kind of mutable rather than "unique immutable"? It may sound like a "glass-half-empty or half-full" question, hinging on uniqueness, but there are reasons to view this as a form of mutability rather than immutability.

(1) Mutability XOR aliasing requirement

The Rust Edition Guide uses the term "mutability XOR aliasing requirement." Let us take this XOR literally and consider how to make the interpretation complete.

In Rust, mutable references are required to be unique, while shared references are required to be transitively immutable. On the other hand, as we have seen so far, unique immutable borrows and immutable variables that are not currently borrowed are not transitively immutable. If we summarize these cases in a table, we obtain the following:

mutable immutable
(not transitive)
immutable
(transitive)
shared
(aliasing)
shared references
(&T)
unshared
(unique)
mutable references
(&mut T)
unique immutable borrows
immutable variables not currently borrowed

If we group the columns "mutable" and "immutable (not transitive)" together, and place "immutable (transitive)" in a separate group, the table takes the same shape as a logical XOR. Since this is an XOR, and the bottom-right cell is empty, the converse implications also hold: if a reference is "immutable (transitive)", then it must be "shared", and if it is "unique", then it must be either "mutable" or "immutable (not transitive)."

If we replace "immutable (not transitive)" with "pre-mutable," it becomes visually clear that "mutable" and "pre-mutable" belong to the same group. This also removes the need to explicitly refer to transitivity when talking about immutability, resulting in a simpler and more concise formulation.

(2) Access permissions on x beyond &mut *x

Given let mut x = &mut a;, Rust admits several ways to obtain mutable access to a:

  • &mut x (writable x, writable a)
  • &mut *x (a reborrow; writable a, but no access to x)
  • a hypothetical &in x (readable x, writable a)

All three provide write access to a, but they differ in the permissions they have over x. &in x has only read access to x, placing it between &mut x, which has write access to x, and &mut *x, which cannot access x at all. Since &in x lies between &mut x and &mut *x in this sense, it is reasonable to treat &in x as a form of "mutable" reference.

Note that these three references can be compared in terms of their permissions on x, since they are all created from x. In particular, the Tree Borrows model treats &mut *x as being "derived from" x and positions it as a child of x in the borrow tree.

This interpretation also suggests that introducing &in x and &in *x would not require a major change to the TB model: they can be seen as restricted forms of &mut x and &mut *x, respectively, which do not grant write access to their parent.

(3) Variable allocation pointer

A variable binding has access to a memory location, which in LLVM IR is represented as a variable’s allocation pointer. Because variables may be uninitialized, viewing a variable as a kind of pointer implies the existence of dangling-like states (even if such pointers are never dereferenced), which requires careful handling. Still, since the Rust compiler itself can be written in Rust, variables must have some pointer-like aspect.

However, immutability behaves differently for variables and for references, particularly with respect to transitivity. Setting aside the uninitialized-as-dangling issue, treating variables as immutable in the same sense as pointers would lead to confusion. Variable immutability therefore calls for a notion distinct from pointer immutability.

One interesting observation that arises in examining &in T is the appearance of the hypothetical types mut T and in T. The type mut T is introduced primarily as a trick to resolve issues in the Deref and Index trait families, but it could also be interpreted as representing the allocation pointer of a mutable variable.

Its symmetric counterpart, in T, though unnecessary for the present discussion, is likewise worth describing as a type related to place expressions that may become mutable when dereferenced. This discussion provides a general reference for reasoning about mutability in the context of variable allocation pointers.

mutable pre-mutable (hypothetical) immutable
variable declaration let mut x let x
reference type &mut T &in T (hypothetical) &T
raw pointer type *mut T *in T (hypothetical) *const T
(variable allocation pointer?) mut T (hypothetical) in T (hypothetical)

That said, in the present discussion, mut T always appears in a borrowed state, so the uninitialized-as-dangling issue does not arise. Moreover, variable allocation pointers are outside the scope of this post and will not be discussed in detail here, aside from a few informal remarks.

Outline

This is a purely theoretical thought experiment, not a proposal. By treating &in T as a restricted form of &mut T, it deliberately avoids lifetime-related questions and instead reexamines existing Rust semantics around mutability and aliasing in a non-breaking way.

This post is organized into three parts:

Part 1: Introduction & motivation presents &in T as a restricted form of &mut T and argues that so-called "immutable" bindings are better understood as potentially mutable, since their mutability can be exercised indirectly by chaining uniqueness through references.

Part 2: Pre-mutable Place Expressions reframes existing semantics by isolating non-mut variables from immutable place expressions via the dereference operator, filling the gap exposed by double dereferencing with types such as &in T, and organizing the result into a mutability transition state machine.

Part 3: Interior Mutability brings the third kind of mutability developed so far into a range of discussions around Rust’s mutability model, checks that it fits naturally while preserving the existing context and reasoning, and explores the new perspective it reveals.

&in T: motivation

&in T serves as an experimental model for a third form of mutability between immutable and mutable references. As a middle ground, while this unique reference behaves like a restricted &mut T and cannot be assigned to through a single dereference, it can be created from a variable that is declared without mut (a non-mut variable) and may allow mutation through deeper dereferencing.

To examine the behavior of &in T, a one-field struct wrapping &mut T serves as a particularly illustrative example. We begin by revisiting its relationship with unique immutable borrows, discussed earlier.

Consider the following code, written in current Rust, in which the update function mutates the referent of a &mut i32 field:

struct X<'a>(&'a mut i32);

fn update(y: &mut X) {
    //        --- `mut` required

    *y.0 = 29;
}

fn main() {
    let mut a = 42;
    let mut x = X(&mut a);
    //  --- `mut` required

    update(&mut x);
}

Inside update, the dereferenced field is assigned to. Consequently, the function parameter y must have type &mut X. To pass an argument of this type at the call site, the local variable x must therefore be declared with mut.

At first glance, this requirement appears natural. However, it becomes questionable when we conceptually inline the body of update at the call site:

    let mut x = X(&mut a);

    {
        *x.0 = 29;
    }

In this case, the compiler warns that x does not need to be mutable:

warning: variable does not need to be mutable
  --> src/main.rs:11:9
   |
11 |     let mut x = X(&mut a);
   |         ----^
   |         |
   |         help: remove this `mut`
   |

This suggests that the mut on x is not semantically essential. Similar behavior can be observed when the block is replaced with a closure. Thanks to unique immutable borrows in captures, the closure may mutate through x even though x itself is not declared mut:

    let x = X(&mut a);
    // ^ `mut` removed

    let mut updater = || {
        *x.0 = 29;
    };
    updater();

In both cases, mutation of the referent is permitted without requiring x itself to be mutable. Taken together, these observations suggest that, conceptually, calling a function like update should not require a mut binding either.

The hypothetical reference type &in T enables precisely this thought experiment:

struct X<'a>(&'a mut i32);
//               ^^^ `mut` in scope

fn update_in(y: &in X) {
    //           ^^ `mut` removed

    *y.0 = 29;
}

fn main() {
    let mut a = 42;
    let x = X(&mut a);
    // ^ `mut` removed

    update_in(&in x);
    //         ^^ `mut` removed
}

This naturally raises the question: why should mutation be still allowed inside update_in despite the absence of mut at the call site?

One possible explanation is that the minimal condition for mutation is already satisfied by the item-level declaration of struct X itself: the fact that X contains a &mut i32 field, and that this declaration is in scope within update_in, suffices to permit mutation of the referent. Any additional mut keywords at the variable or parameter level therefore become redundant and can be eliminated without changing the program’s meaning.

From the signature fn update_in(y: &in X) alone, mutation inside the function body is already expected; otherwise, &in X would serve no purpose, and &X would suffice. That said, &X does not work in this case. In particular, update(y: &X) fails because reference immutability in Rust is transitive, whereas the immutability of a local variable is unique to its place.

As noted earlier, &in T corresponds to a const pointer to a non-const T in C/C++. In LLVM IR, however, a function parameter of this type might be marked noalias but not readonly, just like &mut T. This further illustrates that &in T is a restricted form of &mut T: it enables precisely the mutation that &T cannot.

In this sense, &in T does not introduce new mutability, but rather exposes and reuses mutability that already exists, collapsing it back to where it originates.

Direct and indirect mutation via dereference

The behavior of the hypothetical &in T can be explained by a simple principle: upon dereferencing, it recovers the mutability of its referent. This principle can, in fact, be applied to &mut T as well. The idea becomes clearer if we distinguish between direct mutation -- assigning to a place without dereferencing -- and indirect mutation -- assigning through dereferencing.

For example:

    let mut a = 0;
    a = 42; // direct mutation of `a`

    let x = &mut a;
    *x = 29; // indirect mutation of `a` via dereference

Here, x is bound without mut, yet *x = 29 is allowed. This can be understood as follows: because the referent a is directly mutable, mutation remains possible indirectly through dereferencing.

The same reasoning applies to &in T:

    let x = &mut a;
    let y = &in x;

    // *y = &mut 17; // not allowed
    **y = 17; // allowed

Here, x is not directly mutable, so neither direct mutation of x nor indirect mutation via *y is permitted. However, since a is directly mutable, mutation becomes possible after an additional dereference. In both cases, mutability is recovered precisely at the level where the underlying place is mutable.

In this sense, unique references &in T and &mut T simply extend the mutability of the referent along a chain of unique access. This is possible because a local variable binding that is not currently borrowed also has unique access to the value it is bound to, so the uniqueness chains.

This explains why *x is mutable in the example above. The reference &mut a is unique with respect to a, and the variable x has unique access to that reference; together, this chain of uniqueness satisfies the minimal condition for mutation. Extending the chain further with &in x preserves this uniqueness, allowing mutability to be recovered after additional dereferencing.

Because the uniqueness involved is fundamentally the same, mutability can be treated uniformly whether it is exercised directly or indirectly. The distinction lies only in the number of dereferences involved, with direct mutation corresponding to zero.

This can also be viewed as an analogy to left multiplication by 1 in algebra. For any value M, we have 1 × M = M (left identity), and likewise 1 × 1 × M = M. A unique reference created from M can be viewed as behaving in this way, so the resulting mutability remains that of M, as determined by the final referent.

Under the same analogy, a shared reference &T can be understood as an absorbing element: just as any product containing 0 evaluates to 0, the presence of a shared reference anywhere along a dereference chain transitively forces the result to be immutable, regardless of the referent’s mutability.

In the following sections, we examine the semantics that would support &in T. Since &in T and &mut T are assumed to follow the same underlying principle, their behavior is often symmetric. By contrast, &T would be treated in a fundamentally different way.

Pre-mutable place expressions

To support the hypothetical third kind of reference type &in T -- which can be created from non-mut variables and recover the mutability of the original variable upon dereferencing -- we now examine the semantics of pre-mutable place expressions. As it turns out, this requires only a restatement of existing rules in the Rust Reference.

In current Rust, place expressions (hereafter abbreviated as PEs) are classified into two kinds: mutable and immutable. A pre-mutable PE is not itself mutable, but retains the potential to become mutable; it thus represents a middle ground between mutable and immutable.

In the Rust Reference, immutability appears to be specified explicitly in the context of behavior considered undefined, whereas mutability is not. Elsewhere, the situation seems to be largely reversed: the behavior of mutable PEs is spelled out explicitly, while immutable PEs are defined implicitly as those to which the mutable rules do not apply.

This distinction can be seen in the section "Mutability" in "Place Expressions and Value Expressions" of the Rust Reference, which defines immutable PEs simply as the complement of mutable ones with the word "other":

For a place expression to be assigned to, mutably borrowed, implicitly mutably borrowed, or bound to a pattern containing ref mut, it must be mutable. We call these mutable place expressions. In contrast, other place expressions are called immutable place expressions.

However, not all immutable PEs behave uniformly. This becomes apparent in the description of "the dereference operator" in "Operator Expressions" of the Rust Reference:

If the expression is of type &mut T or *mut T, and is either a local variable, a (nested) field of a local variable, or is a mutable place expression, then the resulting memory location can be assigned to.

This rule implicitly divides non-mutable PEs into two groups: those for which dereferencing &mut T or *mut T yields an assignable memory location -- namely, local variables and their (nested) fields -- and those for which it does not. This suggests that the current category of immutable PEs already contains a meaningful internal distinction.

We can therefore refine the classification by separating the former group from immutable PEs and defining it independently. This newly isolated group is what we call pre-mutable place expressions. At a minimum, pre-mutable PEs include local variables and (nested) fields of local variables that are not themselves mutable PEs -- those declared without mut -- though the category may be extended further if needed. Conceptually, the change in classification can be illustrated as follows:

               +------------+--------------------------------------------+
current Rust:  | mutable PE |          other PE (= immutable PE)         |
               +------------+--------------------------------------------+

                             <------------- non-mutable PE ------------->
               +------------+----------------+---------------------------+
hypothetical:  | mutable PE | pre-mutable PE | other PE (= immutable PE) |
               +------------+----------------+---------------------------+

Here, the boundary between "mutable" and its complement, "non-mutable", remains unchanged, and "non-mutable" matches the notion of "immutable" in behavior considered undefined.

Having introduced pre-mutable PEs as a distinct category, we can now define them explicitly in the "Mutability" section, resembling the style used for mutable PEs in the Reference. The following description would be added after the definition of mutable PEs, and after pre-mutable PEs, immutable PEs are defined as the rest:

For a place expression which is not mutable to be pre-mutably borrowed, implicitly pre-mutably borrowed, or bound to a pattern containing ref in, it must be pre-mutable. We call these pre-mutable place expressions.

With this definition in place, the earlier rule for "the dereference operator" can be rewritten in a more uniform way. Since any assignable memory location is, in effect, a mutable PE, we phrase the result accordingly. For later convenience, we also extend the set of types from &mut T and *mut T to include a hypothetical mut T:

If the expression is of type &mut T, *mut T, or mut T, and is either a pre-mutable place expression or a mutable place expression, then the result can be a mutable place expression.

This relationship can be visualized as follows:

                      type &mut T, *mut T or mut T
 +--------------------------------+      +============================+
 | a pre-mutable place expression |      | a mutable place expression |
 +--------------------------------+      +============================+
  Those bound without `mut`   |             |
   * a local variable         | dereference |
   * a (nested) field         |             |
     of a local variable      v    type T   v
                       +============================+
                       | a mutable place expression |
                       +============================+
                  * memory location which can be assigned to

What is important about this diagram is that PEs appear on both the input and output sides. This makes it possible to connect the diagram repeatedly, allowing dereference chains to be described compositionally and making the behavior of repeated dereferencing easier to reason about.

Note that the hypothetical type mut T, introduced here, is intended purely as a technical device to address issues that arise later in the discussion of Deref and Index trait families, and is otherwise unnecessary. The type mut T has the following properties:

  • it is dereferenceable (that is, a special kind of pointer),
  • it is automatically dereferenced whenever possible, and
  • dereferencing it can be a mutable place expression.

Although mut T is not required for the present discussion, a possible interpretation is worth noting. Under this interpretation, a binding such as let x: mut T would treat x as a pre-mutable PE of type mut T. Because mut T is automatically dereferenced, this would immediately yield a mutable PE of type T, producing the same effect as let mut x: T.

From this perspective, let mut x: T can be viewed as equivalent to let x: mut T, which motivates the naming of this pointer type. Consequently, local variables and their (nested) fields need not be explicitly described as being "bound without mut," and mut T can naturally occupy the corresponding position in the classification table above.

Ways to a mutable place expression via double dereference

With this, we are able to define pre-mutable PEs via the dereference operator. The next step is to position a hypothetical type &in T by means of double dereferencing. Consider the case where T is &mut U, for example, and the diagram above is extended downward to the left, as follows:

     type &in T, *in T or in T                 type &mut T, *mut T or mut T
+------------------+  +==============+    +------------------+  +==============+
| a pre-mutable PE |  | a mutable PE |    | a pre-mutable PE |  | a mutable PE |
+------------------+  +==============+    +------------------+  +==============+
         |             |                                  |             |
         | dereference |                                  | dereference |
         v             v                                  v             v
     +------------------+             type T             +==============+
     | a pre-mutable PE |               ||               | a mutable PE |
     +------------------+  type &mut U, *mut U or mut U  +==============+
               |                                                |
               +-----------------+ dereference +----------------+
                                 |             |
                                 v    type U   v
                                +==============+
                                | a mutable PE |
                                +==============+

In current Rust, there is nothing that can occupy the top-left position. However, if there were a type that could act as a pre-mutable PE when dereferenced, it would fit there. Since pre-mutability corresponds to the mutability of variables declared without mut, the hypothetical type &in T satisfies this role. Furthermore, taking symmetry into account, we can position the hypothetical types &in T, *in T, and in T; these correspond to &mut T, *mut T, and mut T, respectively.

Mutability transitions with each dereference shown above, taking into account the transitivity of shared references, can be summarized by the following state machine. Dereferenced reference types are used as representative inputs, and the parts corresponding to T and U that change with each transition are omitted:

   +-[*&mut]-+  +---[*&mut]---+      +-[*&in]-+
   |         |  |             |      |        |
   |         v  v             |      |        v
  +--------------+           +------------------+
  | a mutable PE | -[*&in]-> | a pre-mutable PE |
  +--------------+           +------------------+
                    \           |
                      [*&]    [*&]  +-[ANY]-+
                          \     |   |       |
                            v   v   |       v
                              +-----------------+
                              | an immutable PE |
                              +-----------------+

From this state machine, we can see that a mutable PE and a pre-mutable PE are mutually reachable, while an immutable PE is a dead end (i.e., a sink node). Once execution reaches an immutable PE, it remains non-mutable thereafter.

Moreover, from a pre-mutable PE, repeated application of *&in transitions alone cannot reintroduce mutability, yielding the same outcome as applying *&; intuitively, this can be read as ALL sequences of *&in behaving like ANY *& with respect to non-mutability.

Necessity: an algebraic perspective

Earlier, we noted that unique references resemble multiplication by 1, while shared references resemble multiplication by 0. Using the state machine above, we can now make this algebraic intuition more precise. This revisiting shows that the existence of pre-mutable PEs is not merely sufficient but necessary.

The machine has two defining properties: (1) once a state becomes an immutable PE, it remains there, and (2) otherwise the state is determined solely by the most recent input. Consequently, each state can be represented by a single dereference of a reference type (e.g., a mutable PE of type T corresponds to *&mut T).

This means that the result of a double dereference can be expressed in terms of a single dereference (e.g., **&in &mut T is observationally equivalent to *&mut T). Ignoring the trailing T and viewing it as an expression in variadic prefix notation (e.g., 2 + 3 + 4 as ++ 2 3 4), we can define a binary operation over borrow operators, summarized in the following table:

... &T ... &in T ... &mut T property
**&... *&T *&T *&T Absorbing element (0 × M = M × 0 = 0)
**&in ... *&T *&in T *&mut T Left identity (1 × M = M)
**&mut ... *&T *&in T *&mut T Left identity

As shown in the table, & acts as an absorbing element (as implied by (1)), while &in and &mut act as left identities (as implied by (2)). As a result, this operation is both substitutive (Leibniz’s Law; e.g., if 2 + 3 = 5, then 2 + 3 + 4 = 5 + 4) and associative (e.g., (2 + 3) + 4 = 2 + (3 + 4)), allowing dereference chains to be locally lengthened and shortened, much like arithmetic expressions, although the operation is not commutative (unlike 3 + 4 = 4 + 3).

A similar table can be constructed for dereferencing reborrows, such as *&in (*&mut T), as follows:

... *&T ... *&in T ... *&mut T
*&... *&T *&T *&T
*&in ... (undefined) *&in T *&in T
*&mut ... (undefined) (undefined) *&mut T

The entries marked (undefined) in the bottom-left corner, for example, indicate that a mutable reference cannot be created from an immutable PE. Observing each row, the result always coincides with the first operand, which is expected since only a single dereference is involved.

Comparing the two tables, the only differing result appears in the middle-right cell. One interpretation of this difference is that &in does not behave like &mut under a single dereference, but may do so under a double dereference. This suggests that embedding a third kind of mutability, lying between mutable and immutable, into the state machine naturally yields the dereference behavior described above.

Note that the borrowing operators and reference types appearing in the tables are used only as representative notation. The tables describe mutability transitions, which include not only dereferencing but also field access (via inherited mutability) and indexing, as will be discussed later.

For example, the idempotence (e.g., a * a = a) visible along the diagonal of the table suggests that each of &, &in, and &mut can be regarded as equivalent to &&, &in &in, and &mut &mut, respectively. This can be interpreted as expressing inherited mutability on field access.

Accordingly, the algebra shown above can be applied flexibly. For instance, a mutable variable of type T may be represented as *&mut T, which can also describe a mutable reference created from it. This expression may then be extended to **&in &mut T, representing a non-mutable variable of type &mut T, since *&in denotes a pre-mutable PE.

Moreover, this expression may be further lengthened to ***&in &in &mut T, which can be interpreted as a non-mutable variable whose type is a one-field struct containing an &mut T. In this interpretation, the first *&in corresponds to the pre-mutability of the variable, the second to inherited mutability, and the final &mut T to the type of the field. This explains why structs wrapping &mut T serve as useful examples in this post.

At the same time, this perspective leaves open intriguing questions, such as the meaning of ***(&in &mut) &mut T (interior mutability?), and why dereferencing pointer types can represent the mutability of variables (variable-allocation pointers?), which could be explored within safe Rust’s reference system.

Coercion and conversion

A third kind of mutability lies between mutable and immutable. This ordering becomes relevant when considering type coercions between references and raw pointers. The following diagram summarizes how the hypothetical reference type &in T and raw pointer type *in T would be embedded into the existing coercion system, together with conversions (e.g., unsafe { &mut *raw_pointer }):

                  mutable    pre-mutable   immutable
                ===========  ===========  ===========

   reference       &mut T ----------------->  &T
     types   :      ^ | \                   ^ ^ |
                    ` |  `----> &in T -----'  ` |        -----> (dashed)
                    ` |   (1)    ^ |    (2)   ` |       coercion
                    ` |          ` |          ` |
                    ` |      (3) ` | (3)      ` |
                    ` |          ` |          ` |
                    ` |   (1)    ` v    (2)   ` |        - - -> (dotted)
 raw pointer        ` |  ,----> *in T -----.  ` |      conversion
    types    :      ` v /                   v ` v
                   *mut T ---------------> *const T

The six arrows forming the surrounding quadrilateral represent the four existing coercions (dashed arrows, left-to-right and top-to-bottom) and the two conversions (dotted arrows, bottom-to-top). The two pre-mutable types are expected to admit (1) coercions from mutable types, (2) coercions to immutable types, and (3) both coercions and conversions between each other.

Property (1) holds because pre-mutable types can be regarded as a restricted form of mutable types. Property (2) may appear a bit tricky, but it follows from the fact that conversions from mutable to immutable types already exist in the current system. Property (3) holds because a reference is, fundamentally, just a raw pointer for which the compiler enforces safety guarantees.

Furthermore, once (1) and (2) hold, the transitivity of type coercion implies the existing coercions from mutable to immutable types as well.

from &T to &(mut T), and from &mut T to &mut (mut T) and &in (mut T), seen later in Deref and Index trait families.

What can be a pre-mutable place expression

So far, we have discussed pre-mutable PEs primarily in terms of their behavior under dereferencing. However, there are additional constructs that can yield a pre-mutable PE. We can list these almost mechanically by reusing the existing "mutability" rules for mutable PEs: we strike through occurrences of mutable and replace them with the third kind, pre-mutable, as follows.

The following expressions can be pre-mutable place expression contexts:

  • Mutable Pre-mutable variables which are not currently borrowed.
  • Mutable Pre-mutable static items.
  • Temporary values. [deleted]
  • Fields: this evaluates the subexpression in a mutable pre-mutable place expression context.
  • Dereferences of a *mut T *in T and in T pointer.
  • Dereference of a variable, or field of a variable, with type &mut T &in T. Note: This is an exception to the requirement of the next rule.
  • Dereferences of a type that implements DerefMut DerefIn: this then requires that the value being dereferenced is evaluated in a mutable pre-mutable place expression context.
  • Array indexing of a type that implements IndexMut IndexIn: this then evaluates the value being indexed, but not the index, in mutable pre-mutable place expression context.

Note that the hypothetical pointer type in T is introduced alongside *in T, while the pointer type mut T is likewise introduced alongside *mut T.

What is crucial here is that temporary values are the only case that must remain mutable. They are created for value expressions in most place-expression contexts, and seem to play an essential role in (re)setting mutability to a mutable PE. While pre-mutable temporaries might be possible, this is beyond the scope of this discussion. Consequently, temporaries are removed entirely from the list above and are not mentioned further.

DerefIn and IndexIn traits

When a third kind of reference type is introduced, it becomes necessary to define a third member in the Deref and Index trait families: namely, traits that provide methods whose receivers and returned types are of type &in T. Accordingly, following the pattern of the existing family members, we introduce a DerefIn trait with a method deref_in, and an IndexIn trait with a method index_in:

trait DerefIn: DerefMut {
    fn deref_in(&in self) -> &in Self::Target;
    //          ^^^          ^^^
}

trait IndexIn<Idx>: IndexMut<Idx> where Idx: ?Sized {
    fn index_in(&in self, index: Idx) -> &in Self::Output;
    //          ^^^                      ^^^
}

These third traits should exist only when the first and second traits exist, so DerefMut and IndexMut are required as supertraits. The associated types Target and Output are already defined by Deref and Index, respectively. In what follows, indexing can be explained analogously, so we focus on dereferencing.

Although these third traits are introduced purely by symmetry, they introduce a new issue: the returned type is now fixed to the form &in T. In current Rust, a one-field struct wrapping an &mut T can, when declared with mut, return that field directly via a properly defined deref_mut method (playground). As always in this post, we would like such a struct to continue working even when it is declared without mut, as in the following example:

    let mut a = 42;
    let x = X(&mut a); // declared without `mut`
    *x = 29;

The dereference operator is used on the left-hand side of an assignment, i.e., in a mutable context. For such a situation, in current Rust, *x is equivalent to *DerefMut::deref_mut(&mut x). Now, let's suppose that if the operand is pre-mutable, and is not a pointer type, then *x is equivalent to *DerefIn::deref_in(&in x) instead.

However, the deref_in method -- which conceptually wants to return an &mut T -- must have the returned type &in T. On the caller side, this is dereferenced into a pre-mutable PE, which cannot be immediately assigned to. To address this issue, let us follow the process step by step:

  1. In deref_in method, the field of type &mut T is coerced at function results to the returned type &in Self::Target .
  2. The caller receives an &in Self::Target.
  3. Dereferencing &in yields a pre-mutable PE of type Self::Target.
  4. Somehow, this must become a mutable PE of type T.
  5. The result is assigned to.

Thus, something special must occur at step (4). In fact, the properties previously assigned to the hypothetical type mut T are designed to apply precisely at this point:

  • It is dereferenceable (that is, a special kind of pointer), because the transition from pre-mutable to mutable was defined via the dereference operator.
  • It is automatically dereferenced whenever possible, since the dereference operator is consumed by the leading &in of the returned type, leaving no explicit dereference operator to apply.
  • Dereferencing it can be a mutable PE, which is required in order to assign to it.

Assuming the existence of such a hypothetical type mut T with these properties, the remaining step is simply to choose mut T as Self::Target:

impl<T> Deref for X<T, _> {
    type Target = mut T; // mut T

    fn deref(&self) -> &Self::Target { self.0 }
}                   // &(mut T) <----- &mut T

impl<T> DerefMut for X<T, _> {
    fn deref_mut(&mut self) -> &mut Self::Target { self.0 }
}                           // &mut (mut T) <----- &mut T

impl<T> DerefIn for X<T, _> {
    fn deref_in(&in self) -> &in Self::Target { self.0 }
}                         // &in (mut T) <----- &mut T

With this choice, the effect of specifying the associated type propagates across the entire trait family. As a result, at the step (2), in addition to the desired return type &in (mut T), the types &(mut T) and &mut (mut T) also appear as by-products. This is not problematic, however: &(mut T) is observationally equivalent to &T due to transitive immutability, and &mut (mut T) is observationally equivalent to &mut T due to preserved mutability.

In the end, changing the associated type Target from T to mut T affects only deref_in in a meaningful way; all other effects are not harmful. Finally, for the step (1), we could allow additional "observationally equivalent" coercions at function return sites: from &T to &(mut T), and from &mut T to &in (mut T) and &mut (mut T).

In conclusion, the resolution is type Target = mut T;. Though mut T is a pointer type, it's not dangling because it appears only in borrowed state. Interestingly, this coincides with the fact that what is returned is of type &mut T, which is created from a mutable variable out of the struct -- as seen earlier, a mutable binding let mut x: T is observationally equivalent to let x: mut T.

Interior mutability for a third kind of mutability

Since interior mutability is meant to complement inherited mutability, it is natural to expect that introducing a new kind of mutability, &in T, would have consequences. Interestingly, this seems to be related to a long-removed feature: the mut keyword on fields.

Before getting there, however, let us -- as usual -- start with the familiar example of a one-field struct wrapping &mut T.

Motivation: In-House or Outsourced

Earlier, in the section on the DerefIn trait, we saw that a one-field struct wrapping &mut T can return its field directly. Interestingly, current Rust allows the same effect for a struct containing an owned T. In both the in-house (owned: T) and outsourced (borrow_mut: &mut T) cases, dereferencing exposes a uniform mutable handle to the underlying T:

    let mut borrowed = 42;
    let mut foo = Outsourced { borrow_mut: &mut borrowed };
    //  ^^^ could be removed by introducing `&in` and `type Target = mut T`

    let mut bar = InHouse { owned: 42 };
    //  ^^^ how about that?

    *foo = 29;
    *bar = 29;
    println!("{}, {}", *foo, *bar);

(Full code: playground)

Since the two cases are indistinguishable under dereferencing, and assuming that InHouse allows mut to be removed from the variable binding by DerefIn, it is natural to expect the same to hold for Outsourced.

In practice, however, this runs into a problem. One could try for InHouse to reuse the same mechanism as Outsourced by making the function return &mut T (1), but in that case an &in receiver inherits its pre-mutability to the field. As a result, the field cannot be used as the operand of &mut (2):

impl<T> DerefIn for InHouse<T> {
    // type Target = mut T;                (1)
    //                       &in (mut T) <----- &mut T
    fn deref_in(&in self) -> &in Self::Target { &mut self.owned }
    //          receiver ----------------------------> field
    //        (pre-mutable)           (2)            (mutable)
}

This issue can be resolved by making the field mutable despite inherited mutability. The minimal requirement -- uniqueness -- is guaranteed, since &in is a unique reference to its struct when used in receiver position. The safe mechanism provided for doing so is interior mutability.

Here, if type Target = mut T, then treating the field as having type mut T would make this work. As discussed earlier, let mut x: T is observationally equivalent to let x: mut T. By the same reasoning, it is natural to guess that field: mut T might be observationally equivalent to mut field: T. Therefore, providing interior mutability for this case via something like mut on fields seems feasible.

Note that since fields may be deinitialized, equating them directly with the pointer type mut T raises dangling concerns. For the purposes of this discussion, we treat this only as an interpretation and defer the implementation to mut on fields.

mut on fields: in historical context of Rust 0.x

Some form of the mut keyword on struct fields existed up through Rust 0.5 (2012-12-21), but it was removed in 0.6 as part of a broader reorganization toward inherited mutability, complemented by interior mutability. Since this discussion revisits a feature that was once removed, it is worth briefly reviewing how and why that change happened.

In the Rust 0.5 tutorial, under Data structures > Structs, a form of mut on fields was explicitly documented. At the time, marking a field as mut was required in order to mutate it:

Fields that you want to mutate must be explicitly marked mut.

struct Stack {
    content: ~[int],
    mut head: uint
}

With a value of such a type, you can do mystack.head += 1. If mut were omitted from the type, such an assignment would result in a type error.

This description was removed in 0.6 and replaced by an explanation based on inherited mutability instead (including the term itself and its definitions that no longer appear formally in current documentation).

Inherited mutability means that any field of a struct may be mutable, if the struct is in a mutable slot (or a field of a struct in a mutable slot, and so forth).

With a value (say, mypoint) of such a type in a mutable location, you can do mypoint.y += 1.0. But in an immutable location, such an assignment to a struct without inherited mutability would result in a type error.

Looking at the 0.6 release notes (2013-04-03), this change appears to have been part of a larger, coordinated shift: &mut references became unaliasable, and several related features were removed or reworked at the same time:

  • The interior mutability qualifier on vectors, [mut T], has been removed. Use &mut [T], etc.
  • mut is no longer valid in ~mut T. Use inherited mutability.
  • Struct fields may no longer be mut. Use inherited mutability, @mut T, core::mut or core::cell.

Finally, in 0.7 (2013-07-03), this transition to inherited mutability was completed:

  • Struct fields may no longer be mutable. Use inherited mutability.
  • [...]
  • std: mut module and Mut type removed.

The consolidation of interior mutability continued in 0.9 (2014-01-09), where it was further organized around explicit mutability roots in the form of cells:

  • @mut has been removed. Use std::cell::{Cell, RefCell} instead.
  • [...]
  • std::cell::Cell and std::cell::RefCell can be used to introduce mutability roots (mutable fields, etc.). Use instead of e.g. @mut.

Then, in 0.10 (2014-04-03), the introduction of the Unsafe type completed the shape of what we now recognize as interior mutability:

  • An Unsafe type was introduced for interior mutability. It is now considered undefined to transmute from &T to &mut T without using the Unsafe type.

Looking back at this history, it appears that field mutability initially had to be annotated explicitly, and mut on fields served that purpose. With the later adoption of inherited mutability and interior mutability as complementary mechanisms, that need disappeared, and the feature was removed along with it.

If this interpretation is correct, then introducing a new kind into inherited mutability -- namely &in T and pre-mutable PEs -- would naturally require a corresponding complement on the interior-mutability side. From that perspective, reintroducing something like mut on fields would serve a different role than it did originally, and may therefore be worth reconsidering in that context of complementing inherited mutability.

On a side note: Mut type of Rust 0.6 can be found in mutable.rs; and in 0.11.0 (2014-07-02),

*~T has been removed from the language. This type is superseded by the Box type.

frozen/unfrozen and freeze/thaw

As we have seen, a third kind of mutability already exists implicitly in current Rust, and separating pre-mutability from "immutability" makes it explicit. In a historical context, a similar clarification can be achieved by distinguishing two different notions of "freeze," which in turn reveals room for a reconsidered mut on fields.

Freeze is a keyword for interior mutability, as suggested by the flag ty_is_freeze in "Dealing with Cells" of the Tree Borrows model. Historically, however, the term has been used with multiple meanings. In current documentation, it may refer either to being bound by a non-mut variable (e.g., "Freezing" in Rust by Example) or to being borrowed through a shared reference (e.g., "Ownership" in The Rustonomicon). The latter corresponds to the notion of Frozen and ty_is_freeze in the Tree Borrows model.

The Rust 0.6 tutorial already reflected this ambiguity by using two distinct pairings: frozen / unfrozen and freeze / thaw. The former appears in the section Freezing, which describes how a shared reference prevents mutation while it exists. The latter appears in the discussion of Vectors and strings as dual-mode data structures, where the mutability of elements is inherited from that of the vector. In current Rust, this can be simplified as follows:

    let vec = vec![42];
    // vec[0] = 29; // error

    let mut mut_vec = vec;
    mut_vec[0] = 29;

This idea was already articulated in "Generalizing inherited mutability" (2012, baby steps), where dual-mode is described as a kind of "switch":

Basically, the outermost qualifier is inherited down to all the data of the hashmap, which makes it very easy to "switch" the map between mutable and immutable (freeze/thaw).

Crucially, this "ease" relies on exclusive ownership:

My proposal is basically that we change this [existing] rule so that mutability always follows exclusive ownership. One consequence of this [proposed] rule is that when data structures are implemented using only exclusive ownership, they can easily be frozen and thawed.

This line of reasoning is instructive because it also explains why vec in the example above is able to switch its mutability: rebinding the value with mut upon move can be viewed as a form of thawing. In this sense, inherited mutability applies to the vector data structure itself, whereas the freeze/thaw distinction concerns transitions enabled by exclusive ownership.

From this perspective, frozen / unfrozen corresponds to the presence or absence of a shared reference, whereas freeze / thaw corresponds to transitions enabled by uniqueness. This contrast can be observed in the mutability transition state machine introduced earlier in the discussion of "ways to a mutable place expression via double dereference":

+--------------------------------------+ (self-loops are omitted)
|               ___ (2)                |
|              v   \                   |
|  a mutable PE ---> a pre-mutable PE  |
|           ^  \     |                 |
+-----------`----\---|-----------------+
            `      v v
        (1) \- - - an immutable PE

Under this interpretation:

  • Self-loops (omitted) represent inherited mutability.
  • Transitions within the rectangle correspond to freeze/thaw, enabled by uniqueness.
  • Transitions crossing the rectangle boundary correspond to frozen/unfrozen, induced by shared references.

Accordingly, the two transitions from non-mutable PEs can be interpreted as follows. Transition (1), the upward arrow from "an immutable PE" to "a mutable PE," corresponds to the API of UnsafeCell<T>, namely fn get(&self) -> *mut T, which provides mutability by overriding the transitive immutability inherited from a shared reference.

By contrast, transition (2), the leftward arrow from "a pre-mutable PE" to "a mutable PE" within the rectangle, represents a form of thawing. This transition is applicable to mut on fields in the context of &in T. As seen so far, interior mutability complements inherited mutability. It is therefore useful to make explicit the exterior non-mutability against which it operates. This relationship is summarized in the following table:

exterior: non-mutability interior: mutability roots
shared mutability dereference of a shared reference
(inherited transitive immutability)
UnsafeCell<T>
mut on fields
(reconsidered)
field access from a pre-mutable PE
(inherited pre-mutability)
mut keyword

Reconsidered mut on fields would not require the unsafe keyword, since safety is guaranteed by the uniqueness of &in self.

&in self and Restrictions on Visibility of mut on fields

Interior mutability inevitably involves trade-offs. For mut on fields, this naturally raises questions of visibility, a topic explored in RFC 3323 ("Restrictions"). Here, we take a slightly different approach than the RFC to examining the visibility of mut on fields in order to clarify the role of &in self.

Rust places great importance on variables being non-mutable by default. Based on this principle, users naturally expect that fields accessed through a non-mut binding cannot be directly assigned to. However, allowing mut on fields would make code such as the following possible:

    let x = X::new();
    x.0 = 42;

If such exceptional code is to be permitted, it must be recognizable as exceptional. If we choose an approach other than the unsafe keyword or UnsafeCell<T>, then, at a minimum, field mutability must not be usable in contexts where the corresponding mut annotation cannot be observed. Consequently, fields marked mut should be treated as opaque outside their visibility boundary. As a matter of syntax, unrestricted combinations of pub and mut should not coexist on fields.

Conversely, let us consider an example where mut on fields is fully encapsulated. The following Counter type is intentionally left unimplemented, as if only its required interface were specified:

pub struct Counter { 
    /* some fields */
}

impl Counter {
    pub fn new() -> Self { unimplemented!() }
    pub fn get(&self) -> u8 { unimplemented!() }
    pub fn count(&in self) { unimplemented!() }
    //           ^^^^^^^^
}

The method count takes &in self. Assuming mut on fields, using it instead of &self indicates that the method may mutate internal state, while still allowing it to be called through a non-mut binding:

    // "immutable" binding: disallow replacing `counter` with another.
    let counter = Counter::new();

    let previous = counter.get();
    counter.count();
    assert_ne!(previous, counter.get());

    // internal state is "frozen" while a shared reference exists
    let freezer = &counter; // ---+
    // counter.count(); // error  |
    let _ = counter.get(); // ok  |
    let _ = freezer; // ----------+

No direct field mutation is observable at the call site; all mutation is confined within the abstraction boundary.

Assuming that internal state changes occur, method receiver types can be characterized as follows:

receiver signals internal mutation requires mutable binding
&self no not required
&mut self yes required
&in self yes not required

A shared reference &self expresses an immutable interface, although internal mutation may still occur through interior-mutable containers such as Cell<T>. A mutable reference &mut self explicitly signals mutation, but it also requires a mutable binding at the call site, enabling rebinding (e.g., caller = new;) or replacement (e.g., *self = new;) of the receiver. By contrast, &in self explicitly signals internal mutation while preserving non-mutability of the binding itself.

In this way, &in self separates field-level mutability from variable-level non-mutability. It makes mutation effects visible in the method signature without exposing mutability at the binding level. Although library authors need to be aware of mut on fields, this does not impose additional requirements on users beyond understanding &in T as a restricted &mut T. This is natural, since &in T is introduced at the beginning by extending a non-mutable binding to a mutable reference (e.g., let x: &mut T), which already exists in current Rust.

Marking as non-mutable: Tracing potential of mutability

As discussed earlier, in the mutable transition state machine, having both the start state and the input be *&in leads to the same non-mutability outcome as having either of them be *&. This raises the following question: if a pre-mutable state persists across successive transitions and remains non-mutable throughout, does it eventually acquire the ability to be shared?

The answer is likely yes. The notion of "pre-mutable" was introduced to describe a state that is not itself mutable, but retains the potential to become mutable. If it is known from the outset that this potential will never be exercised, then there is no longer a reason to require uniqueness as a minimal condition. In that case, classifying the state as transitively immutable rather than unique allows the "mutable XOR aliasing requirement" to be enforced consistently.

While current Rust rarely makes this distinction explicit, the introduction of &in self in order to model interior mutability naturally brings its dual into focus. In particular, when a PE is not currently borrowed, field access is unique, yet the state may remain non-mutable. This situation exposes the notion of unique immutability. Before addressing how such cases might be identified in practice, it is therefore useful to revisit the underlying motivation.

One reason interior immutability is often considered unnecessary in current Rust is that the functional update syntax allows a new struct to be reconstructed while updating a specific field, as long as that field is not currently borrowed. In this sense, field-level mutation is effectively possible without mutating an existing value. For example, the following code demonstrates that even if a field such as non_mut were declared immutable, it could still be updated by moving or copying the remaining fields (using struct field init shorthand for brevity):

    let mut x = X { non_mut, regular };
    x = X { non_mut: updated, ..x };

From the perspective of external observers, specifying interior immutability on a struct may appear meaningless. However, this interpretation overlooks an important distinction. When non_mut is updated, the remaining fields are either moved out of the original struct, thereby ending their lifetimes, or not moved and thus remain with the original struct. In either case, within the interior of each struct, the field non_mut is immutable.

This observation connects to the notions of view types (partial references) and internal references (self-references), as discussed in "The borrow checker within" (2024, baby steps). (Although the present discussion is not intended to contribute to the resolution of the problems addressed there.)

An internal reference is a field that refers to another (nested) field within the same struct. In such a case, the referent field becomes borrowed, and under current Rust rules, it is no longer possible to call a method taking &mut self on that struct. This restriction is inconvenient, motivating the need for a way to share access between a self-reference and an &mut self receiver.

This is where view types come into play. A view type restricts which fields of a struct are accessible through a reference. If a view type provides only non-mutable access to a field that serves as a referent, while the reference to the struct itself is mutable, then the access to that field has unique immutability.

If self-references are restricted to shared references, and if such uniquely immutable field access is permitted to be shareable, then the referent field may be safely shared with a self-reference even under &mut self. Reaching this conclusion requires that the referent field be immutable when viewed both from the struct itself and from the referencing field.

As a byproduct of this discussion, view types naturally suggest the possibility of combining &in self with mut on fields, for example in a form such as &in { non_mut, mut regular } self. Because &in self prevents replacement of the receiver, it avoids the issues associated with functional update syntax, while field-level mutability allows different mutability guarantees to be expressed per field. This should remain compatible with coercion from &mut self, but not from &in self unless fields are annotated with mut.

To determine whether the potential to become mutable is never exercised, it suffices to check not only whether a pre-mutable PE can transition into a mutable PE, but also whether an immutable PE can exhibit mutability via interior mechanisms such as cells.

This naturally leads to considering a marker trait that extends Freeze in core::marker, which Tree Borrows already relies on via the ty_is_freeze flag to track interior mutability. For detecting interior mutability, we introduce a marker NonFreezing, and for detecting the possibility of transitioning to a mutable PE, we introduce a marker MutableIn.

The marker NonFreezing is an extension of the existing Freeze trait -- described as "used to determine whether a type contains any UnsafeCell internally, but not through an indirection" -- such that the presence of interior mutability can also be detected through pointers. This NonFreezing marker is also used when determining MutableIn.

The marker NonFreezing can be defined inductively, using both base cases (1.) and recursive cases (2.–3.), as follows:

A type T implements the marker trait NonFreezing if:

  1. T does not implement Freeze.
  2. T is a pointer type, and its pointee type implements NonFreezing.
  3. T contains a field whose type implements NonFreezing.

An important point here is that the "pointer type" in case 2 includes types such as &T and *const T.

The marker MutableIn can be defined inductively, using both base cases (1.–3.) and recursive cases (4. and 5.), as follows:

A type T implements the marker trait MutableIn if:

  1. T implements NonFreezing.
  2. T is &mut U, *mut U, or mut U for some type U.
  3. T contains a field that is declared with the mut keyword.
  4. T is &in U, *in U, or in U, where U implements MutableIn.
  5. T contains a field of type U that implements MutableIn.

Note that case 3 could be merged into cases 2 and 5 by treating a field of type T declared with the mut keyword as a field of type mut T. And note also that explicit cases for dereferencing and indexing are not necessary, because whenever they apply, at least one of the following cases already holds:

  1. T implements DerefIn, and its associated type Target implements MutableIn.
  2. T implements IndexIn, and its associated type Output implements MutableIn.

Closing remarks

This post is not a proposal or an RFC, and it does not advocate adding &in T or related constructs to Rust. All hypothetical syntax is used purely as a conceptual device.

As a non-native English speaker, I relied on AI-assisted translation for phrasing. Any misunderstandings are my responsibility; all ideas and code are my own.

That said, I welcome discussion on whether this alternative framing of mutability helps make sense of existing Rust semantics and supports further theoretical exploration, or whether it introduces more confusion than clarity.

Feedback on correctness, prior related work, or overlooked discussions would be much appreciated.

2 Likes

This reminds me somewhat of the “unique immutable borrow” that comes up as a mechanism in closure captures, as described in the reference here, and then further here.

Oh… now I see you do already mention that later in the text. Arguably, there may be more value in keeping the already known term “unique immutable borrow” for such an exposition, and discuss a re-name separately.

This transitivity of course ends as soon as the contents of various UnsafeCell-base types are involved (e.g. Mutex, Cell, RefCell).

This section is getting quite confusing. Since when are we talking about something written “in T” or “mut T”, which you call “types” here, and what issues with Deref and Index? Oh, is this supposed to read as a sort of high-level overview like in a research paper? Scrolling ahead, I do find something about a “DerefIn” and “IndexIn” trait. At this point I’m not only confused as a reader, I’m also getting the impression you’re discussing much more here than “just” a third kind of reference type. Yet, I still haven’t found the motivating section.

When is &in T useful enough? When is it impossible to “just use &mut T instead” and live with the fact that a variable is marked “mut” even though for direct access (instead of indirectly through some reference) you could’ve left out that mut marker?


So I guess I’ll have to skip this and going to

though on the way I seem to be spotting in your table the mention of

  • 1 new reference type
  • 1 new raw pointer type
  • 2 new types for “(variable allocation pointer?)”

And I think many readers could appreciate if you considered learning from the RFC structure – even if this isn’t meant to be an actual proposal – in so far as to split off additional things that aren’t core to the idea, fully into a subsequent section akin to the “Future possibilities” in RFCs.


Okay, now I have actually read the “motivation” section, and I can’t say I’m convinced. It explains, as expected, that you can use &in T to avoid marking certain variables as mut. I still don’t really see what additional benefit this gives though.

To me, there seem to be much more useful extensions of “references between mutable and immutable”, in particular partial borrows (only access to certain fields of a struct). The benefit of those is that mutable partial borrows of disjoint sets of fields can co-exist. On the other hand, a &in T reference in your discussion here can not coexist with a &T (AFAICT), so on an abstract type T (e.g. a struct without public fields) it’s basically exactly as restrictive as &mut T. Yes, you don’t need to write mut to create it, but with a real &in T implementation, one would presumably also just be allowed to create a Cell-like type that can do a fn (&in InCell<T>) -> &mut T projection, and any struct implementor that wants to allow users to drop the mut on the variable could wrap their fields in this cell-like type.

A more complex version of “partial borrows” may include e.g. mutable access to some fields, immutable access to other fields, no access at all to yet-other fields. In consideration of such an idea, your &in T becomes like a very particular version of “partial borrows” (immutable access to immediately contained values, like the address and metadata of references, but mutable access to the targets of contained mutable references). But I feel like this is one of the less commonly useful choices that one may actually care about for partial borrows, so it seems unhelpful to single out as the best choice for single kind of “third kind of reference” to introduce.[1]

I guess – viewing it as a sort of partial borrow, and hence coming up with my own motivational arguments of what your proposal might even allow for besides the almost kind-of superficial question of “which variables are marked mut?”, I suppose something like

let mut x = 1;
let s = (&mut x, 2);

let r = &s.1;

some_function(&in s);

println!("{r}");

could probably be allowed then, whereas some_function(&mut s) would kill the immutable borrow of s.1.

That’s actually interesting… currently this is apparently not allowed for the real, existing unique immutable references, as this fails to compile:

let mut x = 1;
let s = (&mut x, 2);

let r = &s.1;

(|| {
    println!("{s:?}");
    *s.0 += 10;
})();

println!("{r}");

Of course, even this minor benefit already runs into limitations again if you argue it for explicit &in T types: The access to &s.1 can only be allowed because it’s direct access to a public field which in turn doesn’t contain any &mut T-like data. But if s was some abstract struct with private fields, and/or 2 wasn’t a i32 but some complex type that might contain – say – a &'static mut T value – or perhaps the abovementioned InCell<T> type, then you can’t actually keep even an immutable reference to this field anymore; so we also rely on “implementation details” of i32.

To solve both these issues, you might actually also want to introduce a complementary partial borrow, let’s call it “pre-immutable” access, and write – for the sake of keeping with the spirit of your existing notational mnemonics – its syntax as &as T (with as for “allow shallow reads” or “another arbitrarily stolen syntax”).

This would be a shared borrow with immutable access to everything but the things that &in T has mutable access to, so by design &as T can co-exist with &in T.

// some_function(_: &in S<'_>) defined elsewhere

struct Field2(i32);
struct S<'a>(&'a mut i32, Field2);

impl<'a> S<'a> {
    fn new(r: &'a i32, x: i32) -> Self { Self(r, Field2(x) }

    fn print_field2(&as self) {
        let value: i32 = self.1.0;
        println!("{value}");
    }
}

// below code no longer needs to rely on any implementation details
// of `S` or `Field2`

let mut x = 1;
let s = S::new(&mut x, 2);

let r = &as s;

some_function(&in s);

print_field(r);

but arguably, there’s not really any good place to stop with introducing new kinds of reference types, right?

It’s already sufficiently tedious that so much API in Rust needs to be duplicated between &T and &mut T versions of the same thing, I don’t think this should ever be made worse.

I do acknowledge that some of my arguments here are addressing this as a feature proposal.


Also I should probably read the rest of your post now, I still haven’t gotten further than somewhere in the middle of “Pre-mutable place expressions”. I’ll end this first reply here though to keep it a bit shorter, and might edit this later or write a second reply if I find anything in the remaining part that I want to comment on.


  1. I’d personally much rather see if partial borrows could somehow – at least conceptually – unify &T and &mut T, so that the information of “which fields are accessible” is just a sort of generic parameter of the reference type, and the existing information of “mutable or not” can be part of such a parameter. ↩︎

1 Like

“a type that could act as a pre-mutable PE when dereferenced”… well… how about Box<T> (in a non-mut variable)? Edit1: I guess you might have meant always yielding a pre-mutable PE though, even if the value itself is in a mutable variable. The extent of “top-left position” is not completely clear.


Edit3: I have given the remaining part a quick read / skim now, and it’s continuing to go a bit too all-over-the-place to really comment on too many points.

I do find the DerefIn-related discussion still relevant though. I don’t think that Target = mut T is really any convincing “solution” to the desire that certain types support (&in Outer) -> &in Inner whilst others support (&in Outer) -> &mut Inner. That is because I don’t think this “automatically dereferenced whenever possible” thing is really that workable.

Also, if you’re still meaning that mut T is a pointer type, it would still need a lifetime, right?

Though your later mention of the idea of using this in field means it sounds much more like a transparent wrapper type (in terms of memory layout), i.e. like InCell I mentioned in my previous reply. Just with more magical syntax and behavior. The syntax really is kind-of terribly, by the way, since &mut T and &(mut T) are both a thing and very different.

This not-a-pointer idea can probably also help make “&in (mut T)” and &mut T more similar – no more double indirection in the former. Still, for maximally “automatic dereferencing / conversion”, you might even want subtyping relationships between them… both ways? It’s just not a particularly nice thing to work with IMHO.

1 Like

To me, this idea seems closely related to that of movability, and the motivation might be clearer if expressed in that form: &in T is a reference which doesn't allow moving of the pointed-to T, but which does guarantee uniqueness and thus can be used to mutate things that the T in question points to. In other words, it supports everything that &mut does, apart from moving/swapping out of the object itself and moving/swapping out of its fields.

As such, it functions as a "mutable reference that preserves object identity" which is something that Rust appears to need (currently Rust only has a concept of object identity while you're using shared references or immutable let bindings, as anything you can form an &mut to can be overwritten). Note that Pin<&mut T> doesn't preserve object identity due to the existence of Pin::set, so it can't be used for this purpose (but Pin has more guarantees than just "you can't swap out of this reference").

1 Like

This is subtle...

e.g.

struct Foo<'a> {
    value: i32,
    mut_value: mut i32,
    mut_ref: &'a mut i32,
    in_ref: &'a in i32,
    shared_ref: &'a i32,
}

let mut a = 5;
let mut i = 30;
let mut j = 40;
let k = 50;
let f = Foo(10, 20, &mut i, &in j, &k);
let r = &in f;

// which of the following are allowed?

// no -- `r` is not mutable
r = &in Foo(...);

// no -- referent of `r` is not mutable
*r = Foo(...);

// no -- neither the referent of `r` nor `value` is mutable
*r.value = 42;

// yes -- `r` is exclusive and `mut_value` is mutable
*r.mut_value = 42;

// no -- neither the referent of `r` nor of `mut_ref` is mutable
*r.mut_ref = &mut a;

// yes -- `r` is exclusive and the referent of `mut_ref` is mutable
**r.mut_ref = 42;

// no -- neither the referent of `r` nor `in_ref` is mutable
*r.in_ref= &in a;

// no -- neither the referent of `r` nor of `in_ref` is mutable
**r.in_ref = 42;

// no -- neither the referent of `r` nor `shared_ref` is mutable
*r.shared_ref = &a;

// no -- neither the referent of `r` nor of `shared_ref` is mutable
**r.shared_ref = 42;

So the notion of &in T, by itself, introduces the notion of an exclusive but not mutable reference; if a field of the referent is a mutable reference, we can safely modify the referent of the referent even tho we cannot modify the field.

And then as a bonus (?), if the language re-introduced mut keyword for allowing interior mutability, it would fit in nicely by allowing such fields to be modified through an exclusive reference even while normal fields remain immutable -- all guaranteed safe at compile time. Cells would still be needed for types that need interior mutability through shared references, which still require runtime checking as always.

Stepping back a bit, tho, this really does feel quite similar to C++ pointers:

  • two flavors, both right associative: * (mutable) or * const (immutable)
  • Foo const* f = ... means "a mutable pointer to an immutable Foo" (can change which Foo the pointer refers to, but cannot change the referent)
  • Foo* const f = ... means "an immutable pointer to a mutable Foo" (can change the referent but not change which Foo the pointer refers to).
  • Just like const is part of the type system in C++, mut is part of the type system in rust.
  • Just like const is "viral" in C++, mut is viral in rust (tho mut is arguably less annoying because it's allowed to omit mut keyword anywhere it's ok for the result to be immutable -- mut is only viral if you want to preserve mutability)
  • Just like how C++ allows const at the front of a pointer type declaration as convenience (e.g. const Foo* f = ... is sugar for Foo const* f = ...), so rust allows mut in front of a variable or field declaration as a convenience (e.g. let mut x: Foo = ... could be interpreted as sugar for let x: mut Foo = ...). As always, neglecting to annotate with mut renders an otherwise mutable result immutable, but let mut x = ... is way nicer than let x: mut _ = ... that would otherwise be required.
  • But if mut is part of the type system, then that means mut T is a different type from T. Just like &mut T is a different type from &T. But the interpretation changes -- now &mut T is different from &T because mut T is a different type than T (nothing special about the &)
  • But if mut T is a different type than T, we technically need different trait impl for the two types, if both are to support the same operations. Or at least, we need a blanket impl<T> Deref for mut T whose target is T, and mut T should decay automatically to T (just like concrete types can decay to dyn trait with or without an as _ cast).
  • Just like const is heritable in C++, mut is directly heritable in rust. Marking a struct instance as const in C++ renders its otherwise-mutable fields immutable. Marking a struct instance as mut in rust renders its otherwise-immutable fields mutable. But in C++ one can mark a field const to block the default mutability, while in rust one cannot mark a field mut to block the default immutability.

The big difference is -- const in C++ does NOT propagate indirectly through pointer types:

struct Foo { int x };
struct Bar { Foo* f };
Foo f = {10};
Bar b = {&f};
const Bar* p = &b;

p->f->x = 42; // perfectly legal!

In contrast, failing to specify mut in rust is transitive. Nothing reachable through &T is mutable, neither fields nor referents of those fields. Even if fields could be marked mut to give interior mutability, referencing such fields through a shared reference still be blocked because the compiler cannot guarantee exclusive access.

This, if I understand correctly, is where rust would need a third reference type even tho C++ does not -- &in T allows interior mutability of an otherwise immutable-but-exclusive reference. Double-dereferencing &in &mut T gives mutable access to all fields of T. If the language allowed mut fields, then double-dereferencing &in &in T would give mutable access only to mutable fields of T. Double-dereferencing a &in &T does not give mutable access to any fields of T, because exclusive access to a shared reference doesn't rule out the existence of other shared references. Especially because &T is Copy!

However, I'm less clear on when the ability to express immutable-but-exclusive access is actually helpful? Do we have any examples of problems it would solve?

NOTE: It could take some work to think of important uses for a new capability like this, because existing rust trains us to not think about this potential tool and to reach for other solutions when a problem arises.

3 Likes

The shape of use case which I am aware of for this type of access is when:

  • there is a container of a set of entities which have some relationships to each other,
  • there are uses for exposing exclusive access to each entity, and
  • it is desirable to avoid allowing unrelated entities to be swapped in.

For a toy example, consider a graph structure:

struct Graph {
    nodes: Vec<Node>,
}

impl Node {
    pub value: SomeOtherType, // suppose this has some `mut` indication on it
    /// Indices of other nodes
    edges: Vec<usize>,
}

Graph benefits from not handing out any &mut Nodes because it keeps the edges within the graph valid, but it would be nice to be able to offer flexible exclusive access to all the values — perhaps by handing out an &in [Node].

It’s possible to write borrowing wrapper types that have a nearly equivalent effect as an explicit API (such that &mut access to the wrapper grants no problematic powers because the wrapper is just references into the data structure and not itself part of the data structure), but those wrappers can’t live in a slice without allocating one.

1 Like

I can't find it right now, but I've read a number of posts lamenting that &mut can both mutate what it points to, and move out of it. This causes unnecessary trouble when things are pinned, to the point that providing std::mem::swap, std::mem::replace and friends was maybe a mistake in hindsight. So perhaps having a new kind of reference that is like &mut, except it doesn't allow moving things out of it, would be a net win.

The trouble is that this proposal competes with the much more useful &out write-only reference that can point to uninitialized memory. And also other proposals like &move for move-only references, &own reference that logically own things but aren't in charge of deallocating them, and others. And I am under impression that there's a resistance on adding more types of references into Rust, to not make it too complex.

(For an example of a model that is perhaps too complex)

See this table on the capabilities of Pony language, which are kind of analogous to & vs &mut in Rust, but I guess it's more similar to how Rust were before INHTPAMA since it models mutable aliases as well)

Maybe a better approach is references having three orthogonal capabilities, read, write and move. Then you have, like, &{read, write, move} T for &mut T, &{read, write} T for &in T, &{write} T for &out T, &{move} T for &move T and &{read} T for &T. Add capability generics, one could write a getter that is generic on & vs &mut and others.

Sorry for the late reply. Thank you all very much for reading and for taking the time to write such thoughtful responses. I may keep people waiting a bit, but I’ll try to reply gradually.

Yes, I agree. In particular, mut T should probably have been grouped and discussed at the very end.

Yes — in this discussion, the focus of &in T is precisely on avoiding mut without using &T. And the claim is that the supporting semantics of pre-mutable place expressions can be constructed as a reframing of current Rust semantics. The goal of this post is to formalize and document a “third kind of mutability”.

I agree that the motivation is weak. Every code example shown in the post can be made to compile in today’s Rust simply by adding mut and using &mut T. In fact, behind the scenes I validated all examples that way to ensure correctness. In other words, &in T is only usable in cases that are already expressible using &mut T.

That said, I still believe that knowing Rust can theoretically admit intermediate forms of mutability beyond cell-like types can be helpful for future discussions. Unfortunately, I wasn’t able to express that convincingly in the motivation section.

That is fair — &in T is a form of partial borrow. In this post, I argue that making it partial allows the target of a unique borrow to extend to non-mut variables.

Also, sorry for the somewhat scattered presentation, but in the last section (“Marking as non-mutable”), I briefly touch on the idea that &in self prevents replacement of the receiver inside method bodies (i.e. *self = new;). Once you introduce partial borrows, replacement is generally impossible anyway — but that restriction itself may be considered part of the semantics of &in T.

Regarding your footnote [1], I actually share a similar perspective to yours:

I’d personally much rather see if partial borrows could somehow – at least conceptually – unify &T and &mut T, so that the information of “which fields are accessible” is just a sort of generic parameter of the reference type, and the existing information of “mutable or not” can be part of such a parameter.

From the perspective of &in T, IMO, a mutable borrow of a struct differs from a partial borrow with mutable access to all of its fields mainly in whether replacement is allowed; fields accessed by &T parameter are movable; and uniqueness of field-level access can coexist with the ability to take &T.

1 Like

I don’t think this would be allowed, because &in T is a unique reference, and therefore it cannot be created from a struct s that is already partially borrowed, as in let r = &s.1;.

IMO, if instead a partial borrow like &in { 0, &1 } s were created in argument position and passed directly, then depending on the function signature, such a call might be possible.

My understanding of &as T is that it would correspond to a partial borrow that unifies &T and &mut T, as you mentioned in your footnote, and that it would represent the &T-side of field access. If so, I think coexistence could make sense in the form of &in T for the struct as a whole and &as for its fields.

In this post, however, the only reference type paired with &in T -- in terms of variable mutability -- is &mut T. &in T is purely a restricted form of &mut T: aside from being creatable from a non-mut variable, it cannot do anything that &mut T cannot. It is, admittedly, a very constrained concept.

Exactly.


Yes -- I mean always yielding a pre-mutable PE. This is, for example, required so that dereferencing a mutable variable of type &in T results in a non-mutable place expression.

That said, you are right that a non-mut Box<T> does fit into the "top-left position". I’ll need to revisit this.

I really appreciate your comments on DerefIn and mut T. Reading them, I realized that I should have structured this differently: the Target should have been left as a placeholder -- say, Trick<T> -- and nothing more. The idea would then be limited to assuming that a function returning &mut T can be coerced to return &in Trick<T>, and that applying a single dereference operator to that yields a mutable place expression of type T.

Had I done that, the discussion could have focused on questions like whether (in Outer) -> &mut Inner should be allowed at all, and whether the need for Trick is fatal to the concept of &in T -- which I believe are the weak points of this post. The discussion of mut T itself could then have been cleanly separated into a “Future possibilities” section. I clearly need to learn more carefully from the RFC structure.

My expectation is that the lifetime of mut T would be asymmetric with that of &mut T, and that in DerefIn it would be tightly coupled to the lifetime of &in in the returned type. However, at this point I think it was premature to treat mut T as a pointer type at all, and that discussions assuming it is a pointer should have been deferred.

I’ve just noticed I was conflating different meanings of “partial borrow.”

I think “partial borrows” should first be split clearly into two categories: (1) what we can already do in current Rust: directly borrowing a field, like &mut s.field; and (2) what we might do in a future Rust: borrowing fields through a struct view type, like &mut { field } S.

And (2) might further split into two sub-cases: (a) directly borrowing multiple fields, and (b) selectively borrowing indirectly from the struct (via the view).

Interesting. Whether they’re distinct or the same... Along with your comment, I’d like to take some time to think this through carefully.


Seeing how my post would be read, as you showed me, has helped me clarify what I should write.

1 Like

Another design axis within this is the question of whether you consider &mut { field } S a “ordinary” reference to a new kind of type { field } S (which you might e.g. consider to be a kind of type that is never-owned[1], and kind-of has discontinuous memory [i.e. the non-included fields are simply considered “not part of the type”[2]]), or if you consider them a new kind of reference type & { field } _ which has S as its generic argument (a complication of this model would be that the declaration of “which fields?” to borrow don’t really work too well generically[3])

I most often think of it in the latter way, which is why your &in T seems like a kind of “partial-borrows” reference type to me in my mind. :wink:


  1. a little bit like unsized types, though they can actually be owned behind indirection

    A similarity for example would be that mem::swap should not be callable on this kind of type ↩︎

  2. this is less troublesome that it may first seem due to the fact that we allow this type never to be directly owned, anyway ↩︎

  3.    on the other hand, for implementation abstraction reasons, being generic over multiple types while having the same “kind” of partiality in your borrow may be a desired property anyway(?)
       So ad-hoc partial-borrow for a field field of ConcreteStruct, could feature reference types are like a generic type type R<T> = & { field ] T with a very restrictive bound where T == ConcreteStruct;
       but for abstraction you could have e.g. a way for a trait to contain abstract “field” declarations, and for a trait Foo { field bar; } you can then have a type & { bar } T more generically with a T: Foo bound :thinking:
       Also though… as with struct definitions with trait bounds – you generally avoid them and only put them on the methods – for these partial-borrow references one can also imagine only requiring that T: Foo whenever actually using the reference, not when merely mentioning the type. ↩︎

I see. I had been thinking about uniqueness and exclusiveness primarily as requirements for mutability, but your comment made me realize that we can also talk about uniqueness or exclusiveness -- in terms of movability -- while explicitly excluding mutability.

[Edit]: Removed the parts that failed to consider dlight’s comment.

Generally, what "move out of it" means might also depend on the type in question.

The example1 through example4 functions below all have same result: they print and clear a vector of integers.

  1. clearly moving the whole Vec<i32> from behind the reference
  2. passing &mut Vec<i32> to another Vec method, which will then move the elements but no clue if it moves the vector itself
  3. calling a method on the reference, which does not document any moves either
  4. consuming elements one by one; what happens to Vec is defined by std too
fn example1(a: &mut Vec<i32>) {
    println!("#1. Moving out. a = {:?}", std::mem::take(a));
}

fn example2(a: &mut Vec<i32>) {
    let mut b = vec![0];  // phantom 0 so a's allocation cannot be reused
    b.append(a);
    let _ = b.remove(0);  // removing phantom element
    
    println!("#2. Pulling elements. b = {b:?}");
}

fn example3(a: &mut Vec<i32>) {
    println!("#3. Clearing. a = {a:?}");
    a.clear();
}

fn example4(a: &mut Vec<i32>) {
    print!("#4. Draining. a = [");
    let mut first = true;
    for v in a.drain(..) {
        // Substitute some useful work on `v`
        if first {
            print!("{v}");
            first = false;
        } else {
            print!(", {v}");
        }
    }
    println!("]");
}


fn main() {
    let mut a = vec![1, 2, 3];
    example1(&mut a);
    assert!(a.is_empty());
    
    let mut a = vec![1, 2, 3];
    example2(&mut a);
    assert!(a.is_empty());
    
    let mut a = vec![1, 2, 3];
    example3(&mut a);
    assert!(a.is_empty());
    
    let mut a = vec![1, 2, 3];
    example4(&mut a);
    assert!(a.is_empty());
}

So developers would have to define what operations can be done with each kind of reference (can one push elements to &{read, write} Vec<T>? and if it reallocates?) and that sounds really like Pin projections.

This duality between mutability and moving/swapping may run throughout the discussion of &in T. With that in mind, I examine interior mutability from the perspective of moving/swapping, using this contrast as a guide.

Let’s start with the case where there is no interior mutability. Since &in T is introduced as a restricted form of &mut T, different aspects become visible depending on whether we focus on the similarities with &mut T or the differences:

  • [mutability] &in T has the potential to become mutable, just like &mut.

  • [moving/swapping] &in T does not allow swapping, unlike &mut.

Because the mutability of &in T lies somewhere between “mutable” and “immutable”, there should also be similarities and differences not only with &mut T but also with &T. For mutability, the key point is the transitivity of immutability that &T has.

  • [mutability] The immutability of &in T is not transitive, unlike &T. For example, given a one-field struct containing an &mut T, you cannot do *x = ...;, but you can do *x.field = ...;.

In contrast, something resembling the transitivity of &T can be found in the relationship between moving before creating an &in T and inherited mutability. When an initialized variable is moved out of, it becomes deinitialized, and reassigning to a deinitialized variable reinitializes it. This is where conditions related to interior mutability come into play.

  • [moving/swapping] If there is no mut on fields, an immutable variable that is not currently borrowed -- and its fields -- are not reinitialized. This includes the case where the field type is Cell<T>.

  • If there is mut on fields, a field may be reinitialized. This is the same as for a mutable variable that is not currently borrowed.

In other words, mut on fields (e.g. mut field: T) and cells (e.g. field: Cell<T>) are similar in that they both provide interior mutability, but they differ from the perspective of moving -- specifically, field reinitialization. However, after an &in T is created, &in T does not allow moving out of T or its fields regardless of interior mutability, so this difference becomes less visible.

This contrast between mut on fields and cells suggests that they serve different purposes, and may be related to comments such as using field: InCell<T> rather than field: mut T, as illustrated by the following:

It may also be connected to the comparison between “a reference to a new kind of type” and “a new kind of reference to a type” in the following passage (with three footnotes):

That's a good point. This made me realize the original post sat halfway between C++ and Rust, which prompted me to step back and rethink the idea. In doing so, my thinking -- particularly on interior mutability -- has shifted significantly.

The feedback here clarified that mut T has issues, and that a Cell-like type such as InCell<T> is a better approach.

Marking fields with mut is problematic for &in T, since it compromises the principle of inherited mutability.[1] One goal of &in T is to show that a third kind of mutability already exists in today's Rust, so it seems preferable to avoid changes that would require more fundamental modifications to the language.

With InCell<T>, I intend to remove both mut T and field-level mut from the idea. The combination of &in T and InCell<T> should fit Rust's design more naturally. (For example, the inherited immutability of &in T might be interpreted as a weakened form of the transitive immutability of &T. Following steffahn's point that the transitivity of &T ends at UnsafeCell-based types, the inherited immutability of &in T could be understood as ending not only at those cell types, but also at &mut T, *mut T, InCell<T>, and types based on these.)

Currently, I see &in T being used primarily with fields of types such as &mut T and InCell<T>. In this model, the exclusiveness of &in T is not about making the struct mutable, but about satisfying the conditions required to exercise the mutability carried by types like &mut T or InCell<T>.

As a somewhat contrived example, consider an iterator that yields mutable reborrows from an immutable vector of &mut T. This is not possible in today's Rust, but could become possible with &in T and InCell<T>. Using GATs, one could apply the LendingIterator pattern from the Rust Blog. The idea would be to borrow the immutable vector in an immutable-but-exclusive way to produce mutable reborrows (e.g., in the next method), while still counting iterations using &mut T or interior mutability. (Code for a mutable vector in today's Rust: playground)

Taking the view that &in T is similar to C++ pointers -- particularly a non-const pointer to a const pointer to a non-const X -- usage patterns from C++ might offer guidance.

That said, a great deal of C++ code has already been ported to Rust, and if discussions about a Rust counterpart to such pointer patterns have not emerged, compelling examples may be hard to find.

I was skeptical myself when &in T first occurred to me.

By now, I think that if a third kind of mutability does exist in Rust, it may be worth formalizing and documenting it, if that hasn't been done already. Doing so could save someone effort in the future, and having it clearly laid out might make it easier to think of possible uses.

For now, my main goal is simply to rewrite and present the idea more clearly.


  1. In C++, the mutable specifier allows a const object to have mutable members. Introducing mut fields in Rust would similarly allow mutable access through an immutable variable, undermining inherited mutability.
    By contrast, with cell types, a field accessed through an immutable variable remains immutable; interior mutability is provided by the type itself, preserving inherited mutability. ↩︎