Rust-like Borrowing with 2nd-class Values
(Short Paper)

Leo Osvald, Tiark Rompf
{losvald,tiark}@purdue.edu

Scala '17

Danger of shared mutability

Data races may occur if there are >2 concurrent accesses to the same memory location with:

  1. at least one being a write,
  2. at least one being non-atomic

Solutions to mutability problems

Pragmatic approaches:

Related work:

Static prevention of mutability (Rust)

Rust's borrow checker enforces that a resource is referred to by either:

Notes:

State of the art (Rust)

Example:

  let mut x = 5;
  let y = &mut x;
  *y += 1;
  println!("{}", x);

fails to compile:

error: cannot borrow 'x' as immutable because it is
       also borrowed as mutable
            println!("{}", x);

State of the art (Rust)

Example (desugared):

let mut x = 5;
{ let y = &mut x;
  *y += 1;
  println!("{}", x);
}

Explanation of compilation error:

|
|         let y = &mut x;
|                      - mutable borrow occurs here
|         *y += 1;
|         println!("{}", x);
|                        ^ immutable borrow occurs here
|     }
|     - mutable borrow ends here

Motivation and Goals

 
Non-goals:

Key idea

E.g., translate Rust code (flow-sensitive):

let y = &mut x;
...

to Scala code (flow-insensitive):

bindMut(x) { y => ... }

Type-checking

bindImm(1) { imm =>
  bindImm(imm) { immAlias => ... }  // ok
  bindMut(imm) { mutAlias => ... }  // error (2a)
  imm.value = 0  // error (3)
}
bindMut(42) { mut =>
  bindImm(mut) { imm => ... }  // error (2b)
  bindMut(mut) { mutAlias => ... }  // error (2)
  val mutAlias = mut  // error (2)
  mut.value = 0  // ok
  mut  // error (1)
} mut.value = 1  // error (1)

Rules:

  1. no escaped variables (lexical scoping)
  2. no creation of mutable variables out of other variables (and vice versa)
  3. no assignments on immutable variables

Overall design

Library-level design

Variable wrapper

class Mut[T]
class Imm[T]
class Var[T,A](private var v: T) {  // A <: Mut[T]/Imm[T]
  def value = v
  def value_=(v2: T)(implicit ev: A =:= Mut[T]) = v = v2
}

Note: the setter that allows assignment is enabled only if access (A) is mutable.

Implicit conversions (bridge methods)

class LowPrioMut
object LowPrioMut {
  implicit def valToMut[T](v: T): Var[T,Mut[T]] =
          new Var[T,Mut[T]](v)
}

object Var extends LowPrioMut {
  implicit def valToImm[T](v: T): Var[T,Imm[T]] =
          new Var[T,Imm[T]](v)
}

Note: conversion to a mutable wrapper takes precedence (higher priority).

Bind functions (lexical scoping)

Idea: exploit 2nd-class values, annotated with @local

Properties:

  1. Cannot be stored in mutable variables (vars) or fields
  2. Cannot be returned from (member) functions
  3. Cannot be accessed by 1st-class functions through free variables
  4. (Cannot be coerced to 1st-class; 1st-class can be treated as 2nd-class.)

 
Intuition: 2nd-class Var[T,A] bindings cannot escape the declaring scope.

See also: our OOPSLA'16 paper on Affordable 2nd-Class Values for Fun and Profit

Preventing shared mutable aliases (Mut[T])

def bindMut[T, U](r: Var[T,Mut[T]])(
  @local f: (@local Var[T,Mut[T]]) => U) = f(r)

How does it work?

bindMut(42) { outer =>  // outer is inferred as 2nd-class
  ...
  bindMut(outer) { inner =>  // error: r must be 1st-class
      ...
      outer  // ok: f may refer to 2nd-class free variables
  }
  ...
}

Allowing shared immutable aliases (Imm[T])

def bindImm[T, U](@local r: Var[T,Imm[T]])(
  @local f: (@local Var[T,Imm[T]]) => U) = f(new Var[T,Imm[T]](r.value))

How does it work?

bindImm(42) { outer =>  // outer is inferred as 2nd-class
  ...
  bindImm(outer) { inner => ... } // ok
  ...
}

Meta-level design

Preventing a ref.value passed to bindMut/bindImm via Macros

Concern: ref.value may be implicitly converted to bind* parameter type

bindMut(123) { ref =>
  bindImm(ref.value) { imm => ... /* ouch */  }
}
bindImm("foo") { ref =>
  bindImm(ref) { ref2 =>
    bindMut(ref.value) { mut => ... /* ouch */ }
  }
}

Preventing escaping of a ref.value through a non-wrapper

Concern: a ref.value may escape through a non-wrapper (val/var)

letMut(123) { mut =>
  letMut(ref.value) { mut => ... }  // error (Var.value as arg.)
  var indirect = mut.value  // error (Var.value in assignment)
  let(indirect) { imm => ... }  // error (not an r-value/Var)
}

Borrowing

Definition: permit temporary aliasing

Idea: let function parameter (or a local variable) be 2nd-class

def doWithBorrowed[T](@local ref: Var[T,Mut[T]]) = ...

Example:

bindMut(new MutableObject()) { mut =>
  ...
  doWithBorrowed(mut)
  ...
  {
    @local val borrowed = mut  // requires @local to type-check
    ...
  }
}

Gradual introduction of borrowing

Wrappers:

def call[T, R](f: (@local T) => R)(@local ref: Var[T,_])
def call[T1,T2, R](f: (@local T1, @local T2) => R)(
  @local ref1: Var[T1,_])(@local ref2: Var[T2,_])
...

Note: Adding @local annotations to function parameters is highly automatable

Borrowing Example

def storeMut(@local sb: StringBuilder,
             @local store: Store): String = {
  store.field = sb  // error (cannot store 2nd-class/borrowed)
  sb.toString
}
  
bindMut(new Store()) { storeThatLeaksMutable =>
  bindMut(new StringBuilder()) { sb =>
    val s = call(storeMut)(sb)(storeThatLeaksMutable)
    sb.value.append("brakes encapsulation")
    assert(s == storeThatLeaksMutable.field.toString)  // would fail
  }
}

Note: the StringBuilder could not be stored during the borrow.

Conclusion

We have shown how to:

  1. Use 2nd-class values in Scala to model key ingredients of borrowing as in Rust
  2. Use macros and virtualized overloads to disallow escape hatches
  3. Stay context-insensitive

 

Key challenges & solutions:

  1. flow sensitivity solved by monadic style
  2. lifetime of borrowed references solved by 2nd-class values