Skip to content

Commit f6875d2

Browse files
committed
Generalize inferred types check
We now need to also check that fields with inferred types would not contribute a fresh capability to the implied capture set of the class.
1 parent 7c233bc commit f6875d2

File tree

8 files changed

+132
-46
lines changed

8 files changed

+132
-46
lines changed

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -378,10 +378,15 @@ object Capabilities:
378378
case tp: SetCapability => tp.captureSetOfInfo.isReadOnly
379379
case _ => this ne stripReadOnly
380380

381-
final def restriction(using Context): Symbol = this match
381+
/** The classifier, either given in an explicit `.only` or assumed for a
382+
* FreshCap. AnyRef for unclassified FreshCaps. Otherwise NoSymbol if no
383+
* classifier is given.
384+
*/
385+
final def classifier(using Context): Symbol = this match
382386
case Restricted(_, cls) => cls
383-
case ReadOnly(ref1) => ref1.restriction
384-
case Maybe(ref1) => ref1.restriction
387+
case ReadOnly(ref1) => ref1.classifier
388+
case Maybe(ref1) => ref1.classifier
389+
case self: FreshCap => self.hiddenSet.classifier
385390
case _ => NoSymbol
386391

387392
/** Is this a reach reference of the form `x*` or a readOnly or maybe variant
@@ -617,7 +622,7 @@ object Capabilities:
617622
case Reach(_) =>
618623
captureSetOfInfo.transClassifiers
619624
case self: CoreCapability =>
620-
if self.derivesFromCapability then toClassifiers(self.classifier)
625+
if self.derivesFromCapability then toClassifiers(self.inheritedClassifier)
621626
else captureSetOfInfo.transClassifiers
622627
if myClassifiers != UnknownClassifier then
623628
classifiersValid == currentId

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ extension (tp: Type)
487487
case _ =>
488488
tp
489489

490-
def classifier(using Context): ClassSymbol =
490+
def inheritedClassifier(using Context): ClassSymbol =
491491
tp.classSymbols.map(_.classifier).foldLeft(defn.AnyClass)(leastClassifier)
492492

493493
extension (tp: MethodType)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ object CapturingType:
4040
apply(parent1, refs ++ refs1, boxed)
4141
case _ =>
4242
if parent.derivesFromMutable then refs.associateWithMutable()
43-
refs.adoptClassifier(parent.classifier)
43+
refs.adoptClassifier(parent.inheritedClassifier)
4444
AnnotatedType(parent, CaptureAnnotation(refs, boxed)(defn.RetainsAnnot))
4545

4646
/** An extractor for CapturingTypes. Capturing types are recognized if

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

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -947,6 +947,17 @@ class CheckCaptures extends Recheck, SymTransformer:
947947
.showing(i"constr type $mt with $argTypes%, % in $constr = $result", capt)
948948
end refineConstructorInstance
949949

950+
/** If `mbr` is a field that has (possibly restricted) FreshCaps in its span capture set,
951+
* their classifiers, otherwise the empty list.
952+
*/
953+
private def classifiersOfFreshInType(mbr: Symbol)(using Context): List[ClassSymbol] =
954+
if contributesFreshToClass(mbr) then
955+
mbr.info.spanCaptureSet.elems
956+
.filter(_.isTerminalCapability)
957+
.toList
958+
.map(_.classifier.asClass)
959+
else Nil
960+
950961
/** The additional capture set implied by the capture sets of its fields. This
951962
* is either empty or, if some fields have a terminal capability in their span
952963
* capture sets, it consists of a single fresh cap that subsumes all these terminal
@@ -964,16 +975,9 @@ class CheckCaptures extends Recheck, SymTransformer:
964975
*/
965976
def impliedClassifiers(cls: Symbol): List[ClassSymbol] = cls match
966977
case cls: ClassSymbol =>
967-
var fieldClassifiers =
968-
for
969-
sym <- setup.fieldsWithExplicitTypes.getOrElse(cls, cls.info.decls.toList)
970-
if contributesFreshToClass(sym)
971-
case fresh: FreshCap <- sym.info.spanCaptureSet.elems
972-
.filter(_.isTerminalCapability)
973-
.map(_.stripReadOnly)
974-
.toList
975-
_ = pushInfo(i"Note: ${sym.showLocated} captures a $fresh")
976-
yield fresh.hiddenSet.classifier
978+
var fieldClassifiers = setup.fieldsWithExplicitTypes // pick fields with explicit types for classes in this compilation unit
979+
.getOrElse(cls, cls.info.decls.toList) // pick all symbols in class scope for other classes
980+
.flatMap(classifiersOfFreshInType)
977981
if cls.typeRef.isMutableType then
978982
fieldClassifiers = defn.Caps_Mutable :: fieldClassifiers
979983
val parentClassifiers =
@@ -1227,11 +1231,20 @@ class CheckCaptures extends Recheck, SymTransformer:
12271231
curEnv = saved
12281232
end recheckDefDef
12291233

1230-
/** If val or def definition with inferred (result) type is visible
1231-
* in other compilation units, check that the actual inferred type
1232-
* conforms to the expected type where all inferred capture sets are dropped.
1233-
* This ensures that if files compile separately, they will also compile
1234-
* in a joint compilation.
1234+
/** Two tests for member definitions with inferred types:
1235+
*
1236+
* 1. If val or def definition with inferred (result) type is visible
1237+
* in other compilation units, check that the actual inferred type
1238+
* conforms to the expected type where all inferred capture sets are dropped.
1239+
* This ensures that if files compile separately, they will also compile
1240+
* in a joint compilation.
1241+
* 2. If a val has an inferred type with a terminal capability in its span capset,
1242+
* check that it this capability is subsumed by the capset that was inferred
1243+
* for the class from its other fields via `captureSetImpliedByFields`.
1244+
* That capset is defined to take into account all fields but is computed
1245+
* only from fields with explicitly given types in order to avoid cycles.
1246+
* See comment on Setup.fieldsWithExplicitTypes. So we have to make sure
1247+
* that fields with inferred types would not change that capset.
12351248
*/
12361249
def checkInferredResult(tp: Type, tree: ValOrDefDef)(using Context): Type =
12371250
val sym = tree.symbol
@@ -1266,11 +1279,17 @@ class CheckCaptures extends Recheck, SymTransformer:
12661279
|The new inferred type $tp
12671280
|must conform to this type."""
12681281

1282+
def covers(classCapset: CaptureSet, fieldClassifiers: List[ClassSymbol]): Boolean =
1283+
fieldClassifiers.forall: cls =>
1284+
classCapset.elems.exists:
1285+
case fresh: FreshCap => cls.isSubClass(fresh.hiddenSet.classifier)
1286+
case _ => false
1287+
12691288
tree.tpt match
1270-
case tpt: InferredTypeTree if !isExemptFromChecks =>
1271-
if !sym.isLocalToCompilationUnit
1272-
// Symbols that can't be seen outside the compilation unit can have inferred types
1273-
// except for the else clause below.
1289+
case tpt: InferredTypeTree =>
1290+
// Test point (1) of doc comment above
1291+
if !sym.isLocalToCompilationUnit && !isExemptFromChecks
1292+
// Symbols that can't be seen outside the compilation unit can have inferred types
12741293
then
12751294
val expected = tpt.tpe.dropAllRetains
12761295
todoAtPostCheck += { () =>
@@ -1279,18 +1298,21 @@ class CheckCaptures extends Recheck, SymTransformer:
12791298
// The check that inferred <: expected is done after recheck so that it
12801299
// does not interfere with normal rechecking by constraining capture set variables.
12811300
}
1282-
else if sym.is(Private)
1283-
&& !sym.isLocalToCompilationUnitIgnoringPrivate
1284-
&& tree.tpt.nuType.spanCaptureSet.containsTerminalCapability
1301+
// Test point (2) of doc comment above
1302+
if sym.owner.isClass && !sym.owner.isStaticOwner
12851303
&& contributesFreshToClass(sym)
1286-
// Private symbols capturing a root capability need explicit types
1287-
// so that we can compute field constributions to class instance
1288-
// capture sets across compilation units.
12891304
then
1290-
report.error(
1291-
em"""$sym needs an explicit type because it captures a root capability in its type ${tree.tpt.nuType}.
1292-
|Fields of publicily accessible classes that capture a root capability need to be given an explicit type.""",
1293-
tpt.srcPos)
1305+
todoAtPostCheck += { () =>
1306+
val cls = sym.owner.asClass
1307+
val fieldClassifiers = classifiersOfFreshInType(sym)
1308+
val classCapset = captureSetImpliedByFields(cls, cls.appliedRef)
1309+
if !covers(classCapset, fieldClassifiers) then
1310+
report.error(
1311+
em"""$sym needs an explicit type because it captures a root capability in its type ${tree.tpt.nuType}.
1312+
|Fields capturing a root capability need to be given an explicit type unless the capability is already
1313+
|subsumed by the computed capability of the enclosing class.""",
1314+
tpt.srcPos)
1315+
}
12941316
case _ =>
12951317
tp
12961318
end checkInferredResult

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

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
-- Error: tests/neg-custom-args/captures/check-inferred.scala:12:19 ----------------------------------------------------
2-
12 | private val count = Ref() // error
3-
| ^
4-
| value count needs an explicit type because it captures a root capability in its type test.Ref^.
5-
| Fields of publicily accessible classes that capture a root capability need to be given an explicit type.
6-
|
7-
| where: ^ refers to a fresh root capability classified as Mutable in the type of value count
81
-- Error: tests/neg-custom-args/captures/check-inferred.scala:18:13 ----------------------------------------------------
92
18 | val incr = () => // error
103
| ^
@@ -24,7 +17,7 @@
2417
| Externally visible type: () -> Unit
2518
21 | count.put(count.get - 1)
2619
-- Error: tests/neg-custom-args/captures/check-inferred.scala:24:14 ----------------------------------------------------
27-
24 | val count = Ref(): Object^ // error
20+
24 | val count = Ref(): Object^ // error // error
2821
| ^^^^^^^^^^^^^^
2922
| value count needs an explicit type because the inferred type does not conform to
3023
| the type that is externally visible in other compilation units.
@@ -33,3 +26,36 @@
3326
| Externally visible type: Object
3427
|
3528
| where: ^ refers to a fresh root capability created in value count
29+
-- Error: tests/neg-custom-args/captures/check-inferred.scala:24:11 ----------------------------------------------------
30+
24 | val count = Ref(): Object^ // error // error
31+
| ^
32+
| value count needs an explicit type because it captures a root capability in its type Object^.
33+
| Fields capturing a root capability need to be given an explicit type unless the capability is already
34+
| subsumed by the computed capability of the enclosing class.
35+
|
36+
| where: ^ refers to a fresh root capability in the type of value count
37+
-- Error: tests/neg-custom-args/captures/check-inferred.scala:45:15 ----------------------------------------------------
38+
45 | private val y = ??? : A^ // error
39+
| ^
40+
| value y needs an explicit type because it captures a root capability in its type test.A^.
41+
| Fields capturing a root capability need to be given an explicit type unless the capability is already
42+
| subsumed by the computed capability of the enclosing class.
43+
|
44+
| where: ^ refers to a fresh root capability in the type of value y
45+
-- Error: tests/neg-custom-args/captures/check-inferred.scala:49:15 ----------------------------------------------------
46+
49 | private val y = ??? : (() => A^{cap.only[caps.Read]}) // error
47+
| ^
48+
| value y needs an explicit type because it captures a root capability in its type () => test.A^{cap.only[Read]}.
49+
| Fields capturing a root capability need to be given an explicit type unless the capability is already
50+
| subsumed by the computed capability of the enclosing class.
51+
|
52+
| where: => refers to a fresh root capability in the type of value y
53+
| cap is a fresh root capability created in value y
54+
-- Error: tests/neg-custom-args/captures/check-inferred.scala:29:21 ----------------------------------------------------
55+
29 | private val count = Ref() // error
56+
| ^
57+
| value count needs an explicit type because it captures a root capability in its type test.Ref^.
58+
| Fields capturing a root capability need to be given an explicit type unless the capability is already
59+
| subsumed by the computed capability of the enclosing class.
60+
|
61+
| where: ^ refers to a fresh root capability classified as Mutable in the type of value count

tests/neg-custom-args/captures/check-inferred.scala

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class Ref extends Mutable:
99
update def put(y: Int): Unit = x = y
1010

1111
class Counter:
12-
private val count = Ref() // error
12+
private val count = Ref()
1313
private val altCount: Ref^ = Ref() // ok
1414

1515
@untrackedCaptures
@@ -21,14 +21,30 @@ class Counter:
2121
count.put(count.get - 1)
2222

2323
trait CounterAPI:
24-
val count = Ref(): Object^ // error
24+
val count = Ref(): Object^ // error // error
2525
private def count2 = Ref() // ok
2626

2727
def test() =
2828
class Counter:
29-
private val count = Ref() // ok
29+
private val count = Ref() // error
3030
val incr = () =>
3131
count.put(count.get + 1)
3232
val decr = () =>
3333
count.put(count.get - 1)
3434

35+
class A:
36+
val x: A^{cap.only[caps.Control]} = ???
37+
private val y = ??? : A^{cap.only[caps.Control]} // ok
38+
39+
class B:
40+
val x: A^ = ???
41+
private val y = ??? : A^{cap.only[caps.Control]} // ok
42+
43+
class C:
44+
val x: A^{cap.only[caps.Control]} = ???
45+
private val y = ??? : A^ // error
46+
47+
class D:
48+
val x: A^{cap.only[caps.Control]} = ???
49+
private val y = ??? : (() => A^{cap.only[caps.Read]}) // error
50+

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,11 @@
77
| Note that capability C.this.c.io is not included in capture set {}.
88
|
99
| longer explanation available when compiling with `-explain`
10+
-- Error: tests/neg-custom-args/captures/i24335.scala:13:7 -------------------------------------------------------------
11+
13 | val r = Ref() // error
12+
| ^
13+
| value r needs an explicit type because it captures a root capability in its type Ref^.
14+
| Fields capturing a root capability need to be given an explicit type unless the capability is already
15+
| subsumed by the computed capability of the enclosing class.
16+
|
17+
| where: ^ refers to a fresh root capability classified as Mutable in the type of value r

tests/neg-custom-args/captures/i24335.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,13 @@ class C(val io: IO):
66
val l1 = () => c.io.write()
77
val _: () -> Unit = l1 // error
88

9+
class Ref extends caps.Mutable:
10+
var x: Int = 0
11+
12+
class D:
13+
val r = Ref() // error
14+
15+
def test =
16+
val d = D()
17+
val _: D^{} = d
918

0 commit comments

Comments
 (0)