Skip to content

Commit 0e6958f

Browse files
committed
Update language reference to new capset syntax
Plus added a test demonstrating branding and uses of upper and lower bounds.
1 parent aab2aa2 commit 0e6958f

File tree

2 files changed

+155
-18
lines changed

2 files changed

+155
-18
lines changed

docs/_docs/reference/experimental/cc.md

Lines changed: 116 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -726,41 +726,139 @@ Reach capabilities take the form `x*` where `x` is syntactically a regular capab
726726
It is sometimes convenient to write operations that are parameterized with a capture set of capabilities. For instance consider a type of event sources
727727
`Source` on which `Listener`s can be registered. Listeners can hold certain capabilities, which show up as a parameter to `Source`:
728728
```scala
729-
class Source[X^]:
730-
private var listeners: Set[Listener^{X^}] = Set.empty
731-
def register(x: Listener^{X^}): Unit =
732-
listeners += x
729+
class Source[cap X]:
730+
private var listeners: Set[Listener^{X}] = Set.empty
731+
def register(x: Listener^{X}): Unit =
732+
listeners += x
733733

734-
def allListeners: Set[Listener^{X^}] = listeners
734+
def allListeners: Set[Listener^{X}] = listeners
735735
```
736-
The type variable `X^` can be instantiated with a set of capabilities. It can occur in capture sets in its scope. For instance, in the example above
737-
we see a variable `listeners` that has as type a `Set` of `Listeners` capturing `X^`. The `register` method takes a listener of this type
736+
The type variable `cap X` (with `cap` being a soft modifier) can be instantiated with a set of capabilities. It can occur in capture sets in its scope. For instance, in the example above
737+
we see a variable `listeners` that has as type a `Set` of `Listeners` capturing `X`. The `register` method takes a listener of this type
738738
and assigns it to the variable.
739739

740-
Capture set variables `X^` are represented as regular type variables with a
741-
special upper bound `CapSet`. For instance, `Source` could be equivalently
740+
Capture-set variables `cap X` are represented as regular type variables within the special interval
741+
`>: CapSet <: CapSet^`. For instance, `Source` could be equivalently
742742
defined as follows:
743743
```scala
744-
class Source[X <: CapSet^]:
745-
...
744+
class Source[X >: CapSet <: CapSet^]:
745+
...
746746
```
747-
`CapSet` is a sealed trait in the `caps` object. It cannot be instantiated or inherited, so its only purpose is to identify capture set type variables and types. Capture set variables can be inferred like regular type variables. When they should be instantiated explicitly one uses a capturing
748-
type `CapSet`. For instance:
747+
`CapSet` is a sealed trait in the `caps` object. It cannot be instantiated or inherited, so its only
748+
purpose is to identify capture-set type variables and types. This representation based on `CapSet` is subject to change and
749+
its direct use is discouraged.
750+
751+
Capture-set variables can be inferred like regular type variables. When they should be instantiated
752+
explicitly one supplies a concrete capture set. For instance:
749753
```scala
750-
class Async extends caps.Capability
754+
class Async extends caps.Capability
751755

752-
def listener(async: Async): Listener^{async} = ???
756+
def listener(async: Async): Listener^{async} = ???
753757

754-
def test1(async1: Async, others: List[Async]) =
755-
val src = Source[CapSet^{async1, others*}]
756-
...
758+
def test1(async1: Async, others: List[Async]) =
759+
val src = Source[{async1, others*}]
760+
...
757761
```
758762
Here, `src` is created as a `Source` on which listeners can be registered that refer to the `async` capability or to any of the capabilities in list `others`. So we can continue the example code above as follows:
759763
```scala
760764
src.register(listener(async1))
761765
others.map(listener).foreach(src.register)
762766
val ls: Set[Listener^{async, others*}] = src.allListeners
763767
```
768+
A common use-case for explicit capture parameters is describing changes to the captures of mutable fields, such as concatenating
769+
effectful iterators:
770+
```scala
771+
class ConcatIterator[A, cap C](var iterators: mutable.List[IterableOnce[A]^{C}]):
772+
def concat(it: IterableOnce[A]^): ConcatIterator[A, {this.C, it}]^{this, it} =
773+
iterators ++= it // ^
774+
this // track contents of `it` in the result
775+
```
776+
In such a scenario, we also should ensure that any pre-existing alias of a `ConcatIterator` object should become
777+
inaccessible after invoking its `concat` method. This is achieved with mutation and separation tracking which are
778+
currently in development.
779+
780+
Finally, analogously to type parameters, we can lower- and upper-bound capability parameters where the bounds consist of concrete capture sets:
781+
```scala
782+
// We can close over anything subsumed branded by the 'trusted' capability, but nothing else
783+
def runSecure[cap C >: {trusted} <: {trusted}](block: () ->{C} Unit): Unit = ...
784+
785+
// This is a 'brand" capability to mark what can be mentioned in trusted code
786+
object trusted extends caps.Capability
787+
788+
// These capabilities are trusted:
789+
val trustedLogger: Logger^{trusted}
790+
val trustedChannel: Channel[String]^{trusted}
791+
// These aren't:
792+
val untrustedLogger: Logger^
793+
val untrustedChannel: Channel[String]^
794+
795+
runSecure: () =>
796+
trustedLogger.log("Hello from trusted code") // ok
797+
798+
runSecure: () =>
799+
trustedChannel.send("I can send") // ok
800+
trustedLogger.log(trustedChannel.recv()) // ok
801+
802+
runSecure: () => "I am pure and that's ok" // ok
803+
804+
runSecure: () =>
805+
untrustedLogger.log("I can't be used") // error
806+
untrustedChannel.send("I can't be used") // error
807+
```
808+
The idea is that every capability derived from the marker capability `trusted` (and only those) are eligible to be used in the `block` closure
809+
passed to `runSecure`. We can enforce this by an explicit capability parameter `C` constraining the possible captures of `block` to the interval `>: {trusted} <: {trusted}`
810+
811+
## Capability Members
812+
813+
Just as parametrization by types can be equally expressed with type members, we could
814+
also define the `Source[cap X]` class above could using a _capability member_:
815+
```scala
816+
class Source:
817+
cap type X
818+
private var listeners: Set[Listener^{this.X}] = Set.empty
819+
... // as before
820+
```
821+
Here, we can refer to capability members using paths in capture sets (such as `{this.X}`). Similarly to type members,
822+
capability members can be upper- and lower-bounded with capture sets:
823+
```scala
824+
trait Thread:
825+
cap type Cap
826+
def run(block: () ->{this.Cap} -> Unit): Unit
827+
828+
trait GPUThread extends Thread:
829+
cap type Cap >: {cudaMalloc, cudaFree} <: {caps.cap}
830+
```
831+
832+
833+
We conclude with a more advanced example, showing how capability members and paths to these members can prevent leakage
834+
of labels for lexically-delimited control operators:
835+
```scala
836+
trait Label extends Capability:
837+
cap type Fv // the capability set occurring freely in the `block` passed to `boundary` below.
838+
839+
def boundary[T, cap C](block: Label{cap type Fv = {C} } ->{C} T): T = ??? // ensure free caps of label and block match
840+
def suspend[U](label: Label)[cap D <: {label.Fv}](handler: () ->{D} U): U = ??? // may only capture the free capabilities of label
841+
842+
def test =
843+
val x = 1
844+
boundary: outer =>
845+
val y = 2
846+
boundary: inner =>
847+
val z = 3
848+
val w = suspend(outer) {() => z} // ok
849+
val v = suspend(inner) {() => y} // ok
850+
val u = suspend(inner): () =>
851+
suspend(outer) {() => w + v} // ok
852+
y
853+
suspend(outer): () =>
854+
println(inner) // error (would leak the inner label)
855+
x + y + z
856+
```
857+
A key property is that `suspend` (think `shift` from delimited continuations) targeting a specific label (such as `outer`) should not accidentally close over labels from a nested `boundary` (such as `inner`), because they would escape their defining scope this way.
858+
By leveraging capability polymorphism, capability members, and path-dependent capabilities, we can prevent such leaks from occurring at compile time:
859+
860+
* `Label`s store the free capabilities `C` of the `block` passed to `boundary` in their capability member `Fv`.
861+
* When suspending on a given label, the suspension handler can capture at most the capabilities that occur freely at the `boundary` that introduced the label. That prevents mentioning nested bound labels.
764862

765863
## Compilation Options
766864

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import language.experimental.captureChecking
2+
import caps.*
3+
4+
5+
def main() =
6+
trait Channel[T] extends caps.Capability:
7+
def send(msg: T): Unit
8+
def recv(): T
9+
10+
trait Logger extends caps.Capability:
11+
def log(msg: String): Unit
12+
13+
// we can close over anything subsumed by the 'trusted' brand capability, but nothing else
14+
def runSecure[cap C >: {trusted} <: {trusted}](block: () ->{C} Unit): Unit = block()
15+
16+
// This is a 'brand" capability to mark what can be mentioned in trusted code
17+
object trusted extends caps.Capability
18+
19+
val trustedLogger: Logger^{trusted} = ???
20+
val trustedChannel: Channel[String]^{trusted} = ???
21+
22+
val untrustedLogger: Logger^ = ???
23+
val untrustedChannel: Channel[String]^ = ???
24+
25+
runSecure: () =>
26+
trustedLogger.log("Hello from trusted code") // ok
27+
28+
runSecure: () =>
29+
trustedChannel.send("I can send")
30+
trustedLogger.log(trustedChannel.recv()) // ok
31+
32+
runSecure: () =>
33+
"I am pure" // ok
34+
35+
runSecure: () => // error
36+
untrustedLogger.log("I can't be used here")
37+
38+
runSecure: () => // error
39+
untrustedChannel.send("I can't be used here")

0 commit comments

Comments
 (0)