Skip to content

Commit b709262

Browse files
authored
Canonicalize capture variable subtype comparisons (#22299)
Fixes #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. Supersedes #22183 and #22289. This solution is overall cleaner and does not require adding a new bit to the TypeComparer's ApproxState. TODOs/Issues/Questions: - [x] Fix extension method in test [cc-poly-varargs.scala](https://github.com/dotty-staging/dotty/blob/capture-subtyping-canon/tests/pos-custom-args/captures/cc-poly-varargs.scala). Currently causes an infinite regress. - [x] Fix the aftermath * tests/neg-custom-args/captures/lazylists-exceptions.scala * tests/neg-custom-args/captures/exceptions.scala * tests/neg-custom-args/captures/real-try.scala * tests/run-custom-args/captures/colltest5 - [x] Some negative cases in test [capture-vars-subtyping.scala](https://github.com/dotty-staging/dotty/blob/capture-subtyping-canon/tests/neg-custom-args/captures/capture-vars-subtyping.scala) pass: `D <: E` fails, but its canonicalized form `CapSet^{D^} <: CapSet^{E^}` now succeeds. Potential problem in the subcapturing implementation. - [x] ~Extend to intersection/unions `def f[C^, D^, E <: C | D, F <: C & D](...) = ...` etc.~ Lacking good uses cases, not planned right now. - [X] ~If we have `C^` declared in the current context, should there be a difference between `C` vs. `C^` for subsequent mentions? We currently do, but seems a bit too subtle for users.~ Will be addressed by a new scheme for declaring capture variables using context bounds.
2 parents 60e9048 + c3fcd7c commit b709262

File tree

5 files changed

+82
-18
lines changed

5 files changed

+82
-18
lines changed

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

+14-2
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ trait CaptureRef extends TypeProxy, ValueType:
108108
* TODO: Document cases with more comments.
109109
*/
110110
final def subsumes(y: CaptureRef)(using Context): Boolean =
111-
112111
def subsumingRefs(x: Type, y: Type): Boolean = x match
113112
case x: CaptureRef => y match
114113
case y: CaptureRef => x.subsumes(y)
@@ -119,6 +118,15 @@ trait CaptureRef extends TypeProxy, ValueType:
119118
case info: SingletonCaptureRef => test(info)
120119
case info: AndType => viaInfo(info.tp1)(test) || viaInfo(info.tp2)(test)
121120
case info: OrType => viaInfo(info.tp1)(test) && viaInfo(info.tp2)(test)
121+
case info @ CapturingType(_,_) if this.derivesFrom(defn.Caps_CapSet) =>
122+
/*
123+
If `this` is a capture set variable `C^`, then it is possible that it can be
124+
reached from term variables in a reachability chain through the context.
125+
For instance, in `def test[C^](src: Foo^{C^}) = { val x: Foo^{src} = src; val y: Foo^{x} = x; y }`
126+
we expect that `C^` subsumes `x` and `y` in the body of the method
127+
(cf. test case cc-poly-varargs.scala for a more involved example).
128+
*/
129+
test(info)
122130
case _ => false
123131

124132
(this eq y)
@@ -149,7 +157,11 @@ trait CaptureRef extends TypeProxy, ValueType:
149157
y.info match
150158
case TypeBounds(_, hi: CaptureRef) => this.subsumes(hi)
151159
case _ => y.captureSetOfInfo.elems.forall(this.subsumes)
152-
case CapturingType(parent, refs) if parent.derivesFrom(defn.Caps_CapSet) =>
160+
case CapturingType(parent, refs) if parent.derivesFrom(defn.Caps_CapSet) || this.derivesFrom(defn.Caps_CapSet) =>
161+
/* The second condition in the guard is for `this` being a `CapSet^{a,b...}` and etablishing a
162+
potential reachability chain through `y`'s capture to a binding with
163+
`this`'s capture set (cf. `CapturingType` case in `def viaInfo` above for more context).
164+
*/
153165
refs.elems.forall(this.subsumes)
154166
case _ => false
155167
|| this.match

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

+3-4
Original file line numberDiff line numberDiff line change
@@ -1085,10 +1085,9 @@ object CaptureSet:
10851085
tp.captureSet
10861086
case tp: TermParamRef =>
10871087
tp.captureSet
1088-
case _: TypeRef =>
1089-
empty
1090-
case _: TypeParamRef =>
1091-
empty
1088+
case tp: (TypeRef | TypeParamRef) =>
1089+
if tp.derivesFrom(defn.Caps_CapSet) then tp.captureSet
1090+
else empty
10921091
case CapturingType(parent, refs) =>
10931092
recur(parent) ++ refs
10941093
case tp @ AnnotatedType(parent, ann) if ann.hasSymbol(defn.ReachCapabilityAnnot) =>

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

+13-2
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,10 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
439439
if (recur(info1.alias, tp2)) return true
440440
if (tp1.prefix.isStable) return tryLiftedToThis1
441441
case _ =>
442-
if (tp1 eq NothingType) || isBottom(tp1) then return true
442+
if isCaptureVarComparison then
443+
return subCaptures(tp1.captureSet, tp2.captureSet, frozenConstraint).isOK
444+
if (tp1 eq NothingType) || isBottom(tp1) then
445+
return true
443446
}
444447
thirdTry
445448
case tp1: TypeParamRef =>
@@ -587,6 +590,9 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
587590
|| narrowGADTBounds(tp2, tp1, approx, isUpper = false))
588591
&& (isBottom(tp1) || GADTusage(tp2.symbol))
589592

593+
if isCaptureVarComparison then
594+
return subCaptures(tp1.captureSet, tp2.captureSet, frozenConstraint).isOK
595+
590596
isSubApproxHi(tp1, info2.lo) && (trustBounds || isSubApproxHi(tp1, info2.hi))
591597
|| compareGADT
592598
|| tryLiftedToThis2
@@ -858,7 +864,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
858864
}
859865
compareTypeBounds
860866
case CapturingType(parent2, refs2) =>
861-
def compareCapturing =
867+
def compareCapturing: Boolean =
862868
val refs1 = tp1.captureSet
863869
try
864870
if refs1.isAlwaysEmpty then recur(tp1, parent2)
@@ -1572,6 +1578,11 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
15721578
(tp2a ne tp2) && recur(tp1, tp2a) && { opaquesUsed = true; true }
15731579
}
15741580

1581+
def isCaptureVarComparison: Boolean =
1582+
isCaptureCheckingOrSetup
1583+
&& tp1.derivesFrom(defn.Caps_CapSet)
1584+
&& tp2.derivesFrom(defn.Caps_CapSet)
1585+
15751586
// begin recur
15761587
if tp2 eq NoType then false
15771588
else if tp1 eq tp2 then true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
val c5: CapSet^{C^} = e1
32+
33+
34+
trait A[+T]
35+
36+
trait B[-C]
37+
38+
def testCong[C^, D^] =
39+
val a: A[C] = ???
40+
val b: A[CapSet^{C^}] = a
41+
val c: A[CapSet^{D^}] = a // error
42+
val d: A[CapSet^{C^,D^}] = a
43+
val e: A[C] = d // error
44+
val f: B[C] = ???
45+
val g: B[CapSet^{C^}] = f
46+
val h: B[C] = g
47+
val i: B[CapSet^{C^,D^}] = h // error
48+
val j: B[C] = i
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
abstract class Source[+T, Cap^]:
2-
def transformValuesWith[U](f: (T -> U)^{Cap^}): Source[U, Cap]^{this, f} = ???
1+
abstract class Source[+T, Cap^]
32

4-
// TODO: The extension version of `transformValuesWith` doesn't work currently.
5-
// extension[T, Cap^](src: Source[T, Cap]^)
6-
// def transformValuesWith[U](f: (T -> U)^{Cap^}): Source[U, Cap]^{src, f} = ???
3+
extension[T, Cap^](src: Source[T, Cap]^)
4+
def transformValuesWith[U](f: (T -> U)^{Cap^}): Source[U, Cap]^{src, f} = ???
75

86
def race[T, Cap^](sources: Source[T, Cap]^{Cap^}*): Source[T, Cap]^{Cap^} = ???
97

@@ -12,8 +10,4 @@ def either[T1, T2, Cap^](
1210
src2: Source[T2, Cap]^{Cap^}): Source[Either[T1, T2], Cap]^{Cap^} =
1311
val left = src1.transformValuesWith(Left(_))
1412
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.
13+
race(left, right)

0 commit comments

Comments
 (0)