Skip to content

Commit acf9df6

Browse files
committed
Explain read-only consumes
- New section in separation checking doc page - Slight generalization of expectsReadOnly - Lots of tests variations of #24373
1 parent 3371325 commit acf9df6

File tree

12 files changed

+262
-16
lines changed

12 files changed

+262
-16
lines changed

compiler/src/dotty/tools/dotc/cc/Mutability.scala

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,9 @@ object Mutability:
105105
def expectsReadOnly(using Context): Boolean = tp match
106106
case tp: PathSelectionProto =>
107107
tp.selector.isReadOnlyMember || tp.selector.isMutableVar && tp.pt != LhsProto
108-
case _ => tp.isValueType && !tp.isMutableType
108+
case _ =>
109+
tp.isValueType
110+
&& (!tp.isMutableType || tp.captureSet.mutability == CaptureSet.Mutability.Reader)
109111

110112
extension (cs: CaptureSet)
111113
private def exclusivity(tp: Type)(using Context): Exclusivity =
@@ -118,11 +120,12 @@ object Mutability:
118120
* - the expected type is a value type that is not a mutable type,
119121
* - the expected type is a read-only selection
120122
*/
121-
def adjustReadOnly(pt: Type)(using Context): Capability =
123+
def adjustReadOnly(pt: Type)(using Context): Capability = {
122124
if ref.derivesFromMutable
123125
&& (pt.expectsReadOnly || ref.exclusivityInContext != Exclusivity.OK)
124126
then ref.readOnly
125127
else ref
128+
}.showing(i"Adjust RO $ref vs $pt = $result", capt)
126129

127130
/** Check that we can call an update method of `qualType` or perform an assignment
128131
* of a field of `qualType`.

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,38 @@ def linearAdd[T](consume buf: Buffer[T]^, elem: T): Buffer[T]^ =
188188
```
189189
`linearAdd` returns a fresh buffer resulting from appending `elem` to `buf`. It overwrites `buf`, but that's OK since the `consume` modifier on `buf` ensures that the argument is not used after the call.
190190

191+
### Consume Parameters and Read Accesses
192+
193+
A good way to conceptualize consume is to that it reserves the passed capability beyond the call with the consume parameter. In the previous `Buffer` example, if we do
194+
```scala
195+
val buf1 = linearAdd(buf, elem)
196+
```
197+
then the exclusive `buf` capability is reserved and therefore no further accesses to `buf` are possible. This means that `linearAdd` can safely overwrite `buf` by appending `elem` to it.
198+
199+
By the same token, when we pass a read-only capability to a consume parameter, it is
200+
only this capability that is reserved beyond the call. For instance, here is a
201+
method that consumes a read-only buffer:
202+
```scala
203+
def contents[T](consume buf: Buffer[T]): Int ->{buf.rd} T =
204+
i => buf(i)
205+
```
206+
The `contents` method takes a read-only buffer and turns it into a function that
207+
produces for each valid index the element of the buffer at that index. Passing a
208+
buffer to `contents` effectively freezes it: Since `buf.rd` is reserved, we cannot
209+
use the exclusive `buf` capability beyond the point of call, so no further appends
210+
are possible. On the other hand, it is possible to read the buffer, and it is also possible
211+
to consume that read capability again in further calls:
212+
```scala
213+
val buf = Buffer[String]()
214+
val buf1 = linearAdd(buf, "hi") // buf unavailable from here
215+
val c1 = contents(buf1) // only buf.rd is consumed
216+
val c2 = contents(buf1) // buf.rd can be consumed repeatedly
217+
```
218+
Note that the only difference between `linearAdd` and `contents` is that `linearAdd`'s consume parameter has type `Buffer[T]^` whereas the
219+
corresponding parameter in `contents` has type `Buffer[T]`. The first type expands
220+
to `Buffer[T]^{cap}` whereas the second expands to `Buffer[T]^{cap.rd}`.
221+
222+
191223
### Consume Methods
192224

193225
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` modifier to the method itself.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
-- Error: tests/neg-custom-args/captures/i24373.scala:28:2 -------------------------------------------------------------
2+
28 | x1.sink1 // error
3+
| ^^
4+
| Separation failure: Illegal access to (x1 : Foo^), which was passed to a
5+
| consume parameter or was used as a prefix to a consume method on line 27
6+
| and therefore is no longer available.
7+
|
8+
| where: ^ refers to a fresh root capability in the type of value x1
9+
-- Error: tests/neg-custom-args/captures/i24373.scala:32:2 -------------------------------------------------------------
10+
32 | x2.sink2 // error
11+
| ^^
12+
| Separation failure: Illegal access to (x2 : Bar^), which was passed to a
13+
| consume parameter or was used as a prefix to a consume method on line 31
14+
| and therefore is no longer available.
15+
|
16+
| where: ^ refers to a fresh root capability in the type of value x2
17+
-- Error: tests/neg-custom-args/captures/i24373.scala:49:8 -------------------------------------------------------------
18+
49 | sink6(x6) // error
19+
| ^^
20+
| Separation failure: Illegal access to (x6 : Bar^), which was passed to a
21+
| consume parameter or was used as a prefix to a consume method on line 48
22+
| and therefore is no longer available.
23+
|
24+
| where: ^ refers to a fresh root capability in the type of value x6
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
trait Foo:
2+
consume def sink1: Unit = ()
3+
4+
trait Bar extends Foo, caps.Mutable:
5+
consume def sink2: Unit = ()
6+
7+
class C extends caps.Mutable:
8+
def sink4(consume x: Foo^) = ()
9+
10+
object Foo:
11+
extension (consume self: Foo^)
12+
def sink: Unit = ()
13+
14+
def Test =
15+
16+
def sink3(consume self: Foo^): Unit = ()
17+
18+
def sink5(consume self: Bar): Unit = ()
19+
20+
def sink6(consume self: Bar^): Unit = ()
21+
22+
val x: Bar^ = new Bar {}
23+
x.sink
24+
x.sink // no error: rd/rd
25+
26+
val x1: Foo^ = new Foo {}
27+
x1.sink1
28+
x1.sink1 // error
29+
30+
val x2: Bar^ = new Bar {}
31+
x2.sink2
32+
x2.sink2 // error
33+
34+
val x3: Bar^ = new Bar {}
35+
sink3(x3)
36+
sink3(x3) // no error: rd/rd
37+
38+
val x4: Bar^ = new Bar {}
39+
val c = C()
40+
c.sink4(x4)
41+
c.sink4(x4) // no error: rd/rd
42+
43+
val x5: Bar^ = new Bar {}
44+
sink5(x5)
45+
sink5(x5) // no error: rd/rd
46+
47+
val x6: Bar^ = new Bar {}
48+
sink6(x6)
49+
sink6(x6) // error
50+
51+
52+
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
-- Error: tests/neg-custom-args/captures/i24373a.scala:15:8 ------------------------------------------------------------
2+
15 | sink2(x1) // error
3+
| ^^
4+
| Separation failure: Illegal access to (x1 : Bar^), which was passed to a
5+
| consume parameter or was used as a prefix to a consume method on line 13
6+
| and therefore is no longer available.
7+
|
8+
| where: ^ refers to a fresh root capability in the type of value x1
9+
-- Error: tests/neg-custom-args/captures/i24373a.scala:19:8 ------------------------------------------------------------
10+
19 | sink1(x2) // error
11+
| ^^
12+
| Separation failure: Illegal access to x2.rd, which was passed to a
13+
| consume parameter or was used as a prefix to a consume method on line 18
14+
| and therefore is no longer available.
15+
-- Error: tests/neg-custom-args/captures/i24373a.scala:20:8 ------------------------------------------------------------
16+
20 | sink2(x2) // error
17+
| ^^
18+
| Separation failure: Illegal access to (x2 : Bar^), which was passed to a
19+
| consume parameter or was used as a prefix to a consume method on line 18
20+
| and therefore is no longer available.
21+
|
22+
| where: ^ refers to a fresh root capability in the type of value x2
23+
-- Error: tests/neg-custom-args/captures/i24373a.scala:25:8 ------------------------------------------------------------
24+
25 | sink2(x3) // error
25+
| ^^
26+
| Separation failure: Illegal access to (x3 : Bar^), which was passed to a
27+
| consume parameter or was used as a prefix to a consume method on line 23
28+
| and therefore is no longer available.
29+
|
30+
| where: ^ refers to a fresh root capability in the type of value x3
31+
-- Error: tests/neg-custom-args/captures/i24373a.scala:29:8 ------------------------------------------------------------
32+
29 | sink3(x4) // error
33+
| ^^
34+
| Separation failure: Illegal access to x4.rd, which was passed to a
35+
| consume parameter or was used as a prefix to a consume method on line 28
36+
| and therefore is no longer available.
37+
-- Error: tests/neg-custom-args/captures/i24373a.scala:30:8 ------------------------------------------------------------
38+
30 | sink2(x4) // error
39+
| ^^
40+
| Separation failure: Illegal access to (x4 : Bar^), which was passed to a
41+
| consume parameter or was used as a prefix to a consume method on line 28
42+
| and therefore is no longer available.
43+
|
44+
| where: ^ refers to a fresh root capability in the type of value x4
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
trait Foo
2+
trait Bar extends Foo, caps.Mutable
3+
4+
def Test =
5+
6+
def sink1(consume self: Foo^): Unit = () // consumes read-only reference
7+
8+
def sink2(consume self: Bar^): Unit = () // consumes exclusive reference
9+
10+
def sink3(consume self: Bar): Unit = () // consumes read-only reference
11+
12+
val x1: Bar^ = new Bar {}
13+
sink1(x1)
14+
sink1(x1) // ok, rd/rd
15+
sink2(x1) // error
16+
17+
val x2: Bar^ = new Bar {}
18+
sink2(x2)
19+
sink1(x2) // error
20+
sink2(x2) // error
21+
22+
val x3: Bar^ = new Bar {}
23+
sink3(x3)
24+
sink3(x3) // ok, rd/rd
25+
sink2(x3) // error
26+
27+
val x4: Bar^ = new Bar {}
28+
sink2(x4)
29+
sink3(x4) // error
30+
sink2(x4) // error
31+
32+
33+
34+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- Error: tests/neg-custom-args/captures/i24373b.scala:3:18 ------------------------------------------------------------
2+
3 |trait Bar extends Foo, caps.Mutable // error
3+
| ^^^
4+
| illegal inheritance: trait Bar which extends `Mutable` is not allowed to also extend trait Foo
5+
| since trait Foo retains exclusive capabilities but does not extend `Mutable`.

tests/neg-custom-args/captures/linear-buffer.check

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,42 +33,48 @@
3333
| ^^^^^^^^^^^^^
3434
| Separation failure: method bar's result type BadBuffer[T]^ hides non-local this of class class BadBuffer.
3535
| The access must be in a consume method to allow this.
36-
-- Error: tests/neg-custom-args/captures/linear-buffer.scala:22:17 -----------------------------------------------------
37-
22 | val buf3 = app(buf, 3) // error
36+
-- Error: tests/neg-custom-args/captures/linear-buffer.scala:23:17 -----------------------------------------------------
37+
23 | val buf3 = app(buf, 3) // error
3838
| ^^^
3939
| Separation failure: Illegal access to (buf : Buffer[Int]^), which was passed to a
40-
| consume parameter or was used as a prefix to a consume method on line 20
40+
| consume parameter or was used as a prefix to a consume method on line 21
4141
| and therefore is no longer available.
4242
|
4343
| where: ^ refers to a fresh root capability in the type of parameter buf
44-
-- Error: tests/neg-custom-args/captures/linear-buffer.scala:29:17 -----------------------------------------------------
45-
29 | val buf3 = app(buf1, 4) // error
44+
-- Error: tests/neg-custom-args/captures/linear-buffer.scala:30:17 -----------------------------------------------------
45+
30 | val buf3 = app(buf1, 4) // error
4646
| ^^^^
4747
| Separation failure: Illegal access to (buf1 : Buffer[Int]^), which was passed to a
48-
| consume parameter or was used as a prefix to a consume method on line 27
48+
| consume parameter or was used as a prefix to a consume method on line 28
4949
| and therefore is no longer available.
5050
|
5151
| where: ^ refers to a fresh root capability in the type of value buf1
52-
-- Error: tests/neg-custom-args/captures/linear-buffer.scala:37:17 -----------------------------------------------------
53-
37 | val buf3 = app(buf1, 4) // error
52+
-- Error: tests/neg-custom-args/captures/linear-buffer.scala:38:17 -----------------------------------------------------
53+
38 | val buf3 = app(buf1, 4) // error
5454
| ^^^^
5555
| Separation failure: Illegal access to (buf1 : Buffer[Int]^), which was passed to a
56-
| consume parameter or was used as a prefix to a consume method on line 34
56+
| consume parameter or was used as a prefix to a consume method on line 35
5757
| and therefore is no longer available.
5858
|
5959
| where: ^ refers to a fresh root capability in the type of value buf1
60-
-- Error: tests/neg-custom-args/captures/linear-buffer.scala:47:17 -----------------------------------------------------
61-
47 | val buf3 = app(buf1, 4) // error
60+
-- Error: tests/neg-custom-args/captures/linear-buffer.scala:48:17 -----------------------------------------------------
61+
48 | val buf3 = app(buf1, 4) // error
6262
| ^^^^
6363
| Separation failure: Illegal access to (buf1 : Buffer[Int]^), which was passed to a
64-
| consume parameter or was used as a prefix to a consume method on line 42
64+
| consume parameter or was used as a prefix to a consume method on line 43
6565
| and therefore is no longer available.
6666
|
6767
| where: ^ refers to a fresh root capability in the type of value buf1
68-
-- Error: tests/neg-custom-args/captures/linear-buffer.scala:51:8 ------------------------------------------------------
69-
51 | app(buf, 1) // error
68+
-- Error: tests/neg-custom-args/captures/linear-buffer.scala:52:8 ------------------------------------------------------
69+
52 | app(buf, 1) // error
7070
| ^^^
7171
| Separation failure: (buf : Buffer[Int]^) appears in a loop, therefore it cannot
7272
| be passed to a consume parameter or be used as a prefix of a consume method call.
7373
|
7474
| where: ^ refers to a fresh root capability in the type of parameter buf
75+
-- Error: tests/neg-custom-args/captures/linear-buffer.scala:62:20 -----------------------------------------------------
76+
62 | val c3 = contents(buf) // error
77+
| ^^^
78+
| Separation failure: Illegal access to buf.rd, which was passed to a
79+
| consume parameter or was used as a prefix to a consume method on line 59
80+
| and therefore is no longer available.

tests/neg-custom-args/captures/linear-buffer.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class BadBuffer[T] extends Mutable:
1212

1313
class Buffer[T] extends Mutable:
1414
consume def append(x: T): Buffer[T]^ = this // ok
15+
def apply(i: Int): T = ???
1516

1617
def app[T](consume buf: Buffer[T]^, elem: T): Buffer[T]^ =
1718
buf.append(elem)
@@ -49,3 +50,14 @@ def Test4(consume buf: Buffer[Int]^) =
4950
def Test5(consume buf: Buffer[Int]^) =
5051
while true do
5152
app(buf, 1) // error
53+
54+
def contents[T](consume buf: Buffer[T]): Int ->{buf.rd} T =
55+
i => buf(i)
56+
57+
def Test6 =
58+
val buf = Buffer[String]()
59+
val buf1 = app(buf, "hi") // buf unavailable from here
60+
val c1 = contents(buf1) // only buf.rd is consumed
61+
val c2 = contents(buf1) // buf.rd can be consumed repeatedly
62+
val c3 = contents(buf) // error
63+

tests/neg/i24375.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def foo(f: (x: Int) ?=> Int) = f(using 0)
2+
@main def Main = println(foo(x))

0 commit comments

Comments
 (0)