Skip to content

Commit 17b2315

Browse files
committed
Finish separation checking section
1 parent 04a496b commit 17b2315

File tree

1 file changed

+163
-10
lines changed

1 file changed

+163
-10
lines changed

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

Lines changed: 163 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,7 @@ In fact, only a single character was added: `c`'s type now carries a universal c
4040

4141
So, effectively, anything that can be updated must be unaliased.
4242

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ᵢ`.
4443

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.
5244

5345
## Capability Kinds
5446

@@ -200,7 +192,7 @@ A read-only access charges the read-only capability `x.rd` to its environment. O
200192
Consider a reference `x` and two closures `f` and `g`.
201193

202194
```scala
203-
val x = Ref()
195+
val x = Ref(1)
204196
val f = () => x.get // f: () ->{x.rd} Unit
205197
val g = () => x.set(1) // g: () ->{x} Unit
206198
```
@@ -210,7 +202,7 @@ accesses an update method of `x`,so its capture set is `{x}`.
210202

211203
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:
212204
```scala
213-
val a: Ref^ = Ref()
205+
val a: Ref^ = Ref(1)
214206
val b1: Ref^{a.rd} = a
215207
val b2: Ref^{cap.rd} = a
216208
```
@@ -243,6 +235,167 @@ The subcapturing theory for sets is then as before, with the following additiona
243235
- `{x, ...}.RD = {x.rd, ...}.RD`
244236
- `{x.rd, ...} <: {x, ...}`
245237

238+
## Separation Checking
239+
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ᵢ`.
241+
242+
Here's an example:
243+
```scala
244+
val x: C^ = y
245+
... x ... // ok
246+
... y ... // error
247+
```
248+
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.
249+
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ᵢ`.
251+
252+
**Definitions:**
253+
254+
- 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`.
255+
256+
- The _transitive capture set_ `tcs(C)` of a capture set C is the union
257+
of `tcs(c)` for all elements `c` of `C`.
258+
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.]
260+
261+
Separation checks are applied in the following scenarios:
262+
263+
### Checking Applications
264+
265+
When checking a function application `f(e_1, ..., e_n)`, we instantiate each `cap` in a formal parameter of `f` to a fresh top capability and compare the argument types with these instantiated parameter types. We then check that the hidden set of each instantiated top capability for an argument `eᵢ` is separated from the capture sets of all the other arguments as well as from the capture sets of the function prefix and the function result. For instance a
266+
call to
267+
```scala
268+
multiply(a, b, a)
269+
```
270+
would be rejected since `a` appears in the hidden set of the last parameter of multiply, which has type `Matrix^` and also appears in the capture set of the
271+
first parameter.
272+
273+
We do not report a separation error between two sets if a formal parameter's capture set explicitly names a conflicting parameter. For instance, consider a method `seq` to apply two effectful function arguments in sequence. It can be declared as follows:
274+
```scala
275+
def seq(f: () => Unit; g: () ->{cap, f} Unit): Unit =
276+
f(); g()
277+
```
278+
Here, the `g` parameter explicitly mentions `f` in its potential capture set. This means that the `cap` in the same capture set would not need to hide the first argument, since it already appears explicitly in the same set. Consequently, we can pass the same function twice to `compose` without violating the separation criteria:
279+
```scala
280+
val r = Ref(1)
281+
val plusOne = r.set(r.get + 1)
282+
seq(plusOne, plusOne)
283+
```
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.
285+
286+
### Checking Statement Sequences
287+
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.
289+
290+
Example:
291+
```scala
292+
val a: Ref^ = Ref(1)
293+
val b: Ref^ = a
294+
val x = a.get // error
295+
```
296+
Here, the last line violates the separation criterion since it uses in `a.get` the capability `a`, which is hidden by the definition of `b`.
297+
Note that this check only applies when there are explicit top capabilities in play. One could very well write
298+
```scala
299+
val a: Ref^ = Ref(1)
300+
val b: Ref^{a} = a
301+
val x = a.get // ok
302+
```
303+
One can also drop the explicit type of `b` and leave it to be inferred. That would
304+
not cause a separation error either.
305+
```scala
306+
val a: Ref^ = Ref(0
307+
val b = a
308+
val x = a.get // ok
309+
```
310+
311+
### Checking Types
312+
313+
When a type contains top capabilities we check that their hidden sets don't interfere with other parts of the same type.
314+
315+
Example:
316+
```scala
317+
val b: (Ref^, Ref^) = (a, a) // error
318+
val c: (Ref^, Ref^{a}) = (a, a) // error
319+
val d: (Ref^{a}, Ref^{a}) = (a, a) // ok
320+
```
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.
322+
323+
### Checking Return Types
324+
325+
When a `cap` appears in the return type of a function it means a "fresh" top capability, different from what is known at the call site. Separation checking makes sure this is the case. For instance, the following is OK:
326+
```scala
327+
def newRef(): Ref^ = Ref(1)
328+
```
329+
And so is this:
330+
```scala
331+
def newRef(): Ref^ =
332+
val a = Ref(1)
333+
a
334+
```
335+
But the next line would cause a separation error:
336+
```scala
337+
val a = Ref(1)
338+
def newRef(): Ref^ = a // error
339+
```
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:
342+
```scala
343+
def incr(a: Ref^): Ref^ =
344+
a.set(a.get + 1)
345+
a
346+
```
347+
These needs to be rejected because otherwise we could have set up the following bad example:
348+
```scala
349+
val a = Ref(1)
350+
val b: Ref^ = incr(a)
351+
```
352+
Here, `b` aliases `a` but does not hide it. If we referred to `a` afterwards we would be surprised to see that the reference has now a value of 2.
353+
Therefore, parameters cannot appear in the hidden sets of fresh result caps either, at least not in general. An exception to this rule is described in the next section.
354+
355+
### Consume Parameters
246356

357+
Returning parameters in fresh result caps is safe if the actual argument to the parameter is not used afterwards. We can signal and enforce this pattern by adding a `@consume` annotation to a parameter. With that annotation, the following variant of `incr` is legal:
358+
```scala
359+
def incr(@consume a: Ref^): Ref^ =
360+
a.set(a.get + 1)
361+
a
362+
```
363+
Here, we increment the value of a reference and then return the same reference while enforcing the condition that the original reference cannot be used afterwards. Then the following is legal:
364+
```scala
365+
val a1 = Ref(1)
366+
val a2 = incr(a1)
367+
val a3 = incr(a2)
368+
println(a3)
369+
```
370+
Each reference `aᵢ` is unused after it is passed to `incr`. But the following continuation of that sequence would be in error:
371+
```scala
372+
val a4 = println(a2) // error
373+
val a5 = incr(a1) // error
374+
```
375+
In both of these assignments we use a capability that was consumed in an argument
376+
of a previous application.
247377

378+
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.
379+
380+
We can define a function `linearAdd` that adds elements to buffers without violating referential transparency:
381+
```scala
382+
def linearAdd[T](@consume buf: Buffer[T]^, elem: T): Buffer[T]^ =
383+
buf += elem
384+
```
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.
386+
387+
### Consume Methods
388+
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.
390+
```scala
391+
class Buffer[T] extends Mutable:
392+
@consume update def +=(x: T): Buffer[T]^ = this // ok
393+
```
394+
Then we can write
395+
```scala
396+
val b = Buffer[Int]() += 1 += 2
397+
val c = b += 3
398+
// b cannot be used from here
399+
```
400+
and this code is equivalent (but a lot more efficient) to functional append with `+`.
248401

0 commit comments

Comments
 (0)