Skip to content
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

Mutable references: mut cells everywhere #159

Closed
wants to merge 25 commits into from
Closed

Conversation

yannbolliger
Copy link
Collaborator

@yannbolliger yannbolliger commented May 26, 2021

Proposal for supporting mutable references. Closes #140

Original solution
The envisioned solution was to wrap mutably borrowed references in a case class MutRef[@mutable T](t: T) when the borrow happens. At the end of the loan's lifetime, we would reassign the changes made on the MutRef object's field back to the original object. A little example:

pub fn main() {
  let mut x = 1;
  let y = &mut x; // borrow happens
  *y = 2;
  assert!(x == 2) // original used again, hence loan over
}

Would have been translated to the following in Scala:

object Mutable {
  final case class MutRef[@mutable T](var t: T)
  def main() = {
    var x = 1
    val y = MutRef(x) // borrow as a wrap in MutRef
    y.t = 2
    x = y.t // propagate changes back to the original
    assert(x == 2)
  }
}

In the above example, the reassignment of changes to the original x is necessary because of Scala semantics. In other cases where the original is an object, Scala semantics would be correct for our use-case but Stainless is not able to track the aliasing that is created by the wrap into MutRef. The following examples shows a weird case of this:

pub struct Thing<T> { field: T }

pub fn change_thing<T>(thing: &mut Thing<T>, t: T) {
  *thing = Thing { field: t };
}

pub fn main() {
  let mut thing = Thing { field: 123 };
  change_thing(&mut thing, 456);
  assert!(thing.field == 456);

  let thing2 = &mut thing;
  change_thing(thing2, 789);
  assert!(thing.field == 789);

  let thing3 = &mut thing;
  *thing3 = Thing { field: 0 };
  assert!(thing.field == 0);
}

This is translated to the following in Scala (only the main is shown):

  def main() = {
    val thing = Thing(123)
    change_thing(MutRef(thing), 456) // MutRef is not bound, last binding is 'thing'
    assert(thing.field == 456)

    val thing2 = MutRef(thing)  // MutRef is bound to 'thing2'
    change_thing(thing2, 789)
    thing = thing2.t
    assert(thing.field == 789)

    val thing3 = MutRef(thing)  // MutRef is bound to 'thing3'
    thing3.t = Thing(0)
    thing = thing3.t
    assert(thing.field == 0)
  }

Stainless correctly tracks aliasing in the first block, where the borrow happens in the argument of the function. This is probably due to the fact that the last binding of that value is still thing. Then, the changes are correctly applied back to thing in the AntiAliasing phase.

For the two later blocks, this is not the case. The newly created MutRef(thing) is bound to a name and Stainless correctly tracks changes to the MutRef objects thing2 and thing3. But it does not track the fact that they are still aliasing thing and therefore it doesn't propagate changes back to thing.

The example demonstrates, how the manual reassignment of changes to the original object at the end of the loan's lifetime is necessary to guarantee correctness. However, to implement this revealed itself to be very hard/impossible by only looking at the HIR (high-level intermediated representation in rustc) because the HIR does not know when the lifetime of a reference is over. Rustc performs borrow checking and lifetime resolution only later in the compilation and uses the MIR (mid-level IR), a sort of CFG that is much further apart from Stainless' AST, to do so.

Work-around

Thanks to @romac, I found a solution to that problem. Instead of propagating the changes back to the original, we can lift a variable that is later borrowed into a MutRef from the start. In that manner, borrowing simply becomes aliasing and Stainless can track the changes to the field of the object all the way through.

object Mutable {
  final case class MutRef[@mutable T](var t: T)
  def main() = {
    var x = MutRef(1)
    val y = x // borrowing simply becomes aliasing
    y.t = 2
    assert(x.t == 2) // changes are already performed on the original object
  }
}

Thanks to the fact that y is a val, Stainless is able to track the aliasing that is introduced and hence correctly verifies the above example.

@yannbolliger yannbolliger force-pushed the mut-refs branch 3 times, most recently from 1ed66d6 to 516a967 Compare May 28, 2021 06:58
@yannbolliger yannbolliger requested review from romac and gsps June 1, 2021 07:29
@yannbolliger yannbolliger changed the title [WIP] Mutable references sketch Mutable references: wrap borrowed variables from the start Jun 1, 2021
@yannbolliger yannbolliger force-pushed the mut-refs branch 2 times, most recently from da1251c to 5b0e976 Compare June 1, 2021 07:57
@yannbolliger yannbolliger marked this pull request as ready for review June 1, 2021 11:39
@yannbolliger yannbolliger force-pushed the mut-refs branch 4 times, most recently from fc19eb1 to 9272b04 Compare June 4, 2021 13:20
@yannbolliger yannbolliger changed the title Mutable references: wrap borrowed variables from the start Mutable references: mut cells everywhere Jun 4, 2021
@yannbolliger yannbolliger force-pushed the mut-refs branch 8 times, most recently from 7896a8c to 8fb560d Compare June 11, 2021 10:32
@yannbolliger
Copy link
Collaborator Author

Closing in favour of #164. Most of the changes here are contained there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant