Skip to content

First version of the Extending Safe Mutability proposal. #78

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
315 changes: 315 additions & 0 deletions active/0000-extending-safe-mutability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
- Start Date: 2014-05-16
- RFC PR #: (leave this empty)
- Rust Issue #: (leave this empty)


# Summary

Introducing an orthogonal handling of mutability and aliasing, which will hopefully make code clearer to read and increase the scope of what can be written without dropping to unsafe code.


# Motivation

The current system of handling mutability is confusing and requires unsafe code and compiler hacks. It is confusing because types like `Cell` can be mutated through `&T` references as they are internally mutable (an invisible property) and requires compiler hacks in the way closures are modelled.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's misleading to blame the closure issues on the mutability system. The closure issues are caused by Rust having a poorly thought out implementation of closures carried over from ancient versions of the language. It is not necessary for there to be any special cases.

It's a stretch to call the simple existing system confusing. The current mut is clearly defined as allowing assignment to the variable, and granting inherited mutability. The inherited mutability system still exists after these changes and is distinct from internal mutability, but is called mutable exclusive instead of mut.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I referenced your proposal for the closures immediately afterward so hopefully it is made clearer that we can get "proper" closures without this proposal.

Regarding the confusion: C++ is clearly defined as well, but it so happens that many users are confused by it anyway. We can either blame the users for not investing themselves enough or blame the language for not being easy enough to understand. I would suggest we try to meet half-way.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the problem with the existing system if you're allowing internal mutability anyway.


*Note: a concurrent RFC for [unboxed closures](https://github.com/rust-lang/rfcs/pull/77/) might solve the closure issue; it does not address the confusion issue though, nor the necessity to drop to unsafe code in order to mutate aliased objects.*

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's misleading to say this might solve the issues. Eliminating the current crippled closures by defining them in terms of other existing features would eliminate special cases. You're stating the current system is confusing again, but I don't buy it. Feigning misunderstanding of inherited mutability while still leaving the entire thing intact with new complexity on top is not a convincing argument.


The confusion is not helped by the compiler diagnostics:

```rust
use std::cell::Cell;

fn main() {
let c = Cell::new(4); // note: prior assignment occurs here
c.set(3);
c = Cell::new(5); // error: re-assignment of immutable variable `c`
}
```

So, it's okay to mutate `c` via `set`, but not to assign to `c` because it is *immutable*...

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right, a mutable variable means you can assign to it. This is how mutable and immutable variables work in most languages, and yet no one claims val vs. var in Scala in confusing. The feigned confusion about this weakens the argument against the current system.



# Drawbacks

By refining the type system, this RFC introduces 4 reference types where there were only 2 previously. It also requires introducing a new Trait.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a lot of extra complexity, without any clear gains.


The benefits may not be worth the extra complexity overhead. It is hard to perform a costs/benefits analysis when everyone has a slightly different and vague idea of what the "system" being discussed is though, therefore here comes a "reasonable" system, in all its ugly details.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current mutability system is certainly reasonable. I think it's a lot more reasonable than one where there's a useless &exclusive / &uniq type that's not intended to be used. That's certainly a lot more confusing to me.

Beyond there being people who understand Rust and those who don't, I'm missing where there's any difference / vagueness in the understanding of the system. It's pretty clearly defined and well understood, unlike the borrow checker rules which are far more subtle.



# Detailed design

*Disclaimer: this proposal introduces several names, they are purposely overly verbose as they are place-holders to facilitate the discussion. If this proposal is ever accepted in whole or in parts, then we can have a grand bike-shed.*


## 1. Defining safe mutation

Rust is nominally typed, and therefore it seems dangerous to allow a `&A` to point to an instance of anything else than a `A`. Not only methods could be called that would be unexpected, but it would possibly confuse the Type-Based Alias Analysis in LLVM.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rust doesn't really make any guarantees about this right now.


> A mutation is safe if after its execution any remaining usable reference `ri` of static type `&Ai` still points to an instance of type `Ai`.

*Note: this definition is valid only when no concurrent thread executes, aliases between threads have to properly synchronize their writes and reads on top of respecting this definition.*


## 2. A focus on aliasing

The simplest way to guarantee that all the remaining usable references in the system can be safely used after the mutation is to guarantee that none of them point to changed content.

This proposal introduces the `exclusive` and `mutable` keyword:

- a reference qualified by `exclusive` is guaranteed to have no alias
- a reference qualified by `mutable` allows mutation of the pointee

*Note: why `mutable` and not `mut`? Just so we can easily identify snippets written in the current system from those written in the proposed system.*

It is trivially provable that mutation through `&exclusive T` or `&exclusive mutable T` is safe, although we disallow the former. The borrow-check current implementation is sufficient to enforce the `exclusive` rule, and therefore we can easily convert today's code:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The non-aliasing of &mut T is still theoretical:

fn main() {
    let mut x = 5;
    let y = &mut x;
    *y = 10;
    let z = x;
    println!("{} {}", z, *y);
}

The aliasing rules are just a way to get safe mutability. This specific case is going to change to support data parallelism, but it's not the case that we have these rules today.


```rust
enum Variant { Integer(int), String(~str) }

fn replace(v: &mut Variant, new: &Variant) {
*v = *new
}
```

to tomorrow's code:

```rust
enum Variant { Integer(int), String(~str) }

fn double(v: &exclusive mutable Variant, new: &Variant) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even as &uniq mut / &mut uniq this is significantly more verbose.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather note enter a bike-shedding argument now. If this proposal is rejected on semantics ground, then all discussions about syntax will have been a waste of effort anyway. And if the proposal is accepted, we can always promote a "fused" keyword if judged necessary.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proposal is mostly just moving things around via renames, so I think questioning the changes is fair. Why not just have 3 reference types rather than 2 keywords you can combine together in 4 ways?

*v = *new
}
```

Essentially, this is simple bike-shedding for now. We add the `exclusive` and `mutable` keywords and declare that mutation may only be achieved through a `&exclusive mutable` reference. `exclusive` guarantees the safety and `mutable` guarantees the programmer intent.

Still, now aliasing and mutability are expressed through two distinct keywords and thus we can move on to:

- having non-aliasing without mutability (which we will just note as an amusing side-effect, for now)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needless complication is more than an amusing side-effect, it demonstrates that the system doesn't map well to what is required.

- having *safe* mutability in the presence of aliasing

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This already exists via types like Cell and RefCell so it's not a new feature.


Let's go.


## 3. Introducing the `SafelyMutable` Trait

Types such as `int` or `Cell` can be safely mutated even when multiple aliases exist, yet the type system currently forbids it. This leads us to:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This functionality already exists via Cell and it's called Pod. There is no use case for Cell if you no longer need it to mutate Pod types through non-exclusive references. The Unsafe<T> marker informs the compiler about internal mutability.


- compile-time errors when attempting to borrow them into multiple `&mut` simultaneously

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's because &T is currently how this is done.

- confusion when `let c = Cell::new(4);` can be mutated even though it is not apparently mutable (and attempts to assign to it fail because it is *immutable*)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A variable or reference being immutable means you can't assign to it. Anyone lacking an understanding of inherited mutability will still be confused after these changes, because the distinction is still there.


The goal of the `SafelyMutable` Trait is thus to mark such types that can be safely mutated in the presence of aliases.

Informally, a type is `SafelyMutable` if its mutation is safe. We propose that a type be declared `SafelyMutable` using the regular Trait syntax:

```rust
#![deriving(SafelyMutable)]
struct Cell<T> { ... }
```

or

```rust
struct Cell<T> { ... }

impl<T> SafelyMutable for Cell<T> {}
```

The compiler will enforce the necessary rules for a `SafelyMutable` type to really be safe. Let us start simple:

- a built-in integral or floating point type is `SafelyMutable`
- a fixed-length array of `SafelyMutable` types, is itself `SafelyMutable`
- a `struct` may be declared `SafelyMutable` if all its members are `SafelyMutable`
- a `struct` may be declared `SafelyMutable` if it has an unsafe interior
- an `enum` may be declared `SafelyMutable` if it is a C-like `enum` (no payload for any enumerator)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that Cell will just be Pod enum -> SafelyMutable enum as the other use cases are gone. I feel like there's a lot of non-orthogonality between SafelyMutable and Cell as they accomplish nearly but not quite the same thing.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, the very goal of SafelyMutable is to describe the functionality of Cell in a Trait the type system is aware of so that other types can implement Cell's functionality (and assignment). Instead of being an OVNI, Cell becomes one of the types implementing SafelyMutable.


Of course, this trait would not be too useful without rules for its usage:

> An instance of a `SafelyMutable` type `T` may be assigned to even if potentially aliased.

This enables the following code:

```rust
#![deriving(SafelyMutable)]
struct Point {
i: int,
j: int,
}

fn main() {
let mutable p = Point{i: 5, j: 10};
let mutable vec = vec!(&mutable p);
p = Point{i: 2, j: 5}; // Mutable even though aliased!
}
```


## 4. Further examples

`&mutable T` is not restricted to `SafelyMutable` types, which allows threading `mutable` down:

```rust
#![deriving(SafelyMutable)]
struct Point {
pub i: int,
pub j: int,
}

enum Line {
Degenerate(Point),
Regular(Point, Point),
}

fn switch_end(line: &mutable Line, point: Point) {
match line {
Degenerate(&mutable p) => *p = point;
Regular(_, &mutable end) => *end = point;
}
}
```

However, unless you derive from `SafelyMutable`, assignment is not allowed if aliases may exist:

```rust
fn other() {
let mutable x = Line::new(Point{3,4}, Point{3,4});
let vec = vec!(&mutable x);
x = Line::new(Point{3,4}, Point{4,5}); // error: `x` is currently borrowed
}
```

And of course, you can mix and match `SafelyMutable` members, allowing:

```rust
struct Geom {
pub origin: Point,
pub vector: Line,
}

fn switch(left: &mutable Geom, right: &Geom) {
left.origin = right.origin; // OK: `origin` is `SafelyMutable`
left.vector = right.vector; // error: `Line` is not `SafelyMutable`
}
```


## 5. Extending safe mutability beyond PODs

I have yet to find a way for the compiler to check that a `SafelyMutable` type embeds non `SafelyMutable` members safely. This does not prevent us to use `unsafe` code to fix `Cell` though:

```rust
#![deriving(SafelyMutable)]
pub struct Cell<T> {
value: Unsafe<T>,
noshare: marker::NoShare,
}

impl<T:Copy> Cell<T> {
pub fn new(v: T) -> Cell<T> { Cell { value: Unsafe::new(v), noshare: marker::NoShare, } }
pub fn get(&self) -> T { unsafe { *self.value.get() } }
pub fn set(&mutable self) { unsafe { *self.value.get() = value; } }
}
```

Benefits of the new version:

- `Cell` can be assigned to whenever `Cell::set` can be called, and vice-versa, which matches our intuition
- it is now possible to pass *read-only* references to a `Cell`

The same could probably be applied to arrays, except that dynamic arrays are not exactly part of Rust, indeed `Vec` is implemented with a `*mut T` backing array and capacity and actual length are tracked separately. Still, there could be some interesting cases:

```rust
impl<T> Vec<T> {
/// Append an element to a vector *if* there is enough capacity already.
///
/// Returns the element if it could not be appended.
pub fn push_no_alloc(&mutable self, value: T) -> Option<T> {
if self.len == self.cap {
Some(value)
} else {
unsafe {
let end = (self.ptr as *T).offset(self.len as int) as *mutable T;
move_val_init(&mutable *end, value);
}
self.len += 1;
None
}
}
}
```

*Note: since we are manipulating raw memory, it's impossible to avoid `unsafe`; however do note that we did not pass `&exclusive mutable self`: references to existing elements are safe as there is no re-allocation of the backing array.*


# Opened doors

## Conditional implementation of `SafelyMutable`

Since we use a trait system, we can use conditional implementation of `SafelyMutable`:

```rust
struct Cons<T> {
pub t: T,
}

impl<T: SafelyMutable> SafelyMutable for Cons<T> {}
```

*Note: this is maybe too complicated, and we can start by requiring that a type either always is or is not `SafelyMutable` regardless of the parameters, as for `Cell`.*


## Mutable members and `&exclusive self`

In C++, one can use the `mutable` keyword to declare members that may be mutated even in `const` methods. This is often used to implement either lazy-computation or caching of values. Today, this code in Rust requires either:

- unsafe code
- exposing a `&mut self` method, even though the mutation is hidden to the user

If we allow such an extension to the language, then `&exclusive self` is necessary:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means the &exclusive reference type means inherited mutability just like &exclusive mutable. The only difference is the need to jump through an extra hoop to use the obfuscated &exclusive mutability.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll refer you to my precedent comment. Mutability not exposed to the user is an implementation detail.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not an implementation detail, because it has an impact on the reasoning that's possible for the type. If it gets in the way of sane user-defined optimizations via rewrite rules (it does) then it's part of the public API.


```rust
struct LazyComputation<T> {
priv producer: mutable proc () -> T,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're proposing extra complexity to get rid of hidden internal mutability, but here it is added right back. Just as we can't enforce how people are going to use Cell today, there's no way to enforce conventions for mutable.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First: this is a possible extension.

Second, I do not propose to get rid of internal mutability. I propose to get rid of visible internal mutability. A type can be logically immutable while still being implemented with internal mutability for efficiency reasons; what matters is its interface not its implementation.

The problem with Cell today is that its interface exposes surprising changes; on the other hand I take no issue with Rc maintaining a references counter internally because the users care little about that counter.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How will the compiler enforce that mutable is not used for visible mutability? The Rc internal mutability is certainly visible, because a destructor can have a side effect and calling clone will delay the effect.

priv value: mutable Option<T>,
}

impl<T> LazyComputation<T> {
pub fn get(&'a exclusive self) -> &'a T {
// since `self` is `&exclusive`, `self.value` is `&exclusive mutable`
match self.value {
None => self.value = producer();
_ => ();
}
self.value.get()
}
}
```


# Alternatives

There are several other RFCs that aim to tackle some issues, and Niko had a blog post about focusing on non-aliasing rather than mutability to enforce memory-safety.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mutability system isn't what provides safety in today's system either.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe poorly worded; I certainly did not attempt to make such claim. My point was that Niko wanted to shift the emphasis on non-aliasing.


For reference:

- [RFC #77: Unboxed Closures](https://github.com/rust-lang/rfcs/pull/77/) aims at solving the closure issue without modifying the current type system

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was proposed long before the fuss about mutability. It was not proposed as a way to eliminate a special case from the compiler. It eliminates the special case as a side effect of being sane rather than as a goal.

- [Focusing on Ownership](http://smallcultfollowing.com/babysteps/blog/2014/05/13/focusing-on-ownership/) details how Rust would be better off focusing on aliasing than mutability
- [RFC #58: Rename &mut to &only](https://github.com/rust-lang/rfcs/pull/58) also argues that aliasing is the better focus

There has, however, been an uproar of part of the community, the message was:

- mutability is an important concept, even if memory-safety can be enforced without it

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really sure that it's an important concept. Enforcing a complex mutability system feels a lot like obfuscating code while doing a pointless song and dance to work around it because it's not really how things work at a low-level. The current system is only worth it to me because the inherited mutability concept already exists for &mut and having it available to catch common errors with local variables has a low cost.

I can almost see the point of having 3 reference types (transitive immutability, internal/external mutability, internal/external/inherited mutability) but I can't understand what we gain from having four. Even with 3 reference types, it certainly feels like there's going to be a large increase in complexity over the current system. At the very least it's going to result in a plethora of new methods for handling each of &imm T, &mut T and &T unless I'm missing something.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I must admit I still do not understand your classification; it's probably that I lack some background but unfortunately I consequently have a hard time understanding this system.

My feeling is that imm would represent bit-wise immutable types, whilst the two others would stand for what they are today; but in this case once again we fall on the issue that this proposal is about the interface of a type and not its implementation: logical const-ness vs bit-wise const-ness.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's truly a concept of "logical" immutability for a language to expose in a coherent way. Logical immutability is one of the C++ misfeatures removed by D.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that D uses bitwise immutability for thread safety and optimization, problems that Rust tackles through different means. Coming from C++, I find logical immutability very useful, as it allows one to see intent and reason about behavior independently of implementation.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rust will still depend on immutability for thread safety when using &T for data parallelism. The &T type will never simply mean aliasing reference, because otherwise it could allow all mutation that does not invalidate references, without Cell. This would be more flexible than Cell, because you could mutate a field without a by-value assignment or take references into fields of the mutable data. It's simply untrue that aliasing is somehow independent of mutability, since aliasing has to be defined in terms of mutation. Two references alias if there can be a memory dependency between them. It's trivial to prove that Rust does not enforce aliasing defined terms of address equality.

- internally mutable types (such as `Cell`) are confusing because even though `mut` is about inherited mutability it is viewed as a marker of the actual mutability of the object, causing a paper cut

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This problem isn't solved if mutable exists for fields for mutation through &exclusive.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once again, we worry about different things. I worry about logical mutability while you worry about bit-wise mutability.

From a logical mutability point of view, the existence of mutable T or `Unsafe fields is not necessarily an issue providing this implementation detail does not leak into the interface.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the compiler is not enforcing that it's only logical mutability (whatever that means), it's no different than Cell. It's only a convention that you want people to follow, but have no way of enforcing. The fact that it's a language feature doesn't make the convention any more likely to be respected.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C++ provides many ways to defeat const, but it's still very useful for expressing intent and catching programming mistakes.


On the other hand, this proposal has a complexity overhead. Still, I do believe that the resulting code is clearer as we stop conflating mutability and aliasing, and it also opens up interesting avenues (such as `Vec::push_no_alloc`).


# Unresolved questions

* The exact interactions with concurrent/parallel programming are still unclear. It seems unsafe to attempt to share an instance of a `SafelyMutable` across multiple tasks, and thus I believe it more prudent to require non-aliasing, as it is today.

* Should references be considered `SafelyMutable` ?

* It is unclear how best to make `Cell` being `SafelyMutable` work; for now I used the rule that unsafe interior is OK, however it could be formulated by white-listing some types, etc...

* The exact names should be decided. I purposely refrain from giving my opinion on the matter.