Skip to content

Commit c911f7a

Browse files
committed
Update doc page
1 parent e8167e4 commit c911f7a

File tree

1 file changed

+132
-9
lines changed
  • docs/_docs/reference/experimental

1 file changed

+132
-9
lines changed

docs/_docs/reference/experimental/cc.md

+132-9
Original file line numberDiff line numberDiff line change
@@ -216,13 +216,13 @@ This widening is called _avoidance_; it is not specific to capture checking but
216216

217217
## Capability Classes
218218

219-
Classes like `CanThrow` or `FileSystem` have the property that their values are always intended to be capabilities. We can make this intention explicit and save boilerplate by declaring these classes with a `@capability` annotation.
219+
Classes like `CanThrow` or `FileSystem` have the property that their values are always intended to be capabilities. We can make this intention explicit and save boilerplate by letting these classes extend the `Capability` class defined in object `cap`.
220220

221-
The capture set of a capability class type is always `{cap}`. This means we could equivalently express the `FileSystem` and `Logger` classes as follows:
221+
The capture set of a `Capability` subclass type is always `{cap}`. This means we could equivalently express the `FileSystem` and `Logger` classes as follows:
222222
```scala
223-
import annotation.capability
223+
import caps.Capability
224224

225-
@capability class FileSystem
225+
class FileSystem extends Capability
226226

227227
class Logger(using FileSystem):
228228
def log(s: String): Unit = ???
@@ -290,7 +290,7 @@ The captured references of a class include _local capabilities_ and _argument ca
290290
the local capabilities of a superclass are also local capabilities of its subclasses. Example:
291291

292292
```scala
293-
@capability class Cap
293+
class Cap extends caps.Capability
294294

295295
def test(a: Cap, b: Cap, c: Cap) =
296296
class Super(y: Cap):
@@ -317,7 +317,7 @@ The inference observes the following constraints:
317317

318318
For instance, in
319319
```scala
320-
@capability class Cap
320+
class Cap extends caps.Capability
321321
def test(c: Cap) =
322322
class A:
323323
val x: A = this
@@ -502,7 +502,7 @@ Under the language import `language.experimental.captureChecking`, the code is i
502502
```
503503
To integrate exception and capture checking, only two changes are needed:
504504

505-
- `CanThrow` is declared as a `@capability` class, so all references to `CanThrow` instances are tracked.
505+
- `CanThrow` is declared as a class extending `Capability`, so all references to `CanThrow` instances are tracked.
506506
- Escape checking is extended to `try` expressions. The result type of a `try` is not allowed to
507507
capture the universal capability.
508508

@@ -635,9 +635,132 @@ To summarize, there are two "sweet spots" of data structure design: strict lists
635635
side-effecting or resource-aware code and lazy lists in purely functional code.
636636
Both are already correctly capture-typed without requiring any explicit annotations. Capture annotations only come into play where the semantics gets more complicated because we deal with delayed effects such as in impure lazy lists or side-effecting iterators over strict lists. This property is probably one of the greatest plus points of our approach to capture checking compared to previous techniques which tend to be more noisy.
637637

638-
## Function Type Shorthands
638+
## Existential Capabilities
639639

640-
TBD
640+
In fact, what is written as the top type `cap` can mean different capabilities, depending on scope. For instance, consider the function type
641+
`() -> Iterator[T]^`. This is taken to mean
642+
```scala
643+
() -> Exists x. Iterator[T]^x
644+
```
645+
In other words, it means an unknown type bound `x` by an "existential" in the scope of the function result. A `cap` in a function result is therefore different from a `cap` at the top-level or in a function parameter.
646+
647+
Internally, an existential type is represented as a kind of dependent function type. The type above would be modelled as
648+
```scala
649+
() -> (x: Exists) -> Iterator[T]^x
650+
```
651+
Here, `Exists` is a sealed trait in the `caps` object that serves to mark
652+
dependent functions as representations of existentials. It should be noted
653+
that this is strictly an internal representation. It is explained here because it can show up in error messages. It is generally not recommended to use this syntax in source code. Instead one should rely on the automatic expansion of `^` and `cap` to existentials, which can be
654+
influenced by introducing the right alias types. The rules for this expansion are as follows:
655+
656+
- If a function result type contains covariant occurrences of `cap`,
657+
we replace these occurrences with a fresh existential variable which
658+
is bound by a quantifier scoping over the result type.
659+
- We might want to do the same expansion in function arguments, but right now this is not done.
660+
- Occurrences of `cap` elsewhere are not translated. They can be seen as representing an existential at the top-level scope.
661+
662+
**Examples:**
663+
664+
- `A => B` is an alias type that expands to `(A -> B)^`, therefore
665+
`() -> A => B` expands to `() -> Exists c. A ->{c} B`.
666+
667+
- `() -> Iterator[A => B]` expands to `() -> Exists c. Iterator[A ->{c} B]`
668+
669+
- `A -> B^` expands to `A -> Exists c.B^{c}`.
670+
671+
- If we define `type Fun[T] = A -> T`, then `() -> Fun[B^]` expands to `() -> Exists c.Fun[B^{c}]`, which dealiases to `() -> Exists c.A -> B^{c}`. This demonstrates how aliases can be used to force existential binders to be in some specific outer scope.
672+
673+
- If we define
674+
```scala
675+
type F = A -> Fun[B^]
676+
```
677+
then the type alias expands to
678+
```scala
679+
type F = A -> Exists c.A -> B^{c}
680+
```
681+
682+
**Typing Rules:**
683+
684+
- When we typecheck the body of a function or method, any covariant occurrences of `cap` in the result type are bound with a fresh existential.
685+
- Conversely, when we typecheck the application of a function or method,
686+
with an existential result type `Exists ex.T`, the result of the application is `T` where every occurrence of the existentially bound
687+
variable `ex` is replaced by `cap`.
688+
689+
## Reach Capabilities
690+
691+
Say you have a method `f` that takes an impure function argument which gets stored in a `var`:
692+
```scala
693+
def f(op: A => B)
694+
var x: A ->{op} B = op
695+
...
696+
```
697+
This is legal even though `var`s cannot have types with `cap` or existential capabilities. The trick is that the type of the variable `x`
698+
is not `A => B` (this would be rejected), but is the "narrowed" type
699+
`A ->{op} B`. In other words, all capabilities retained by values of `x`
700+
are all also referred to by `op`, which justifies the replacement of `cap` by `op`.
701+
702+
A more complicated situation is if we want to store successive values
703+
held in a list. Example:
704+
```scala
705+
def f(ops: List[A => B])
706+
var xs = ops
707+
var x: ??? = xs.head
708+
while xs.nonEmpty do
709+
xs = xs.tail
710+
x = xs.head
711+
...
712+
```
713+
Here, `x` cannot be given a type with an `ops` capability. In fact, `ops` is pure, i.e. it's capture set is empty, so it cannot be used as the name of a capability. What we would like to express is that `x` refers to
714+
any operation "reachable" through `ops`. This can be expressed using a
715+
_reach capability_ `ops*`.
716+
```scala
717+
def f(ops: List[A => B])
718+
var xs = ops
719+
var x: A ->{ops*} B = xs.head
720+
...
721+
```
722+
Reach capabilities take the form `x*` where `x` is syntactically a regular capability. If `x: T` then `x*` stands for any capability that appears covariantly in `T` and that is accessed through `x`. The least supertype of this capability is the set of all capabilities appearing covariantly in `T`.
723+
724+
## Capability Polymorphism
725+
726+
It is sometimes convenient to write operations that are parameterized with a capture set of capabilities. For instance consider a type of event sources
727+
`Source` on which `Listener`s can be registered. Listeners can hold certain capabilities, which show up as a parameter to `Source`:
728+
```scala
729+
class Source[X^]:
730+
private var listeners: Set[Listener^{X^}] = Set.empty
731+
def register(x: Listener^{X^}): Unit =
732+
listeners += x
733+
734+
def allListeners: Set[Listener^{X^}] = listeners
735+
```
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
738+
and assigns it to the variable.
739+
740+
Capture set variables `X^` are represented as regular type variables with a
741+
special upper bound `CapSet`. For instance, `Source` could be equivalently
742+
defined as follows:
743+
```scala
744+
class Source[X <: CapSet^]:
745+
...
746+
```
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:
749+
```scala
750+
class Async extends caps.Capability
751+
752+
def listener(async: Async): Listener^{async} = ???
753+
754+
def test1(async1: Async, others: List[Async]) =
755+
val src = Source[CapSet^{async1, others*}]
756+
...
757+
```
758+
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:
759+
```scala
760+
src.register(listener(async1))
761+
others.map(listener).foreach(src.register)
762+
val ls: Set[Listener^{async, others*}] = src.allListeners
763+
```
641764

642765
## Compilation Options
643766

0 commit comments

Comments
 (0)