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
&Tand&mut T, modeled as&in T. - The core idea is to treat
&in Tas 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-
mutrefers to being declared without themutkeyword.
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;
"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(writablex, writablea)&mut *x(a reborrow; writablea, but no access tox)- a hypothetical
&in x(readablex, writablea)
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 Tor*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, ormut 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:
MutablePre-mutable variables which are not currently borrowed.MutablePre-mutablestaticitems.Temporary values. [deleted]- Fields: this evaluates the subexpression in a
mutablepre-mutable place expression context.- Dereferences of a
*mut T*in Tandin Tpointer.- 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
DerefMutDerefIn: this then requires that the value being dereferenced is evaluated in amutablepre-mutable place expression context.- Array indexing of a type that implements
IndexMutIndexIn: this then evaluates the value being indexed, but not the index, inmutablepre-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:
- In
deref_inmethod, the field of type&mut Tis coerced at function results to the returned type&in Self::Target. - The caller receives an
&in Self::Target. - Dereferencing
&inyields a pre-mutable PE of typeSelf::Target. - Somehow, this must become a mutable PE of type
T. - 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
&inof 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. Ifmutwere 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 domypoint.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.mutis no longer valid in~mut T. Use inherited mutability.- …
- Struct fields may no longer be
mut. Use inherited mutability,@mut T,core::mutorcore::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:
mutmodule andMuttype 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:
@muthas been removed. Usestd::cell::{Cell, RefCell}instead.- [...]
std::cell::Cellandstd::cell::RefCellcan 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
Unsafetype was introduced for interior mutability. It is now considered undefined to transmute from&Tto&mut Twithout using theUnsafetype.
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),
*
~Thas been removed from the language. This type is superseded by theBoxtype.
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
Timplements the marker traitNonFreezingif:
Tdoes not implementFreeze.Tis a pointer type, and its pointee type implementsNonFreezing.Tcontains a field whose type implementsNonFreezing.
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
Timplements the marker traitMutableInif:
TimplementsNonFreezing.Tis&mut U,*mut U, ormut Ufor some typeU.Tcontains a field that is declared with themutkeyword.Tis&in U,*in U, orin U, whereUimplementsMutableIn.Tcontains a field of typeUthat implementsMutableIn.
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:
TimplementsDerefIn, and its associated typeTargetimplementsMutableIn.TimplementsIndexIn, and its associated typeOutputimplementsMutableIn.
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.