You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -40,15 +40,7 @@ In fact, only a single character was added: `c`'s type now carries a universal c
40
40
41
41
So, effectively, anything that can be updated must be unaliased.
42
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
43
45
-
Here's an example:
46
-
```scala
47
-
valx: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
44
53
45
## Capability Kinds
54
46
@@ -200,7 +192,7 @@ A read-only access charges the read-only capability `x.rd` to its environment. O
200
192
Consider a reference `x` and two closures `f` and `g`.
201
193
202
194
```scala
203
-
valx=Ref()
195
+
valx=Ref(1)
204
196
valf= () => x.get // f: () ->{x.rd} Unit
205
197
valg= () => x.set(1) // g: () ->{x} Unit
206
198
```
@@ -210,7 +202,7 @@ accesses an update method of `x`,so its capture set is `{x}`.
210
202
211
203
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
204
```scala
213
-
vala:Ref^=Ref()
205
+
vala:Ref^=Ref(1)
214
206
valb1:Ref^{a.rd} = a
215
207
valb2:Ref^{cap.rd} = a
216
208
```
@@ -243,6 +235,167 @@ The subcapturing theory for sets is then as before, with the following additiona
243
235
-`{x, ...}.RD = {x.rd, ...}.RD`
244
236
-`{x.rd, ...} <: {x, ...}`
245
237
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
+
valx: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
+
defseq(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
+
valr=Ref(1)
281
+
valplusOne= 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
+
vala:Ref^=Ref(1)
293
+
valb:Ref^= a
294
+
valx= 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
+
vala:Ref^=Ref(1)
300
+
valb:Ref^{a} = a
301
+
valx= 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
+
vala:Ref^=Ref(0
307
+
valb= a
308
+
valx= 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
+
valb: (Ref^, Ref^) = (a, a) // error
318
+
valc: (Ref^, Ref^{a}) = (a, a) // error
319
+
vald: (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
+
defnewRef():Ref^=Ref(1)
328
+
```
329
+
And so is this:
330
+
```scala
331
+
defnewRef():Ref^=
332
+
vala=Ref(1)
333
+
a
334
+
```
335
+
But the next line would cause a separation error:
336
+
```scala
337
+
vala=Ref(1)
338
+
defnewRef():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
+
defincr(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
+
vala=Ref(1)
350
+
valb: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
246
356
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
+
defincr(@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
+
vala1=Ref(1)
366
+
vala2= incr(a1)
367
+
vala3= 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
+
vala4= println(a2) // error
373
+
vala5= 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.
247
377
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:
`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
+
classBuffer[T] extendsMutable:
392
+
@consume update def+=(x: T):Buffer[T]^=this// ok
393
+
```
394
+
Then we can write
395
+
```scala
396
+
valb=Buffer[Int]() +=1+=2
397
+
valc= 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 `+`.
0 commit comments