Skip to content

Commit adbf280

Browse files
committed
Enhancement: Take purity of classes into account when capture checking
Two major changes: 1. During setup, don't generate a capture set variable for a type T if one of the base types of `T` is a pure class. 2. During subtype checking, admit `cs T <: T` if one of the base types of `T` is a pure class. The idea is that in this case we know that the capture set will always be empty at runtime. But don't do (2.) if we are checking self types. In this case the capture set cannot be stripped. Example: class A: this: A => class B extends A this: {c} B => In this case we should not assume `{c} B <: B` since we need to flag an error that the self type of B does not conform to the self type of A. are checking self types. In that cas
1 parent 8b3924c commit adbf280

File tree

10 files changed

+77
-40
lines changed

10 files changed

+77
-40
lines changed

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

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,16 +166,43 @@ extension (tp: Type)
166166
case CapturingType(_, _) => true
167167
case _ => false
168168

169+
/** Is type known to be always pure by its class structure,
170+
* so that adding a capture set to it would not make sense?
171+
*/
172+
def isAlwaysPure(using Context): Boolean = tp.dealias match
173+
case tp: (TypeRef | AppliedType) =>
174+
val sym = tp.typeSymbol
175+
if sym.isClass then sym.isPureClass
176+
else tp.superType.isAlwaysPure
177+
case CapturingType(parent, refs) =>
178+
parent.isAlwaysPure || refs.isAlwaysEmpty
179+
case tp: TypeProxy =>
180+
tp.superType.isAlwaysPure
181+
case tp: AndType =>
182+
tp.tp1.isAlwaysPure || tp.tp2.isAlwaysPure
183+
case tp: OrType =>
184+
tp.tp1.isAlwaysPure && tp.tp2.isAlwaysPure
185+
case _ =>
186+
false
187+
169188
extension (sym: Symbol)
170189

171-
/** A class is pure if one of its base types has an explicitly declared self type
172-
* with an empty capture set.
190+
/** A class is pure if:
191+
* - one its base types has an explicitly declared self type with an empty capture set
192+
* - or it is a value class
193+
* - or it is Nothing or Null
173194
*/
174195
def isPureClass(using Context): Boolean = sym match
175196
case cls: ClassSymbol =>
197+
val AnyValClass = defn.AnyValClass
176198
cls.baseClasses.exists(bc =>
177-
val selfType = bc.givenSelfType
178-
selfType.exists && selfType.captureSet.isAlwaysEmpty)
199+
bc == AnyValClass
200+
|| {
201+
val selfType = bc.givenSelfType
202+
selfType.exists && selfType.captureSet.isAlwaysEmpty
203+
})
204+
|| cls == defn.NothingClass
205+
|| cls == defn.NullClass
179206
case _ =>
180207
false
181208

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

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -120,38 +120,41 @@ extends tpd.TreeTraverser:
120120
case tp: (TypeRef | AppliedType) =>
121121
val sym = tp.typeSymbol
122122
if sym.isClass then
123-
tp.typeSymbol == defn.AnyClass
123+
sym == defn.AnyClass
124124
// we assume Any is a shorthand of {*} Any, so if Any is an upper
125125
// bound, the type is taken to be impure.
126126
else superTypeIsImpure(tp.superType)
127127
case tp: (RefinedOrRecType | MatchType) =>
128128
superTypeIsImpure(tp.underlying)
129129
case tp: AndType =>
130-
superTypeIsImpure(tp.tp1) || canHaveInferredCapture(tp.tp2)
130+
superTypeIsImpure(tp.tp1) || needsVariable(tp.tp2)
131131
case tp: OrType =>
132132
superTypeIsImpure(tp.tp1) && superTypeIsImpure(tp.tp2)
133133
case _ =>
134134
false
135135
}.showing(i"super type is impure $tp = $result", capt)
136136

137137
/** Should a capture set variable be added on type `tp`? */
138-
def canHaveInferredCapture(tp: Type): Boolean = {
138+
def needsVariable(tp: Type): Boolean = {
139139
tp.typeParams.isEmpty && tp.match
140140
case tp: (TypeRef | AppliedType) =>
141141
val tp1 = tp.dealias
142-
if tp1 ne tp then canHaveInferredCapture(tp1)
142+
if tp1 ne tp then needsVariable(tp1)
143143
else
144144
val sym = tp1.typeSymbol
145-
if sym.isClass then !sym.isValueClass && sym != defn.AnyClass
145+
if sym.isClass then
146+
!sym.isPureClass && sym != defn.AnyClass
146147
else superTypeIsImpure(tp1)
147148
case tp: (RefinedOrRecType | MatchType) =>
148-
canHaveInferredCapture(tp.underlying)
149+
needsVariable(tp.underlying)
149150
case tp: AndType =>
150-
canHaveInferredCapture(tp.tp1) && canHaveInferredCapture(tp.tp2)
151+
needsVariable(tp.tp1) && needsVariable(tp.tp2)
151152
case tp: OrType =>
152-
canHaveInferredCapture(tp.tp1) || canHaveInferredCapture(tp.tp2)
153-
case CapturingType(_, refs) =>
154-
refs.isConst && !refs.isUniversal
153+
needsVariable(tp.tp1) || needsVariable(tp.tp2)
154+
case CapturingType(parent, refs) =>
155+
needsVariable(parent)
156+
&& refs.isConst // if refs is a variable, no need to add another
157+
&& !refs.isUniversal // if refs is {*}, an added variable would not change anything
155158
case _ =>
156159
false
157160
}.showing(i"can have inferred capture $tp = $result", capt)
@@ -184,7 +187,7 @@ extends tpd.TreeTraverser:
184187
CapturingType(OrType(parent1, tp2, tp.isSoft), refs1, tp1.isBoxed)
185188
case tp @ OrType(tp1, tp2 @ CapturingType(parent2, refs2)) =>
186189
CapturingType(OrType(tp1, parent2, tp.isSoft), refs2, tp2.isBoxed)
187-
case _ if canHaveInferredCapture(tp) =>
190+
case _ if needsVariable(tp) =>
188191
val cs = tp.dealias match
189192
case CapturingType(_, refs) => CaptureSet.Var(refs.elems)
190193
case _ => CaptureSet.Var()

compiler/src/dotty/tools/dotc/core/CheckRealizable.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,15 +149,15 @@ class CheckRealizable(using Context) {
149149
*/
150150
private def boundsRealizability(tp: Type) = {
151151

152-
val memberProblems = withMode(Mode.CheckBounds) {
152+
val memberProblems = withMode(Mode.CheckBoundsOrSelfType) {
153153
for {
154154
mbr <- tp.nonClassTypeMembers
155155
if !(mbr.info.loBound <:< mbr.info.hiBound)
156156
}
157157
yield new HasProblemBounds(mbr.name, mbr.info)
158158
}
159159

160-
val refinementProblems = withMode(Mode.CheckBounds) {
160+
val refinementProblems = withMode(Mode.CheckBoundsOrSelfType) {
161161
for {
162162
name <- refinedNames(tp)
163163
if (name.isTypeName)

compiler/src/dotty/tools/dotc/core/Mode.scala

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,14 @@ object Mode {
7070
/** We are currently unpickling Scala2 info */
7171
val Scala2Unpickling: Mode = newMode(13, "Scala2Unpickling")
7272

73-
/** We are currently checking bounds to be non-empty, so we should not
74-
* do any widening when computing members of refined types.
73+
/** Signifies one of two possible situations:
74+
* 1. We are currently checking bounds to be non-empty, so we should not
75+
* do any widening when computing members of refined types.
76+
* 2. We are currently checking self type conformance, so we should not
77+
* ignore capture sets added to otherwise pure classes (only needed
78+
* for capture checking).
7579
*/
76-
val CheckBounds: Mode = newMode(14, "CheckBounds")
80+
val CheckBoundsOrSelfType: Mode = newMode(14, "CheckBoundsOrSelfType")
7781

7882
/** Use Scala2 scheme for overloading and implicit resolution */
7983
val OldOverloadingResolution: Mode = newMode(15, "OldOverloadingResolution")

compiler/src/dotty/tools/dotc/core/TypeComparer.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import typer.ProtoTypes.constrained
2323
import typer.Applications.productSelectorTypes
2424
import reporting.trace
2525
import annotation.constructorOnly
26-
import cc.{CapturingType, derivedCapturingType, CaptureSet, stripCapturing, isBoxedCapturing, boxed, boxedUnlessFun, boxedIfTypeParam}
26+
import cc.{CapturingType, derivedCapturingType, CaptureSet, stripCapturing, isBoxedCapturing, boxed, boxedUnlessFun, boxedIfTypeParam, isAlwaysPure}
2727

2828
/** Provides methods to compare types.
2929
*/
@@ -522,6 +522,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
522522
case CapturingType(parent1, refs1) =>
523523
if tp2.isAny then true
524524
else if subCaptures(refs1, tp2.captureSet, frozenConstraint).isOK && sameBoxed(tp1, tp2, refs1)
525+
|| !ctx.mode.is(Mode.CheckBoundsOrSelfType) && tp1.isAlwaysPure
525526
then recur(parent1, tp2)
526527
else thirdTry
527528
case tp1: AnnotatedType if !tp1.isRefining =>

compiler/src/dotty/tools/dotc/core/TypeOps.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@ object TypeOps:
615615
boundss: List[TypeBounds],
616616
instantiate: (Type, List[Type]) => Type,
617617
app: Type)(
618-
using Context): List[BoundsViolation] = withMode(Mode.CheckBounds) {
618+
using Context): List[BoundsViolation] = withMode(Mode.CheckBoundsOrSelfType) {
619619
val argTypes = args.tpes
620620

621621
/** Replace all wildcards in `tps` with `<app>#<tparam>` where `<tparam>` is the

compiler/src/dotty/tools/dotc/core/Types.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -775,11 +775,11 @@ object Types {
775775
val rinfo = tp.refinedInfo
776776
if (name.isTypeName && !pinfo.isInstanceOf[ClassInfo]) { // simplified case that runs more efficiently
777777
val jointInfo =
778-
if rinfo.isInstanceOf[TypeAlias] && !ctx.mode.is(Mode.CheckBounds) then
778+
if rinfo.isInstanceOf[TypeAlias] && !ctx.mode.is(Mode.CheckBoundsOrSelfType) then
779779
// In normal situations, the only way to "improve" on rinfo is to return an empty type bounds
780780
// So, we do not lose anything essential in "widening" to rinfo.
781781
// We need to compute the precise info only when checking for empty bounds
782-
// which is communicated by the CheckBounds mode.
782+
// which is communicated by the CheckBoundsOrSelfType mode.
783783
rinfo
784784
else if ctx.base.pendingMemberSearches.contains(name) then
785785
pinfo safe_& rinfo

compiler/src/dotty/tools/dotc/typer/RefChecks.scala

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -101,20 +101,22 @@ object RefChecks {
101101
* is the intersection of the capture sets of all its parents
102102
*/
103103
def checkSelfAgainstParents(cls: ClassSymbol, parents: List[Symbol])(using Context): Unit =
104-
val cinfo = cls.classInfo
105-
106-
def checkSelfConforms(other: ClassSymbol, category: String, relation: String) =
107-
val otherSelf = other.declaredSelfTypeAsSeenFrom(cls.thisType)
108-
if otherSelf.exists then
109-
if !(cinfo.selfType <:< otherSelf) then
110-
report.error(DoesNotConformToSelfType(category, cinfo.selfType, cls, otherSelf, relation, other),
111-
cls.srcPos)
112-
113-
for psym <- parents do
114-
checkSelfConforms(psym.asClass, "illegal inheritance", "parent")
115-
for reqd <- cls.asClass.givenSelfType.classSymbols do
116-
if reqd != cls then
117-
checkSelfConforms(reqd, "missing requirement", "required")
104+
withMode(Mode.CheckBoundsOrSelfType) {
105+
val cinfo = cls.classInfo
106+
107+
def checkSelfConforms(other: ClassSymbol, category: String, relation: String) =
108+
val otherSelf = other.declaredSelfTypeAsSeenFrom(cls.thisType)
109+
if otherSelf.exists then
110+
if !(cinfo.selfType <:< otherSelf) then
111+
report.error(DoesNotConformToSelfType(category, cinfo.selfType, cls, otherSelf, relation, other),
112+
cls.srcPos)
113+
114+
for psym <- parents do
115+
checkSelfConforms(psym.asClass, "illegal inheritance", "parent")
116+
for reqd <- cls.asClass.givenSelfType.classSymbols do
117+
if reqd != cls then
118+
checkSelfConforms(reqd, "missing requirement", "required")
119+
}
118120
end checkSelfAgainstParents
119121

120122
/** Check that self type of this class conforms to self types of parents

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:29:43 ------------------------------------------
1111
29 | val b = handle[Exception, () -> Nothing] { // error
1212
| ^
13-
| Found: ? (x: {*} CT[Exception]) -> {x} () -> ? Nothing
13+
| Found: ? (x: {*} CT[Exception]) -> {x} () -> Nothing
1414
| Required: CanThrow[Exception] => () -> Nothing
1515
30 | (x: CanThrow[Exception]) => () => raise(new Exception)(using x)
1616
31 | } {

tests/run-custom-args/captures/colltest5/Test_2.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ object Test {
7676
val x4 = xs.head
7777
val y4: Int = x4
7878
val x5 = xs.to(List)
79-
val y5: {x5} List[Int] = x5
79+
val y5: List[Int] = x5
8080
val (xs6, xs7) = xs.partition(isEven)
8181
val ys6: {xs6, isEven} View[Int] = xs6
8282
val ys7: {xs7, isEven} View[Int] = xs7

0 commit comments

Comments
 (0)