A prototype implementation of the polymorphic reachability type system, dubbed the Diamond programming language. The Diamond language has a Scala-like syntax, and enhances types with reachability qualifiers that track reachability and sharing of resources.
- Tracking aliases in types
A freshly allocated resource (e.g. memory allocation) is tracked but has not aliased to any variable:
Ref 42 // : Ref[Int]^◆The diamond qualifier notation indicates that this term yields a fresh resource.
Assignment propagates aliases (or reachability):
val x = Ref 42;
val y = x; // : Ref[Int]^y under the context of y: Ref[Int]^x, x: Ref[Int]^◆
...- Polymorphic identity function
def polyId[T <: Top](x: T^<>): T^x = x
val x = id[Int](3); // : Int^∅
// type argument to id is optional
val c = id(Ref 42); // : Ref[Int]^c
x + (! c) // : Int^∅The identity function is not only polymorphic with respect to argument types, but also
to its reachability.
The result of id(3) is untracked, since its argument is untracked (indicated
by the empty set notation).
In contrast, the result of id(Ref 42) remains tracked (indicated by the c
in the type), since Ref 42 is a freshly allocated resource that we want to
track.
The type system guarantees tracked resources remain tracked all the time.
- Separation
val c1 = Ref 42;
val c2 = c1; // c2 and c1 are aliased
def f(x: Ref[Int]^<>): Int = { (!x) + (!c1) };
f(c2) // type errorFunction applications in Diamond check separation between the applied function and argument.
For example, f captures c1 from its defining scope, and also takes an argument x.
The argument of x is annotated with the diamond, indicating that the function cannot
take argument that has overlap with what can be observed by the function from the environment.
Therefore, the above example issues a type error.
Programmers can indicate permissible overlap at the function's argument position.
For example, we can add c1 into the f's argument qualifier, then the following
program type checks (even though we provide c2 as argument but the typechecker knows
that c1 can be reached from c2).
val c1 = Ref 42;
val c2 = c1; // c2 and c1 are aliased
def f(x: Ref[Int]^{c1, <>}): Int = { (!x) + (!c1) };
f(c2) // ok- Escaped resources
Diamond smoothly supports tracking escaped resources via self-reference.
The following snippet shows an example that we allocate
a mutable cell with function counter, which then returns
two functions to increase and decrease the mutable cell.
These two functions they capture the same resource and are
returned as a pair.
def counter(n: Int) = {
val x = Ref n;
val inc = () => { x := (! x) + 1 };
val dec = () => { x := (! x) - 1 };
makePair[(() => Unit)^x][(() => Unit)^x](inc)(dec)
};
val p = counter(0);
val inc = fst[(() => Unit)^p][(() => Unit)^p](p); // (() => Unit)^p
val dec = snd[(() => Unit)^p][(() => Unit)^p](p); // (() => Unit)^pIn the outer scope, once we leave the scope of x, we can still track
the fact that the inc and dec function they access and mutate the same
hidden resource (see examples/counter.dia for full example).
Diamond is written in Scala 3, so to get started with the project, you need
sbt, the build tool for Scala projects. You can install sbt and JVM
environment using Coursier.
You can play with the type checker and interpreter by writing programs under examples and run
the following command to see its type and evaluation result.
sbt:Diamond> run <filename.dia>
For example, examples/polyId.dia contains a snippet of the polymorphic identity function:
def polyId[T <: Top](x: T^<>): T^x = x
val x = id(3); // : Int^∅
val c = id(Ref 42); // : Ref[Int]^◆
x + (! c) // : Int^∅Running run polyId.dia shows the parsed AST, the type of the final result, as
well as the evaluation result.
Additionally, you may run test in sbt to check all mechanized test cases.
sbt:Diamond> test
The implemented front-end language adds a topval form to define top-level
bindings.
It is useful to assist experimenting with the language, but is not intended to
be used as a real feature of the language.
In contrast to ordinary let-binding using the val keyword, bindings declared
with topval will be persistent, i.e. we would not leave its scope at the end
of the program. In this way, we can examine the qualifiers more clearly.
For example, if we define x using topval:
def polyId[T <: Top](x: T^<>): T^x = x
topval x = id(Ref 42);
x
We will see the final type is Ref[Int^∅]^x where the reachability x is residualized
(instead of Ref[Int^∅]^◆).
src/main/scala/qualfsubCoreLang.scalathe core language definitions.TypeCheck.scalathe type checker.Eval.scalathe interpreter.Run.scalatop-level driver and pretty-printers.Parser.scalathe parser that translates the Diamond front-end language to the core language.
src/test/scalatest cases.examplesexamples written in the Diamond front-end language.coq: Coq proof for the metatheory of algorithmic subtyping used in the implementation.
-
Polymorphic Reachability Types: Tracking Freshness, Aliasing, and Separation in Higher-Order Generic Programs
by Guannan Wei, Oliver Bračevac, Songlin Jia, Yuayn Bao, and Tiark Rompf (preprint). -
Reachability Types: Tracking Aliasing and Separation in Higher-order Functional Programs (OOPSLA 2021)
by Yuyan Bao, Guannan Wei, Oliver Bračevac, Luke Jiang, Qiyang He, and Tiark Rompf (pdf). -
Mechanizations and Coq proofs of the core calculus can be found here.