Skip to content

Commit 04a496b

Browse files
committed
WIP add separation checking
1 parent 8fb5325 commit 04a496b

File tree

3 files changed

+256
-21
lines changed

3 files changed

+256
-21
lines changed

docs/_docs/internals/exclusive-capabilities.md

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,19 @@ Language design draft
66
## Capability Kinds
77

88
A capability is called
9-
- _exclusive_ if it is `cap` or it has an exclusive capability in its capture set.
10-
- _shared_ otherwise.
11-
12-
There is a new top capability `shared` which can be used as a capability for deriving shared capture sets. Other shared capabilities are created as read-only versions of exclusive capabilities.
9+
- _shared_ if it is classified as a `SharedCapability`
10+
- _exclusive_ otherwise.
1311

1412
## Update Methods
1513

1614
We introduce a new trait
1715
```scala
18-
trait Mutable
16+
trait Mutable extends ExclusiveCapability, Classifier
1917
```
2018
It is used as a base trait for types that define _update methods_ using
2119
a new soft modifier `update`.
2220

23-
`update` can only be used in classes or objects extending `Mutable`. An update method is allowed to access exclusive capabilities in the method's environment. By contrast, a normal method in a type extending `Mutable` may access exclusive capabilities only if they are defined locally or passed to it in parameters.
21+
`update` can only be used in classes or objects extending `Mutable`. An update method is allowed to access exclusive capabilities in the method's environment. By contrast, a normal method in a type extending `Mutable` may access exclusive capabilities only if they are defined locally or passed to it in parameters.
2422

2523
**Example:**
2624
```scala
@@ -115,11 +113,6 @@ a method that accesses exclusive capabilities.
115113

116114
If `x` is an exclusive capability of a type extending `Mutable`, `x.rd` is its associated, shared _read-only_ capability.
117115

118-
`shared` can be understood as the read-only capability corresponding to `cap`.
119-
```scala
120-
shared = cap.rd
121-
```
122-
123116
A _top capability_ is either `cap` or `shared`.
124117

125118

@@ -134,7 +127,7 @@ The meaning of `^` and `=>` is the same as before:
134127

135128
**Implicitly added capture sets**
136129

137-
A reference to a type extending any of the traits `Capability` or `Mutable` gets an implicit capture set `{shared}` in case no explicit capture set is given.
130+
A reference to a type extending trait `Mutable` gets an implicit capture set `{cap.rd}` provided no explicit capture set is given.
138131

139132
For instance, a matrix multiplication method can be expressed as follows:
140133

@@ -148,7 +141,7 @@ def mul(a: Matrix, b: Matrix, c: Matrix^): Unit =
148141
```
149142
Here, `a` and `b` are implicitly read-only, and `c`'s type has capture set `cap`. I.e. with explicit capture sets this would read:
150143
```scala
151-
def mul(a: Matrix^{shared}, b: Matrix^{shared}, c: Matrix^{cap}): Unit
144+
def mul(a: Matrix^{cap.rd}, b: Matrix^{cap.rd}, c: Matrix^{cap}): Unit
152145
```
153146
Separation checking will then make sure that `a` and `b` must be different from `c`.
154147

@@ -173,8 +166,6 @@ The read-only function `ro` maps capture sets to read-only capture sets. It is d
173166
- `ro(x) = x` if `x` is shared
174167
- `ro(x) = x.rd` if `x` is exclusive
175168

176-
177-
178169
## Subcapturing
179170

180171
Subcapturing has to take the mode of capture sets into account. We let `m` stand for arbitrary modes.
@@ -271,7 +262,7 @@ A reference `p.m` to an update method or class `m` of a mutable type is allowed
271262

272263
If `e` is an expression of a type `T^cs` extending `Mutable` and the expected type is a value type that is not a mutable type, then the type of `e` is mapped to `T^ro(cs)`.
273264

274-
265+
<!--
275266
## Expression Typing
276267
277268
An expression's type should never contain a top capability in its deep capture set. This is achieved by the following rules:
@@ -302,7 +293,6 @@ An expression's type should never contain a top capability in its deep capture s
302293
where the capture set of `x` contains a top capability,
303294
replace `x` by a fresh skolem `val sk: T`. Alternatively: keep it as is, but don't widen it.
304295
305-
306296
## Post Processing Right Hand Sides
307297
308298
The type of the right hand sides of `val`s or `def`s is post-processed before it becomes the inferred type or is compared with the declared type. Post processing
@@ -340,7 +330,7 @@ Since expression types can't mention cap, widening happens only
340330
341331
**Definitions:**
342332
343-
- The _transitive capture set_ `tcs(c)` of a capability `c` with underlying capture set `C` is `c` itself, plus the transitive capture set of `C`, but excluding `cap` or `shared`.
333+
- The _transitive capture set_ `tcs(c)` of a capability `c` with underlying capture set `C` is `c` itself, plus the transitive capture set of `C`.
344334
345335
- The _transitive capture set_ `tcs(C)` of a capture set C is the union
346336
of `tcs(c)` for all elements `c` of `C`.
@@ -356,7 +346,9 @@ Since expression types can't mention cap, widening happens only
356346
357347
**Algorithm outline:**
358348
359-
- Associate _shadowed sets_ with blocks, template statement sequences, applications, and val symbols. The idea is that a shadowed set gets populated when a capture reference is widened to cap. In that case the original references that were widened get added to the set.
349+
350+
351+
Associate _shadowed sets_ with blocks, template statement sequences, applications, and val symbols. The idea is that a shadowed set gets populated when a capture reference is widened to cap. In that case the original references that were widened get added to the set.
360352
361353
- After processing a `val x: T2 = t` with `t: T1` after post-processing:
362354

docs/_docs/reference/experimental/capture-checking/scoped-caps.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ title: "Scoped Caps"
44
nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/capture-checking/scoped-caps.html
55
---
66

7-
## Introduction
7+
## Scoped Universal Capabilities
88

99
When discussing escape checking, we referred to a scoping discipline. That is, capture sets can contain only capabilities that are visible at the point where the set is defined. But that raises the question where a universal capability `cap` is defined? In fact, what is written as the top type `cap` can mean different capabilities, depending on scope. Usually a `cap` refers to a universal capability defined in the scope where the `cap` appears.
1010

@@ -84,6 +84,7 @@ To summarize:
8484
variable `ex` is replaced by `cap`.
8585
-->
8686

87+
<!--
8788
## Reach Capabilities
8889
8990
Say you have a method `f` that takes an impure function argument which gets stored in a `var`:
@@ -118,3 +119,4 @@ def f(ops: List[A => B])
118119
...
119120
```
120121
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`.
122+
-->

docs/_docs/reference/experimental/capture-checking/separation-checking.md

Lines changed: 242 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,245 @@ title: "Separation Checking"
44
nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/capture-checking/separation-checking.html
55
---
66

7-
## TODO
7+
## Introduction
8+
9+
Separation checking is an extension of capture checking that enforces unique, un-aliased access to capabilities. It is enabled by a language import
10+
```scala
11+
import language.experimental.separationChecking
12+
```
13+
(or the corresponding setting `-language:experimental.separationChecking`).
14+
The import has to be given in addition to the `language.experimental.captureChecking` import that enables capture checking.
15+
The reason for the second language import is that separation checking is less mature than capture checking proper, so we are less sure
16+
we got the balance of safety and expressivity right for it at the present time.
17+
18+
The purpose of separation checking is to ensure that certain accesses to capabilities are not aliased. As an example, consider matrix multiplication. A method that multiplies matrices `a`, and `b` into `c`, could be declared like this:
19+
```scala
20+
def multiply(a: Matrix, b: Matrix, c: Matrix): Unit
21+
```
22+
But that signature alone does not tell us which matrices are supposed to be inputs and which one is the output. Nor does it guarantee that an input matrix is not re-used as the output, which would lead to wrong results.
23+
24+
With separation checking, we can declare `Matrix` as a `Mutable` type, like this:
25+
```scala
26+
class Matrix(nrows: Int, ncols: Int) extends Mutable:
27+
update def setElem(i: Int, j: Int, x: Double): Unit = ...
28+
def getElem(i: Int, j: Int): Double = ...
29+
```
30+
We further declare that the `setElem` method in a `Matrix` is an `update` method, which means it has a side effect.
31+
32+
Separation checking also gives a special interpretation to the following modified signature of `multiply`:
33+
```scala
34+
def multiply(a: Matrix, b: Matrix, c: Matrix^): Unit
35+
```
36+
In fact, only a single character was added: `c`'s type now carries a universal capability. This signature enforces at the same time two desirable properties:
37+
38+
- Matrices `a`, and `b` are read-only; `multiply` will not call their update method. By contrast, matrix `c` can be updated.
39+
- Matrices `a` and `b` are different from matrix `c`, but `a` and `b` could refer to the same matrix.
40+
41+
So, effectively, anything that can be updated must be unaliased.
42+
43+
The idea behind separation checking is simple: We now interpret each occurrence of `cap` as a separate top capability. This includes derived syntaxes like `A^` and `B => C`. We further keep track during capture checking which capabilities are subsumed by each `cap`. If capture checking widens a capability `x` to the universal capability, we say `x` is _hidden_ by the universal. The rule then is that any capability hidden by a universal capability `capᵢ` cannot be referenced independently or hidden in another cap in code that can see `capᵢ`.
44+
45+
Here's an example:
46+
```scala
47+
val x: C^ = y
48+
... x ... // ok
49+
... y ... // error
50+
```
51+
This principle ensures that capabilities such as `x` that have `cap` as underlying capture set are un-aliased or "fresh". Any previously existing aliases such as `y` in the code above are inaccessible as long as `x` is also visible.
52+
53+
## Capability Kinds
54+
55+
A capability is called
56+
- _shared_ if it is [classified](../classifiers.md) as a `SharedCapability`
57+
- _exclusive_ otherwise.
58+
59+
## The Mutable Trait
60+
61+
We introduce a new trait
62+
```scala
63+
trait Mutable extends ExclusiveCapability, Classifier
64+
```
65+
It is used as a [classifier](../classifiers.md) trait for types that define _update methods_ using
66+
a new soft modifier `update`.
67+
68+
**Example:**
69+
```scala
70+
class Ref(init: Int) extends Mutable:
71+
private var current = init
72+
def get: Int = current
73+
update def put(x: Int): Unit = current = x
74+
```
75+
`update` can only be used in classes or objects extending `Mutable`. An update method is allowed to access exclusive capabilities in the method's environment. By contrast, a normal method in a type extending `Mutable` may access exclusive capabilities only if they are defined locally or passed to it in parameters.
76+
77+
In class `Ref`, the `put` should be declared as an update method since it accesses the exclusive write capability of the variable `current` in its environment.
78+
79+
`update` can also be used on an inner class of a class or object extending `Mutable`. It gives all code in the class the right
80+
to access exclusive capabilities in the class environment. Normal classes
81+
can only access exclusive capabilities defined in the class or passed to it in parameters.
82+
83+
```scala
84+
object Registry extends Mutable:
85+
var count = 0
86+
update class Counter:
87+
update def next: Int =
88+
count += 1
89+
count
90+
```
91+
Normal method members of `Mutable` classes cannot call update methods. This is indicated since accesses in the callee are recorded in the caller. So if the callee captures exclusive capabilities so does the caller.
92+
93+
An update method cannot implement or override a normal method, whereas normal methods may implement or override update methods. Since methods such as `toString` or `==` inherited from Object are normal methods, it follows that none of these methods may be implemented as an update method.
94+
95+
The `apply` method of a function type is also a normal method, hence `Mutable` classes may not implement a function type with an update method as the `apply` method.
96+
97+
## Mutable Types
98+
99+
A type is called a _mutable_ if it extends `Mutable` and it has an update method or an update class as non-private member or constructor.
100+
101+
When we create an instance of a mutable type we always add `cap` to its capture set. For instance, if class `Ref` is declared as shown previously then `new Ref(1)` has type `Ref[Int]^`.
102+
103+
**Restriction:** A non-mutable type cannot be downcast by a pattern match to a mutable type.
104+
105+
**Definition:** A class is _read_only_ if the following conditions are met:
106+
107+
1. It does not extend any exclusive capabilities from its environment.
108+
2. It does not take parameters with exclusive capabilities.
109+
3. It does not contain mutable fields, or fields that take exclusive capabilities.
110+
111+
**Restriction:** If a class or trait extends `Mutable` all its parent classes or traits must either extend `Mutable` or be read-only.
112+
113+
The idea is that when we upcast a reference to a type extending `Mutable` to a type that does not extend `Mutable`, we cannot possibly call a method on this reference that uses an exclusive capability. Indeed, by the previous restriction this class must be a read-only class, which means that none of the code implemented
114+
in the class can access exclusive capabilities on its own. And we
115+
also cannot override any of the methods of this class with a method
116+
accessing exclusive capabilities, since such a method would have
117+
to be an update method and update methods are not allowed to override regular methods.
118+
119+
120+
121+
**Example:**
122+
123+
Consider trait `IterableOnce` from the standard library.
124+
125+
```scala
126+
trait IterableOnce[+T] extends Mutable:
127+
def iterator: Iterator[T]^{this}
128+
update def foreach(op: T => Unit): Unit
129+
update def exists(op: T => Boolean): Boolean
130+
...
131+
```
132+
The trait is a mutable type with many update methods, among them `foreach` and `exists`. These need to be classified as `update` because their implementation in the subtrait `Iterator` uses the update method `next`.
133+
```scala
134+
trait Iterator[T] extends IterableOnce[T]:
135+
def iterator = this
136+
def hasNext: Boolean
137+
update def next(): T
138+
update def foreach(op: T => Unit): Unit = ...
139+
update def exists(op; T => Boolean): Boolean = ...
140+
...
141+
```
142+
But there are other implementations of `IterableOnce` that are not mutable types (even though they do indirectly extend the `Mutable` trait). Notably, collection classes implement `IterableOnce` by creating a fresh
143+
`iterator` each time one is required. The mutation via `next()` is then restricted to the state of that iterator, whereas the underlying collection is unaffected. These implementations would implement each `update` method in `IterableOnce` by a normal method without the `update` modifier.
144+
145+
```scala
146+
trait Iterable[T] extends IterableOnce[T]:
147+
def iterator = new Iterator[T] { ... }
148+
def foreach(op: T => Unit) = iterator.foreach(op)
149+
def exists(op: T => Boolean) = iterator.exists(op)
150+
```
151+
Here, `Iterable` is not a mutable type since it has no update method as member.
152+
All inherited update methods are (re-)implemented by normal methods.
153+
154+
**Note:** One might think that we don't need a base trait `Mutable` since in any case
155+
a mutable type is defined by the presence of update methods, not by what it extends. In fact the importance of `Mutable` is that it defines _the other methods_ as read-only methods that _cannot_ access exclusive capabilities. For types not extending `Mutable`, this is not the case. For instance, the `apply` method of a function type is not an update method and the type itself does not extend `Mutable`. But `apply` may well be implemented by
156+
a method that accesses exclusive capabilities.
157+
158+
## Read-only Capabilities
159+
160+
If `x` is an exclusive capability of a type extending `Mutable`, `x.rd` is its associated _read-only_ capability. It counts as a shared capability. A read-only capability does not permit access to the mutable fields of a matrix.
161+
162+
A read-only capability can be seen as a classified capability
163+
using a classifier trait `Read` that extends `Mutable`. I.e.
164+
`x.rd` can be seen as being essentially the same as `x.only[Read]`.
165+
Currently, this precise equivalence is still waiting to be implemented.
166+
167+
**Implicitly added capture sets**
168+
169+
A reference to a type extending trait `Mutable` gets an implicit capture set `{cap.rd}` provided no explicit capture set is given. This is different from
170+
other capability traits which implicitly add `{cap}`.
171+
172+
For instance, consider the matrix multiplication method mentioned previously:
173+
```scala
174+
def multiply(a: Matrix, b: Matrix, c: Matrix^): Unit
175+
```
176+
It is expanded to
177+
```scala
178+
def multiply(a: Matrix^{cap.rd}, b: Matrix^{cap.rd}, c: Matrix^{cap}): Unit
179+
```
180+
Here, `a` and `b` are implicitly read-only, and `c`'s type has capture set `cap`. I.e. with explicit capture sets this would read:
181+
```scala
182+
def mul(a: Matrix^{cap.rd}, b: Matrix^{cap.rd}, c: Matrix^{cap}): Unit
183+
```
184+
Separation checking will then make sure that `a` and `b` must be different from `c`.
185+
186+
## Accesses to Mutable Types
187+
188+
An access `p.m` to an update method or class `m` in a mutable type is permitted only if the type of the prefix `M` retains exclusive capabilities. If `M` is pure or its capture set has only shared
189+
capabilities then the access is not permitted.
190+
191+
A _read-only access_ is a reference `x` to a type extending `Mutable` that is either
192+
193+
- widened to a value type that is not a mutable type, or
194+
- immediately followed by a selection with a member that is a normal method or class (not an update method or class).
195+
196+
A read-only access charges the read-only capability `x.rd` to its environment. Other accesses charge the full capability `x`.
197+
198+
**Example:**
199+
200+
Consider a reference `x` and two closures `f` and `g`.
201+
202+
```scala
203+
val x = Ref()
204+
val f = () => x.get // f: () ->{x.rd} Unit
205+
val g = () => x.set(1) // g: () ->{x} Unit
206+
```
207+
208+
`f` accesses a regular method, so it charges only `x.rd` to its environment which shows up in its capture set. Bu contrast, `g`
209+
accesses an update method of `x`,so its capture set is `{x}`.
210+
211+
A reference to a mutable type with an exclusive capture set can be widened to a reference with a read-only set. For instance, the following is OK:
212+
```scala
213+
val a: Ref^ = Ref()
214+
val b1: Ref^{a.rd} = a
215+
val b2: Ref^{cap.rd} = a
216+
```
217+
218+
## Read-Only Capsets
219+
220+
If we consider subtyping and subcapturing, we observe what looks like a contradiction: `x.rd` is seen as a restricted capability, so `{x.rd}` should subcapture `{x}`. Yet, we have seen in the example above that sometimes it goes the other way: `a`'s capture set is either `{a}` or `{cap}`, yet `a` can be used to define `b1` and `b2`, with capture sets `{a.rd}` and `{cap.rd}`, respectively.
221+
222+
The contradiction can be explained by noting that we use a capture set in two different roles.
223+
224+
First, and as always, a capture set defines _retained capabilities_ that may or may be not used by a value. More capabilities give larger types, and the empty capture set is the smallest set according to that ordering. That makes sense: If a higher-order function like `map` is willing to accept a function `A => B` that can have arbitrary effects it's certainly OK to pass a pure function of type `A -> B` to it.
225+
226+
But for mutations, we use a capture set in a second role, in which it defines a set of _access permissions_. If we have a `Ref^`, we can access all its methods, but if we have a `Ref^{cap.rd}`, we can access only regular methods, not update methods. From that viewpoint a mutable type with exclusive capabilities lets you do more than a mutable type with just read-only capabilities. So by the Liskov substitution principle, sets with exclusive capabilities subcapture sets with only read-only capabilities.
227+
228+
The contradiction can be solved by distinguishing these two roles. For access permissions, we express read-only sets with an additional _qualifier_ `RD`. That qualifier is used only in the formal theory and the implementation, it currently cannot be expressed in source.
229+
We add an implicit read-only qualifier `RD` to all capture sets on mutable types that consist only of shared or read-only capabilities.
230+
So when we write
231+
```scala
232+
val b1: Ref^{a.rd} = a
233+
```
234+
we really mean
235+
```scala
236+
val b1: Ref^{a.rd}.RD = a
237+
```
238+
239+
The subcapturing theory for sets is then as before, with the following additional rules:
240+
241+
- `C <: C.RD`
242+
- `C₁.RD <: C₂.RD` if `C₍ <: C₂`
243+
- `{x, ...}.RD = {x.rd, ...}.RD`
244+
- `{x.rd, ...} <: {x, ...}`
245+
246+
247+
248+

0 commit comments

Comments
 (0)