Leo Osvald, Tiark Rompf {losvald,tiark}@purdue.edu
Scala '17
Data races may occur if there are >2 concurrent accesses to the same memory location with:
undefined behavior in sequential programs; e.g., iterator invalidation if simultaneously:
mutating the data structure
val nats = scala.collection.mutable.MutableList(1)
for (nat <- nats) {
println(nat)
if (nat < 10) nats += nat + 1
} // prints 1 and 2 (not 1 only, not 1 through 10)
Pragmatic approaches:
Related work:
Rust's borrow checker enforces that a resource is referred to by either:
&T
); or&mut T
)Notes:
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);
hard to reason about; the line at fault is
let y = &mut x;
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
Non-goals:
E.g., translate Rust code (flow-sensitive):
let y = &mut x;
...
to Scala code (flow-insensitive):
bindMut(x) { y => ... }
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:
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.
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).
Idea: exploit 2nd-class values, annotated with @local
Properties:
var
s) or fields
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
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
}
...
}
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
...
}
ref.value
passed to bindMut
/bindImm
via MacrosConcern: 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 */ }
}
}
bindMut
/bindImm
are either:
Var[Int,Imm[Int]]
)letMut
/let
ref.value
through a non-wrapperConcern: 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)
}
__newVar
to disallow syntactic forms like var x = ref.value
__assign
to disallow syntactic forms such as x = ref.value
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
...
}
}
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
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.
We have shown how to:
Key challenges & solutions: