Why is field behind a shared reference not a mutable place expressions?

struct A<'a>(&'a mut i32);
fn main(){
   let mut i = 0;
   let mut a = A(& mut i);
   let srf = &a;
   let f = & mut *(*srf).0; // #1 error

  // let mrf = & mut a;
  // let mf = & mut *(*mrf).0; // #2 Ok
}

So, I want to find the interpretation why #1 is an error. I found the definition of mutable place expressions in Expressions - The Rust Reference, however, the relevant rule cannot interpret such two cases. The reference says:

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.

Then, the reference lists all mutable place expressions:

The following expressions can be mutable place expression contexts:

  • [...]

  • Dereference of a variable, or field of a variable, with type &mut T. Note: This is an exception to the requirement of the next rule.

This is the only relevant rule to my cases because the field has type & mut i32, however, there are two issues here:

  1. whether (*srf) can be called "variable"?
  2. This rule can just imply #1 should be ok if the answer to the first issue is yes

If the answer to the first issue is no, then we will also have no relevant rule to interpret why #2 is OK, anyway.

If I misunderstand the rule, where is the rule in reference that can interpret my two cases?

If my analysis is correct, is it more plausible if the referenced rule is changed to the following?

  • Dereference of a variable, or field of a mutable place expression, with type &mut T. Note: This is an exception to the requirement of the next rule.

whether (*srf) can be called "variable"?

srf is a variable. *srf is a dereference of a variable.

This rule can just imply #1 should be ok if the answer to the first issue is yes

*srf is a dereference of a variable, but the important second half of the sentence is “with type &mut T.” The variable srf doesn't have type &mut i32, it has type &A, which is not permitted by any of the rules.

1 Like

However, for the field with type & mut T, the rule does not impose any requirement on the variable, right?

Dereference of a variable, or field of a variable, with type &mut T

This rule just imposes that the field should be of type & mut T, and it says nothing of the variable for the field.

I don't personally attach any formal meaning to "field of a variable" — I'm just interpreting the text you quoted — but I'd say that (*srf).0 isn't a field of a variable, it's a field of an dereference of a variable. And the dereference is immutable.

(In general, you can expect that you can never mutate something that's behind an & reference[1]; because that would definitely be unsound. If the text disagrees, the text has a bug; if the compiler disagrees, the compiler has a bug.)


  1. without there also being an UnsafeCell involved ↩︎

5 Likes

All right, that means, the rule I cited does not apply to these cases, and the reference lacks a corresponding rule to interpret these cases, right?

Yes, but I wouldn't say “lacks a rule”, in the sense of a perhaps accidental omission, but rather “if not listed here, then it's not mutable”.

1 Like

The reference could probably be improved (with examples if nothing else) but I think the rules cover this case.

   &mut *(*srf).0
//      ^^^^^^^^^ - Place with &mut being taken - is a deref of a type
// &mut *(*srf).0   which implements DerefMut:
//       ^^^^^^^^ - value being derefed which must be in a mutable place
// &mut *(*srf).0   expression.  It's a field, with subexpression:
//        ^^^^    - a deref of a variable with type &A

The rule you quoted in the OP doesn't apply because (*srf).0 is not a variable nor the field of a variable. It's another place expression.

A place expression is an expression that represents a memory location. These expressions are paths which refer to local variables, static variables, dereferences (*expr), array indexing expressions (expr[expr]), field references (expr.f) and parenthesized place expressions.


But you don't really need the reference[1] to interpret why this is an error if you understand the higher-level guarantees of &_ and/or &mut _.

Guarantees of &mut: Exclusivity. You can safely observe the value held by i through the &A, so you can't take an exclusive reference to that place. This can't be mitigated by something like a reborrow analysis which temporarily "disables" the &A, because shared reference implement Copy -- there could be arbitrary more observers for the same period.

Guarantees of &:

If you have a reference &T, then normally in Rust the compiler performs optimizations based on the knowledge that &T points to immutable data. Mutating that data, for example through an alias or by transmuting an &T into an &mut T, is considered undefined behavior. UnsafeCell<T> opts-out of the immutability guarantee for &T: a shared reference &UnsafeCell<T> may point to data that is being mutated. This is called “interior mutability”.
[...]
Note that only the immutability guarantee for shared references is affected by UnsafeCell. The uniqueness guarantee for mutable references is unaffected. There is no legal way to obtain aliasing &mut, not even with UnsafeCell<T>.

There's no UnsafeCell barrier here to enable mutability to the place of i safely reachable through the &A.

(Optimizations aside, exceptions either of these reules would change the semantics of Rust, since humans can logically rely on the immutability and exclusivity guarantees as well.)


  1. or some future actual-spec ↩︎

1 Like

However, the #2 case also does not have a corresponding rule, it does not mean the case in #2 is immutable.

I think the issue is that

a variable, or field of a variable, with type &mut T

is supposed to be read as

a variable with type &mut T, or field of {a variable with type &mut T}

It's correct but ambiguous English.

However, this only applies to &mut *srf.0 (this is also not allowed). For &mut *(*srf).0 it doesn't matter because the (*srf).0 part is a field of an expression (the "Fields" rule), not a field of a variable.

No matter what the reference says, you can't get mutable access to data behind & (except for UnsafeCell). This is on purpose and working as intended.


I wrote that up, and thought it was fully correct. But then I looked more.

I think the only reason the "or field of a variable" part is there is for splitting borrows, like when &mut x.0 and &mut x.1 coexist. When reaching the second one, x is already borrowed, but the 1 field of x is not. The "field of a variable" rule catches cases where the subexpression of a field access is a variable that's already mutably borrowed.

However, you can do &mut (*x).0 and &mut (*x).1, which these rules seem to not allow, since x is already borrowed.

What this means is that as far as the reference is concerned (*x).0 is a field of a variable. So then (*srf).0 fails because srf is &A which is not &mut T. Actually, this simply a dereference of a variable of type &mut T. So this is left to the borrow checker to ensure the fields are not the same. I think the only reason they put "which are not currently borrowed" in the first rule is just to add understanding. That's actually enforced by the borrow checker, not by the place expression rules, which makes sense.

So in conclusion, the "field of a variable" rule isn't actually needed.

Lemme clean up that last reply:

First, (*srf).0 is a field access, followed by a failure of *srf to be a mutable place expression. In the other case, *mrf is simply a dereference of &mut T. This is the difference between your two cases.


The reference could use two changes:

Mutable variables which are not currently borrowed.

becomes

Mutable variables.

The "not currently borrowed" part is not decided by place expression rules, but by the borrow checker. Otherwise, (*x).0 and (*x).1 couldn't be simultaneously valid.

And

Dereference of a variable, or field of a variable, with type &mut T.

becomes

Dereference of a variable with type &mut T.

The "field of a variable" part is redundant with the "Fields" rule, which with the first removal, is able to handle all field accesses.

1 Like

So, IIUC, you think

  • Dereference of a variable, or field of a variable, with type &mut T. Note: This is an exception to the requirement of the next rule.

the rule does not apply to my cases because (*srf) is not a variable even if it has type A<'_>.

Instead, you think the relevant rule should be

Dereferences of a type that implements DerefMut

since (*srf).0 has type & mut i32, which implements DerefMut by the item

#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> DerefMut for &mut T {
    fn deref_mut(&mut self) -> &mut T {
        *self
    }
}

then the dereference to (*srf).0 need to obey the requirement

this then requires that the value being dereferenced is evaluated in a mutable place expression context.

That is (*srf).0 should be a place expression context, which is regulated by

  • Fields: this evaluates the subexpression in a mutable place expression context.

which in turns requires (*srf) to be a mutable place expression context, recursively, the following rule apply to:

Dereference of a variable, or field of a variable, with type &mut T

srf does not satisfy the requirement, hence it is not a mutable place expression. Is my understanding right?

I think you are attaching too much meaning to the literal interpretation. The documentation is trying to tell you that you can mutate stuff that's behind &mut or that you own and declared as mutable. Anything else is immutable.

4 Likes

No I’m fairly certain it’s supposed to mean:

a variable with type &mut T, or a field with type &mut T of a variable

In the second case, the type &mut T is the type of the field.


Arguably, this listing is still incomplete. A field of a field of a variable (again, the innermost one being &mut T) is fine, too. And for example deref of a Box is allowed as well. And arbitrarily deep combinations of these. E.g.

let mut n = 42;
let x: Box<(i32, (i32, Box<(i32, &mut i32)>))> = Box::new((0, (1, Box::new((2, &mut n)))));
let n_ref: &mut i32 = &mut *(*(*x).1.1).1;

The important part is that even if the &mut i32 itself is not mutable, it is still uniquely accessible. (For closure captures, there even is the niche concept of “unique immutable reference” that the reference defines elsewhere for this kind of access.)

Deref of Box is not even the only special case. There’s more, e.g. another one is indexing an array is also a built-in operations that supports such unique immutable access… to complicate the previous example further, e.g.:

let mut n = 42;
let x: Box<(i32, (i32, Box<(i32, [&mut i32; 1])>))> = Box::new((0, (1, Box::new((2, [&mut n])))));
let n_ref: &mut i32 = &mut *(*(*x).1.1).1[0];

(note how x itself is still not marked mutable)

Perhaps there are more things I’ve missed, or maybe Box and arrays are the only other things that allow built-in and “owned” access to a sub-place in a way that allows for a form of “unique immutable borrow” like this.

On the other hand, one other aspect that the reference doesn’t seem to define at all is the details of matching a place expression against a pattern. Suddenly, the mutability requirements also strongly depend on the pattern, and it’s no longer a clear immutable vs. mutable distinction, but instead a more fine-grained question of what sub-place (for lack of a better term) is borrowed in which manner; which then would need to result in corresponding requirements to the place expression and/or the pattern…? (Haven’t thought this through.)

I.e. the reference doesn’t seem to explain the intricacies of how code compiles:

let mut n = 0;
let y: Option<&mut i32> = Some(&mut n);
// important: `y` is not marked `mut`

let r: &mut i32 = match y {
    Some(&mut ref mut r) => r,
    None => panic!(),
};

though this is, in some sense, similar to borrowing the dereference of a &mut i32-typed field of the Option, but expressed via pattern-matching.


Also, the wording around “currently borrowed” for

  • Mutable variables which are not currently borrowed.

Is weird, because none of the other cases cares much about active borrows, and in fact borrow-checking seems like a separate concern from determining what’s a “mutable place expression” in the first place.

6 Likes

Yeah, that is what I was trying to convey.

Under the current phrasing that was the one that seemed to apply, but probably it's a bit more nuanced than phrased. I imagine the actual rules are mostly just recursive until you hit some base case.

This is probably it? Walking through it without being familiar or consulting anything else...[1]

  • Can no longer recurse? (last_projection() is None)
    • Is this a mut variable binding? (Mutability::Mut)
      • Mutable :+1:
    • It is not but could be uninitialized or something (Mutability::Not)
      • Depends on some LocalMutation* from the larger context
  • Can recurse, and we're a deref (ProjectionElem::Deref)
    • It's a &: Not mutable :-1:
    • It's a &mut: Recurse modulo/with some LocalMutation* logic :arrow_heading_up:
    • It's a *const: Not mutable :-1:
    • It's a *mut: Mutable :+1:
    • It's a Box: Unconditionally recurse :arrow_heading_up:
  • Can recurse, and we're something else (field, index, subslice, ...)
    • Is closure capture (upvar): Check captured thing with some subtlties :arrow_heading_up:
    • Check last projection :arrow_heading_up:

So yeah that looks mostly recursive to me, with some short-circuits (*const and &: Not mutatable; *mut: mutable). There's some subtleties around closure captures and larger-context reasoning involving LocalMutationIsAllowed. Searches... That looks to be aimed at allowing let x; x = 2; type initialization from the comments at least. When I search for that symbol I see other things that look like drops, moved places, uniq closure captures...


Long story short, that bullet list is not the end-all-be-all of mutable place expressions. Let me re-read it and your OP with fresh eyes...

  • Is *(*srf).0 a mutable place? That's a &mut deref, so recurse
  • Is (*srf).0 a mutable place? That's a field access, so recurse
  • Is *srf a mutable place? That's a & deref, so no

I skipped over some LocalMutationIsAllowed considerations that don't matter in this case.

  • Dereference of a variable, or field of a variable, with type &mut T. Note: This is an exception to the requirement of the next rule.

I think this is primarily getting at the fact that you can mutate through a x: &mut T even if x is not a mut binding (and similarly for (nested) fields of x that are &mut U). That's part of the LocalMutationIsAllowed logic above: If recursing on a &mut deref, ignoring even more details around closure captures, it's still okay to be a mutable place if we end up at a local without a mut binding (LocalMutationIsAllowed::Yes).

And again there are other cases not covered by the current text, like let x; x = 1; and a bunch of closure capture nuance I didn't attempt to follow.


Yeah I agree. Looking at the file history, the information seems to have been spread out across the different expression types originally. Here's the massive rewrite that introduced that phrase as it exists today. But I don't really see any explanation there.[2]

Ponders...

You know, IMO it feels like initialization state[3] shouldn't really "matter" to being a mutable place expression or not either, but is one of the LocalMutationIsAllowed use-cases. Maybe this is what Niko was getting at with the suggestion to split is_assignable out of is_mutable. A kind of fall-out from having mut bindings be part of the language and not a lint?

So then also, maybe[4] some "we need to error if this local is borrowed in any case" logic was also part of this area of the compiler pre-NLL,[5] when references were much more tightly coupled to syntactical scopes. The older reference does talk about putting lvalues into a "borrowed state" too.

But that's where I stopped digging. :slight_smile:


  1. treat this like a coffee shop conversation and not gospel ↩︎

  2. You can see that it used to say "(nested) field". ↩︎

  3. which relies on the larger flow analysis ↩︎

  4. pure speculation ↩︎

  5. the above rewrite was early 2017 ↩︎

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.