Skip to content

Commit 11ecbfb

Browse files
authored
Don't add cap when creating instances of capability classes. (#24256)
Don't automatically add cap when creating instances of capability classes. To make up for this, refine the scheme to determine an implied fresh cap from the class fields. Also: Fix spurious warnings about redundant capabilities.
2 parents 07d63c4 + c0505d0 commit 11ecbfb

File tree

17 files changed

+119
-105
lines changed

17 files changed

+119
-105
lines changed

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -961,7 +961,6 @@ object Capabilities:
961961
case UnapplyInstance(info: MethodType)
962962
case LocalInstance(restpe: Type)
963963
case NewInstance(tp: Type)
964-
case NewCapability(tp: Type)
965964
case LambdaExpected(respt: Type)
966965
case LambdaActual(restp: Type)
967966
case OverriddenType(member: Symbol)
@@ -992,9 +991,6 @@ object Capabilities:
992991
i" when instantiating expected result type $restpe of function literal"
993992
case NewInstance(tp) =>
994993
i" when constructing instance $tp"
995-
case NewCapability(tp) =>
996-
val kind = if tp.derivesFromMutable then "mutable" else "Capability instance"
997-
i" when constructing $kind $tp"
998994
case LambdaExpected(respt) =>
999995
i" when instantiating expected result type $respt of lambda"
1000996
case LambdaActual(restp: Type) =>

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

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -881,16 +881,12 @@ class CheckCaptures extends Recheck, SymTransformer:
881881
* annotations avoid problematic intersections of capture sets when those
882882
* parameters are selected.
883883
*
884-
* Second half: union of initial capture set and all capture sets of arguments
885-
* to tracked parameters. The initial capture set `initCs` is augmented with
886-
* a fresh cap if `core` extends Capability.
884+
* Second half: union of initial capture set, all capture sets of arguments
885+
* to tracked parameters, and the capture set implied by the fields of the class.
887886
*/
888887
def addParamArgRefinements(core: Type, initCs: CaptureSet): (Type, CaptureSet) =
889888
var refined: Type = core
890-
var allCaptures: CaptureSet =
891-
if core.derivesFromCapability
892-
then initCs ++ FreshCap(Origin.NewCapability(core)).singletonCaptureSet
893-
else initCs ++ impliedByFields(core)
889+
var allCaptures: CaptureSet = initCs ++ impliedByFields(core)
894890
for (getterName, argType) <- mt.paramNames.lazyZip(argTypes) do
895891
val getter = cls.info.member(getterName).suchThat(_.isRefiningParamAccessor).symbol
896892
if !getter.is(Private) && getter.hasTrackedParts then
@@ -917,28 +913,33 @@ class CheckCaptures extends Recheck, SymTransformer:
917913
/** The additional capture set implied by the capture sets of its fields. This
918914
* is either empty or, if some fields have a terminal capability in their span
919915
* capture sets, it consists of a single fresh cap that subsumes all these terminal
920-
* capabiltities. Class parameters are not counted.
916+
* capabiltities. Class parameters are not counted. If the type is a mutable type,
917+
* we add a fresh cap in any case -- this is because we can currently hide
918+
* mutability in array vals, an example is neg-customargs/captures/matrix.scala.
921919
*/
922920
def impliedByFields(core: Type): CaptureSet =
923921
var infos: List[String] = Nil
924922
def pushInfo(msg: => String) =
925923
if ctx.settings.YccVerbose.value then infos = msg :: infos
926924

927925
/** The classifiers of the fresh caps in the span capture sets of all fields
928-
* in the given class `cls`.
926+
* in the given class `cls`. Mutable types get at least a fresh classified
927+
* as mutable.
929928
*/
930929
def impliedClassifiers(cls: Symbol): List[ClassSymbol] = cls match
931930
case cls: ClassSymbol =>
932-
val fieldClassifiers =
931+
val fields = cls.info.decls.toList
932+
var fieldClassifiers =
933933
for
934-
sym <- cls.info.decls.toList
935-
if contributesFreshToClass(sym)
934+
sym <- fields if contributesFreshToClass(sym)
936935
case fresh: FreshCap <- sym.info.spanCaptureSet.elems
937936
.filter(_.isTerminalCapability)
938937
.map(_.stripReadOnly)
939938
.toList
940939
_ = pushInfo(i"Note: ${sym.showLocated} captures a $fresh")
941940
yield fresh.hiddenSet.classifier
941+
if cls.typeRef.isMutableType then
942+
fieldClassifiers = defn.Caps_Mutable :: fieldClassifiers
942943
val parentClassifiers =
943944
cls.parentSyms.map(impliedClassifiers).filter(_.nonEmpty)
944945
if fieldClassifiers.isEmpty && parentClassifiers.isEmpty

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

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
332332
if !tptToCheck.isEmpty then report.error(msg, tptToCheck.srcPos)
333333

334334
/** If C derives from Capability and we have a C^cs in source, we leave it as is
335-
* instead of expanding it to C^{cap.rd}^cs. We do this by stripping capability-generated
335+
* instead of expanding it to C^{cap}^cs. We do this by stripping capability-generated
336336
* universal capture sets from the parent of a CapturingType.
337337
*/
338338
def stripImpliedCaptureSet(tp: Type): Type = tp match
@@ -936,21 +936,24 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
936936
if others.accountsFor(ref) then
937937
report.warning(em"redundant capture: $dom already accounts for $ref", pos)
938938

939-
if ref.captureSetOfInfo.elems.isEmpty
940-
&& !ref.coreType.derivesFrom(defn.Caps_Capability)
941-
&& !ref.coreType.derivesFrom(defn.Caps_CapSet) then
942-
val deepStr = if ref.isReach then " deep" else ""
943-
report.error(em"$ref cannot be tracked since its$deepStr capture set is empty", pos)
944-
check(parent.captureSet, parent)
945-
946-
val others =
947-
for
948-
j <- 0 until retained.length if j != i
949-
r = retained(j)
950-
if !r.isTerminalCapability
951-
yield r
952-
val remaining = CaptureSet(others*)
953-
check(remaining, remaining)
939+
if !ref.coreType.derivesFrom(defn.Caps_Capability)
940+
// Capability classes don't have their implied capture set yet, so
941+
// they would be seen as pure
942+
&& !ref.coreType.derivesFrom(defn.Caps_CapSet)
943+
then
944+
if ref.captureSetOfInfo.elems.isEmpty then
945+
val deepStr = if ref.isReach then " deep" else ""
946+
report.error(em"$ref cannot be tracked since its$deepStr capture set is empty", pos)
947+
check(parent.captureSet, parent)
948+
949+
val others =
950+
for
951+
j <- 0 until retained.length if j != i
952+
r = retained(j)
953+
if !r.isTerminalCapability
954+
yield r
955+
val remaining = CaptureSet(others*)
956+
check(remaining, remaining)
954957
end for
955958
catch case ex: IllegalCaptureRef =>
956959
report.error(em"Illegal capture reference: ${ex.getMessage}", tpt.srcPos)

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,12 @@ This widening is called _avoidance_; it is not specific to capture checking but
219219

220220
## Capability Classes
221221

222-
Classes like `CanThrow` or `FileSystem` have the property that their values are always intended to be capabilities. We can make this intention explicit and save boilerplate by letting these classes extend the `SharedCapability` class defined in object `cap`.
222+
Classes like `CanThrow` or `FileSystem` have the property that their values are always intended to be capabilities. We can make this intention explicit and save boilerplate by letting these classes extend the `SharedCapability` class defined in object `caps`.
223+
224+
A type extending `SharedCapability` always comes with a capture set. If no capture set is given explicitly, we assume the capture set is `{cap}`.
225+
226+
This means we could equivalently express the `FileSystem` and `Logger` classes as follows:
223227

224-
The capture set of type extending `SharedCapability` is always `{cap}`. This means we could equivalently express the `FileSystem` and `Logger` classes as follows:
225228
```scala
226229
import caps.SharedCapability
227230

@@ -234,9 +237,10 @@ def test(using fs: FileSystem) =
234237
val l: Logger^{fs} = Logger()
235238
...
236239
```
237-
In this version, `FileSystem` is a capability class, which means that the `{cap}` capture set is implied on the parameters of `Logger` and `test`.
240+
In this version, `FileSystem` is a capability class, which means that the occurrences of `FileSystem` in the types of the parameters of `Logger` and `test` are implicitly expanded to `FileSystem^`. On the other hand, types like `FileSystem^{f}` or
241+
`FileSystem^{}` are kept as written.
238242

239-
Another, unrelated change in the version of the last example here is that the `FileSystem` capability is now passed as an implicit parameter. It is quite natural to model capabilities with implicit parameters since it greatly reduces the wiring overhead once multiple capabilities are in play.
243+
Another, unrelated change in the last version of the `Logger` example is that the `FileSystem` capability is now passed as an implicit parameter. It is quite natural to model capabilities with implicit parameters since it greatly reduces the wiring overhead once multiple capabilities are in play.
240244

241245
## Escape Checking
242246

tests/neg-custom-args/captures/boundary.check

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22
4 | boundary[AnyRef^]:
33
5 | l1 ?=> // error // error
44
| ^
5-
|Found: scala.util.boundary.Label[Object^'s1]^
6-
|Required: scala.util.boundary.Label[Object^²]^³
5+
| Found: scala.util.boundary.Label[Object^'s1]
6+
| Required: scala.util.boundary.Label[Object^]^²
77
|
8-
|Note that capability cap cannot be included in outer capture set 's1.
8+
| Note that capability cap cannot be included in outer capture set 's1.
99
|
10-
|where: ^ refers to a fresh root capability classified as Control created in value local when constructing Capability instance scala.util.boundary.Label[Object^'s1]
11-
| ^² and cap refer to the universal root capability
12-
| ^³ refers to a fresh root capability classified as Control in the type of value local
10+
| where: ^ and cap refer to the universal root capability
11+
| ^² refers to a fresh root capability classified as Control in the type of value local
1312
6 | boundary[Unit]: l2 ?=>
1413
7 | boundary.break(l2)(using l1) // error
1514
8 | ???

tests/neg-custom-args/captures/extending-cap-classes.check

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,5 @@
1-
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/extending-cap-classes.scala:7:15 -------------------------
2-
7 | val x2: C1 = new C2 // error
3-
| ^^^^^^
4-
|Found: C2^
5-
|Required: C1
6-
|
7-
|Note that capability cap is not included in capture set {}.
8-
|
9-
|where: ^ and cap refer to a fresh root capability classified as SharedCapability created in value x2 when constructing Capability instance C2
10-
|
11-
| longer explanation available when compiling with `-explain`
12-
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/extending-cap-classes.scala:8:15 -------------------------
13-
8 | val x3: C1 = new C3 // error
14-
| ^^^^^^
15-
|Found: C3^
16-
|Required: C1
17-
|
18-
|Note that capability cap is not included in capture set {}.
19-
|
20-
|where: ^ and cap refer to a fresh root capability classified as SharedCapability created in value x3 when constructing Capability instance C3
21-
|
22-
| longer explanation available when compiling with `-explain`
23-
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/extending-cap-classes.scala:13:15 ------------------------
24-
13 | val z2: C1 = y2 // error
1+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/extending-cap-classes.scala:14:15 ------------------------
2+
14 | val z2: C1 = y2 // error
253
| ^^
264
| Found: (y2 : C2)
275
| Required: C1

tests/neg-custom-args/captures/extending-cap-classes.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ class C3 extends C2
44

55
def test =
66
val x1: C1 = new C1
7-
val x2: C1 = new C2 // error
8-
val x3: C1 = new C3 // error
7+
val x2: C1 = new C2 // was error, now ok
8+
val x3: C1 = new C3 // was error, now ok
99

1010
val y2: C2 = new C2
1111
val y3: C3 = new C3
12+
val y2ok: C2^{} = new C2
1213

1314
val z2: C1 = y2 // error
15+
val z2ok: C1 = y2ok
1416

tests/neg-custom-args/captures/filevar.check

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,3 @@
1212
18 | o.log
1313
|
1414
| longer explanation available when compiling with `-explain`
15-
-- Warning: tests/neg-custom-args/captures/filevar.scala:11:55 ---------------------------------------------------------
16-
11 |def withFile[T](op: (l: caps.Capability) ?-> (f: File^{l}) => T): T =
17-
| ^
18-
| redundant capture: File already accounts for (l : scala.caps.Capability)

tests/neg-custom-args/captures/i15923.check

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,3 @@
2020
|where: => refers to a fresh root capability created in anonymous function of type (using lcap: scala.caps.Capability): Cap^{lcap} -> Id[Cap] when instantiating expected result type Cap^{lcap} ->{cap²} Id[Cap^'s13]^'s14 of function literal
2121
|
2222
| longer explanation available when compiling with `-explain`
23-
-- Warning: tests/neg-custom-args/captures/i15923.scala:21:56 ----------------------------------------------------------
24-
21 | def withCap[X](op: (lcap: caps.Capability) ?-> Cap^{lcap} => X): X = {
25-
| ^^^^
26-
| redundant capture: test2.Cap already accounts for (lcap : scala.caps.Capability)
27-
-- Warning: tests/neg-custom-args/captures/i15923.scala:6:54 -----------------------------------------------------------
28-
6 | def withCap[X](op: (lcap: caps.Capability) ?-> Cap^{lcap} => X): X = {
29-
| ^^^^
30-
| redundant capture: Cap already accounts for (lcap : scala.caps.Capability)

tests/neg-custom-args/captures/i21614.check

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,12 @@
1313
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:15:12 ---------------------------------------
1414
15 | files.map(new Logger(_)) // error, Q: can we improve the error message?
1515
| ^^^^^^^^^^^^^
16-
|Found: (_$1: File^'s1) ->{C} Logger{val f: File^{_$1}}^{cap, _$1}
16+
|Found: (_$1: File^'s1) ->{C} Logger{val f: File^{_$1}}^{_$1}
1717
|Required: File^{C} => Logger{val f: File^'s2}^'s3
1818
|
1919
|Note that capability C is not classified as trait SharedCapability, therefore it
2020
|cannot be included in capture set 's1 of parameter _$1 of SharedCapability elements.
2121
|
22-
|where: => refers to a fresh root capability created in method mkLoggers2 when checking argument to parameter f of method map
23-
| cap is a root capability associated with the result type of (_$1: File^'s1): Logger{val f: File^{_$1}}^{cap, _$1}
22+
|where: => refers to a fresh root capability created in method mkLoggers2 when checking argument to parameter f of method map
2423
|
2524
| longer explanation available when compiling with `-explain`

0 commit comments

Comments
 (0)