Skip to content

Commit 7dc761a

Browse files
committed
Canonicalize capture variable subtype comparisons
Fixes scala#22103 Subtype problems where at least one side is a type variable representing a capture variable are canonicalized to capturing type comparisons on the special `CapSet` for the universe capture sets. For example, `C <: CapSet^{C^}` becomes `CapSet^{C^} <: CapSet^{C^}`, and `A <: B` becomes `CapSet^{A^} <: CapSet^{B^}` if both `A` and `B` are capture variables.
1 parent bd699fc commit 7dc761a

File tree

3 files changed

+88
-6
lines changed

3 files changed

+88
-6
lines changed

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

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,43 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
298298
}
299299
}
300300

301+
//TODO move the following functions somewhere more appropriate
302+
/**
303+
* Is the type `tp` a `CapSet` type, i.e., a capture variable?
304+
*
305+
* @param tp The type to check
306+
* @param includeCapSet Whether to include the bare `CapSet` type itself in the check, false at the top level
307+
*/
308+
def isCapSet(tp: Type, includeCapSet: Boolean = false): Boolean = tp match {
309+
case tp: TypeRef => (includeCapSet && (tp.symbol eq defn.Caps_CapSet)) || {
310+
tp.underlying match
311+
case TypeBounds(lo, hi) => isCapSet(lo, true) && isCapSet(hi, true)
312+
case TypeAlias(alias) => isCapSet(alias) // TODO: test cases involving type aliases
313+
case _ => false
314+
}
315+
case tp: SingletonType => isCapSet(tp.underlying)
316+
case CapturingType(parent, _) => isCapSet(parent, true)
317+
case _ => false
318+
}
319+
320+
/**
321+
* Assumes that isCapSet(tp) is true.
322+
*/
323+
def canonicalizeToCapSet(tp: Type): Type = tp match
324+
case ct @ CapturingType(_,_) => ct
325+
case tp: TypeRef if tp.symbol eq defn.Caps_CapSet => CapturingType(tp, CaptureSet.empty)
326+
case tp: SingletonType => canonicalizeToCapSet(tp.underlying)
327+
case tp: CaptureRef => CapturingType(defn.Caps_CapSet.typeRef, CaptureSet(tp))
328+
329+
330+
/** In capture checking, implements the logic to compare type variables which represent
331+
* capture variables.
332+
*
333+
* Note: should only be called in a context where tp1 or tp2 is a type variable representing a capture variable.
334+
*/
335+
def tryHandleCaptureVars: Boolean =
336+
isCaptureCheckingOrSetup && isCapSet(tp1) && isCapSet(tp2) && recur(canonicalizeToCapSet(tp1), canonicalizeToCapSet(tp2)) // TODO: we could probably just call subcapturing right away here and terminate early
337+
301338
def firstTry: Boolean = tp2 match {
302339
case tp2: NamedType =>
303340
def compareNamed(tp1: Type, tp2: NamedType): Boolean =
@@ -346,6 +383,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
346383
&& isSubPrefix(tp1.prefix, tp2.prefix)
347384
&& tp1.signature == tp2.signature
348385
&& !(sym1.isClass && sym2.isClass) // class types don't subtype each other
386+
|| tryHandleCaptureVars
349387
|| thirdTryNamed(tp2)
350388
case _ =>
351389
secondTry
@@ -858,9 +896,10 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
858896
}
859897
compareTypeBounds
860898
case CapturingType(parent2, refs2) =>
861-
def compareCapturing =
899+
def compareCapturing: Boolean =
862900
val refs1 = tp1.captureSet
863901
try
902+
if tp1.isInstanceOf[TypeRef] && tryHandleCaptureVars then return true
864903
if refs1.isAlwaysEmpty then recur(tp1, parent2)
865904
else
866905
// The singletonOK branch is because we sometimes have a larger capture set in a singleton
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import language.experimental.captureChecking
2+
import caps.*
3+
4+
def test[C^] =
5+
val a: C = ???
6+
val b: CapSet^{C^} = a
7+
val c: C = b
8+
val d: CapSet^{C^, c} = a
9+
10+
// TODO: make "CapSet-ness" of type variables somehow contagious?
11+
// Then we don't have to spell out the bounds explicitly...
12+
def testTrans[C^, D >: CapSet <: C, E >: CapSet <: D, F >: C <: CapSet^] =
13+
val d1: D = ???
14+
val d2: CapSet^{D^} = d1
15+
val d3: D = d2
16+
val e1: E = ???
17+
val e2: CapSet^{E^} = e1
18+
val e3: E = e2
19+
val d4: D = e1
20+
val c1: C = d1
21+
val c2: C = e1
22+
val f1: F = c1
23+
val d_e_f1: CapSet^{D^,E^,F^} = d1
24+
val d_e_f2: CapSet^{D^,E^,F^} = e1
25+
val d_e_f3: CapSet^{D^,E^,F^} = f1
26+
val f2: F = d_e_f1
27+
val c3: C = d_e_f1 // error
28+
val c4: C = f1 // error
29+
val e4: E = f1 // error
30+
val e5: E = d1 // error
31+
32+
33+
trait A[+T]
34+
35+
trait B[-C]
36+
37+
def testCong[C^, D^] =
38+
val a: A[C] = ???
39+
val b: A[CapSet^{C^}] = a
40+
val c: A[CapSet^{D^}] = a // error
41+
val d: A[CapSet^{C^,D^}] = a
42+
val e: A[C] = d // error
43+
val f: B[C] = ???
44+
val g: B[CapSet^{C^}] = f
45+
val h: B[C] = g
46+
val i: B[CapSet^{C^,D^}] = h // error
47+
val j: B[C] = i

tests/pos-custom-args/captures/cc-poly-varargs.scala

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,4 @@ def either[T1, T2, Cap^](
1212
src2: Source[T2, Cap]^{Cap^}): Source[Either[T1, T2], Cap]^{Cap^} =
1313
val left = src1.transformValuesWith(Left(_))
1414
val right = src2.transformValuesWith(Right(_))
15-
race[Either[T1, T2], Cap](left, right)
16-
// Explicit type arguments are required here because the second argument
17-
// is inferred as `CapSet^{Cap^}` instead of `Cap`.
18-
// Although `CapSet^{Cap^}` subsumes `Cap` in terms of capture sets,
19-
// `Cap` is not a subtype of `CapSet^{Cap^}` in terms of subtyping.
15+
race(left, right)

0 commit comments

Comments
 (0)