Skip to content

Commit 426edbd

Browse files
committed
Fixes to separation checking
1 parent 17b2315 commit 426edbd

File tree

1 file changed

+21
-26
lines changed

1 file changed

+21
-26
lines changed

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

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ class Matrix(nrows: Int, ncols: Int) extends Mutable:
2929
```
3030
We further declare that the `setElem` method in a `Matrix` is an `update` method, which means it has a side effect.
3131

32-
Separation checking also gives a special interpretation to the following modified signature of `multiply`:
32+
Separation checking gives a special interpretation to the following modified signature of `multiply`:
3333
```scala
3434
def multiply(a: Matrix, b: Matrix, c: Matrix^): Unit
3535
```
3636
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:
3737

38-
- Matrices `a`, and `b` are read-only; `multiply` will not call their update method. By contrast, matrix `c` can be updated.
38+
- Matrices `a`, and `b` are read-only; `multiply` will not call their update method. By contrast, the `c` matrix can be updated.
3939
- Matrices `a` and `b` are different from matrix `c`, but `a` and `b` could refer to the same matrix.
4040

4141
So, effectively, anything that can be updated must be unaliased.
@@ -62,11 +62,11 @@ a new soft modifier `update`.
6262
class Ref(init: Int) extends Mutable:
6363
private var current = init
6464
def get: Int = current
65-
update def put(x: Int): Unit = current = x
65+
update def set(x: Int): Unit = current = x
6666
```
6767
`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.
6868

69-
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.
69+
In class `Ref`, the `set` should be declared as an update method since it accesses the exclusive write capability of the variable `current` in its environment.
7070

7171
`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
7272
to access exclusive capabilities in the class environment. Normal classes
@@ -84,7 +84,7 @@ Normal method members of `Mutable` classes cannot call update methods. This is i
8484

8585
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.
8686

87-
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.
87+
The `apply` method of a function type is also a normal method, hence `Mutable` classes may not implement a function type using an update method as the `apply` method.
8888

8989
## Mutable Types
9090

@@ -154,7 +154,7 @@ If `x` is an exclusive capability of a type extending `Mutable`, `x.rd` is its a
154154
A read-only capability can be seen as a classified capability
155155
using a classifier trait `Read` that extends `Mutable`. I.e.
156156
`x.rd` can be seen as being essentially the same as `x.only[Read]`.
157-
Currently, this precise equivalence is still waiting to be implemented.
157+
(Currently, this precise equivalence is still waiting to be implemented.)
158158

159159
**Implicitly added capture sets**
160160

@@ -165,10 +165,6 @@ For instance, consider the matrix multiplication method mentioned previously:
165165
```scala
166166
def multiply(a: Matrix, b: Matrix, c: Matrix^): Unit
167167
```
168-
It is expanded to
169-
```scala
170-
def multiply(a: Matrix^{cap.rd}, b: Matrix^{cap.rd}, c: Matrix^{cap}): Unit
171-
```
172168
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:
173169
```scala
174170
def mul(a: Matrix^{cap.rd}, b: Matrix^{cap.rd}, c: Matrix^{cap}): Unit
@@ -177,7 +173,7 @@ Separation checking will then make sure that `a` and `b` must be different from
177173

178174
## Accesses to Mutable Types
179175

180-
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
176+
An access `p.m` to an update method or class `m` in a mutable type is permitted only if the type `M` of the prefix `p` retains exclusive capabilities. If `M` is pure or its capture set has only shared
181177
capabilities then the access is not permitted.
182178

183179
A _read-only access_ is a reference `x` to a type extending `Mutable` that is either
@@ -197,8 +193,8 @@ val f = () => x.get // f: () ->{x.rd} Unit
197193
val g = () => x.set(1) // g: () ->{x} Unit
198194
```
199195

200-
`f` accesses a regular method, so it charges only `x.rd` to its environment which shows up in its capture set. Bu contrast, `g`
201-
accesses an update method of `x`,so its capture set is `{x}`.
196+
`f` accesses a regular method, so it charges only `x.rd` to its environment which shows up in its capture set. By contrast, `g`
197+
accesses an update method of `x`, so its capture set is `{x}`.
202198

203199
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:
204200
```scala
@@ -237,7 +233,7 @@ The subcapturing theory for sets is then as before, with the following additiona
237233

238234
## Separation Checking
239235

240-
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ᵢ`.
236+
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 a top capability `capᵢ`, we say `x` is _hidden_ by `capᵢ`. The rule then is that any capability hidden by a top capability `capᵢ` cannot be referenced independently or hidden in another `capⱼ` in code that can see `capᵢ`.
241237

242238
Here's an example:
243239
```scala
@@ -247,7 +243,7 @@ val x: C^ = y
247243
```
248244
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.
249245

250-
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ᵢ`.
246+
Separation checking applies only to exclusive capabilities and their read-only versions. Any capability extending `SharedCapability` in its type is exempted; the following definitions and rules do not apply to them.
251247

252248
**Definitions:**
253249

@@ -256,7 +252,7 @@ The idea behind separation checking is simple: We now interpret each occurrence
256252
- The _transitive capture set_ `tcs(C)` of a capture set C is the union
257253
of `tcs(c)` for all elements `c` of `C`.
258254

259-
- Two capture sets _interfere_ if one contains an exclusive capability `x` and the other also contains `x` or contains the read-only capability `x.rd`. Conversely, two capture sets are _separated_ if they don't interfere.]
255+
- Two capture sets _interfere_ if one contains an exclusive capability `x` and the other also contains `x` or contains the read-only capability `x.rd`. Conversely, two capture sets are _separated_ if their transitive capture sets don't interfere.
260256

261257
Separation checks are applied in the following scenarios:
262258

@@ -281,11 +277,11 @@ val r = Ref(1)
281277
val plusOne = r.set(r.get + 1)
282278
seq(plusOne, plusOne)
283279
```
284-
Without the explicit mention of parameter `f` in the capture set of parameter `g` of `seq` we'd get a separation error, since the capture sets of both arguments contain `r` and are therefore not separated.
280+
Without the explicit mention of parameter `f` in the capture set of parameter `g` of `seq` we'd get a separation error, since the transitive capture sets of both arguments contain `r` and are therefore not separated.
285281

286282
### Checking Statement Sequences
287283

288-
When a capability `x` is used at some point in a statement sequence, check that `x` does not appear in the hidden set of a previous definition.
284+
When a capability `x` is used at some point in a statement sequence, we check that `{x}` is separated from the hidden sets of all previous definitions.
289285

290286
Example:
291287
```scala
@@ -318,7 +314,7 @@ val b: (Ref^, Ref^) = (a, a) // error
318314
val c: (Ref^, Ref^{a}) = (a, a) // error
319315
val d: (Ref^{a}, Ref^{a}) = (a, a) // ok
320316
```
321-
Here, the definition of `b` is in error since the hidden sets of the two `^` in its type both contain `a`. Likewise, the definition of `c` is in error since the hidden set of the `^` in its type contains `a`, which is also part of a capture set somewhere else in the type. On the other hand, the definition of `d` is legal since there are no hidden sets to check.
317+
Here, the definition of `b` is in error since the hidden sets of the two `^`s in its type both contain `a`. Likewise, the definition of `c` is in error since the hidden set of the `^` in its type contains `a`, which is also part of a capture set somewhere else in the type. On the other hand, the definition of `d` is legal since there are no hidden sets to check.
322318

323319
### Checking Return Types
324320

@@ -332,13 +328,12 @@ def newRef(): Ref^ =
332328
val a = Ref(1)
333329
a
334330
```
335-
But the next line would cause a separation error:
331+
But the next definitions would cause a separation error:
336332
```scala
337333
val a = Ref(1)
338334
def newRef(): Ref^ = a // error
339335
```
340-
Here the rule is that the hidden set of a fresh cap in a return type cannot reference capabilities defined outside of the function. What about parameters?
341-
Here's another illegal version:
336+
The rule is that the hidden set of a fresh cap in a return type cannot reference exclusive or read-only capabilities defined outside of the function. What about parameters? Here's another illegal version:
342337
```scala
343338
def incr(a: Ref^): Ref^ =
344339
a.set(a.get + 1)
@@ -377,16 +372,16 @@ of a previous application.
377372

378373
Consume parameters enforce linear access to resources. This can be very useful. As an example, consider Scala's buffers such as `ListBuffer` or `ArrayBuffer`. We can treat these buffers as if they were purely functional, if we can enforce linear access.
379374

380-
We can define a function `linearAdd` that adds elements to buffers without violating referential transparency:
375+
For instance, we can define a function `linearAdd` that adds elements to buffers in-place without violating referential transparency:
381376
```scala
382377
def linearAdd[T](@consume buf: Buffer[T]^, elem: T): Buffer[T]^ =
383378
buf += elem
384379
```
385-
`linearAdd` returns a fresh buffer resulting from appending `elem` to `buf`. It overwrites `buf`, but that's OK since the `@consume` annotation on `buf `makes the argument is not used after the call.
380+
`linearAdd` returns a fresh buffer resulting from appending `elem` to `buf`. It overwrites `buf`, but that's OK since the `@consume` annotation on `buf` ensures that the argument is not used after the call.
386381

387382
### Consume Methods
388383

389-
Buffers in Scala's standard library use a single-argument method `+=` instead of a two argument global function. We can enforce linearity in this case by adding the `@consume` annotation to the method itself.
384+
Buffers in Scala's standard library use a single-argument method `+=` instead of a two argument global function like `linearAdd`. We can enforce linearity in this case by adding the `@consume` annotation to the method itself.
390385
```scala
391386
class Buffer[T] extends Mutable:
392387
@consume update def +=(x: T): Buffer[T]^ = this // ok
@@ -397,5 +392,5 @@ val b = Buffer[Int]() += 1 += 2
397392
val c = b += 3
398393
// b cannot be used from here
399394
```
400-
and this code is equivalent (but a lot more efficient) to functional append with `+`.
395+
This code is equivalent to functional append with `+`, and is at the same time more efficient since it re-uses the storage of the argument buffer.
401396

0 commit comments

Comments
 (0)