Skip to content

Commit 29d01c2

Browse files
committed
Type inference for existentials
- Add existentials to inferred types - Map existentials in one compared type to existentials in the other - Also: Don't re-analyze existentials in mapCap.
1 parent c870c97 commit 29d01c2

File tree

8 files changed

+148
-53
lines changed

8 files changed

+148
-53
lines changed

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

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import printing.{Showable, Printer}
1414
import printing.Texts.*
1515
import util.{SimpleIdentitySet, Property}
1616
import typer.ErrorReporting.Addenda
17-
import TypeComparer.canSubsumeExistentially
17+
import TypeComparer.subsumesExistentially
1818
import util.common.alwaysTrue
1919
import scala.collection.mutable
2020
import CCState.*
@@ -173,7 +173,7 @@ sealed abstract class CaptureSet extends Showable:
173173
x.info match
174174
case x1: SingletonCaptureRef => x1.subsumes(y)
175175
case _ => false
176-
case x: TermParamRef => canSubsumeExistentially(x, y)
176+
case x: TermParamRef => subsumesExistentially(x, y)
177177
case _ => false
178178

179179
/** {x} <:< this where <:< is subcapturing, but treating all variables
@@ -498,10 +498,13 @@ object CaptureSet:
498498
deps = state.deps(this)
499499

500500
final def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult =
501-
if isConst || !recordElemsState() then
502-
CompareResult.Fail(this :: Nil) // fail if variable is solved or given VarState is frozen
501+
if isConst // Fail if variable is solved,
502+
|| !recordElemsState() // or given VarState is frozen,
503+
|| Existential.isBadExistential(elem) // or `elem` is an out-of-scope existential,
504+
then
505+
CompareResult.Fail(this :: Nil)
503506
else if !levelOK(elem) then
504-
CompareResult.LevelError(this, elem)
507+
CompareResult.LevelError(this, elem) // or `elem` is not visible at the level of the set.
505508
else
506509
//if id == 34 then assert(!elem.isUniversalRootCapability)
507510
assert(elem.isTrackableRef, elem)
@@ -694,19 +697,10 @@ object CaptureSet:
694697
if cond then propagate else CompareResult.OK
695698

696699
val mapped = extrapolateCaptureRef(elem, tm, variance)
700+
697701
def isFixpoint =
698702
mapped.isConst && mapped.elems.size == 1 && mapped.elems.contains(elem)
699703

700-
def addMapped =
701-
val added = mapped.elems.filter(!accountsFor(_))
702-
addNewElems(added)
703-
.andAlso:
704-
if mapped.isConst then CompareResult.OK
705-
else if mapped.asVar.recordDepsState() then { addAsDependentTo(mapped); CompareResult.OK }
706-
else CompareResult.Fail(this :: Nil)
707-
.andAlso:
708-
propagateIf(!added.isEmpty)
709-
710704
def failNoFixpoint =
711705
val reason =
712706
if variance <= 0 then i"the set's variance is $variance"
@@ -716,11 +710,14 @@ object CaptureSet:
716710
CompareResult.Fail(this :: Nil)
717711

718712
if origin eq source then // elements have to be mapped
719-
addMapped
713+
val added = mapped.elems.filter(!accountsFor(_))
714+
addNewElems(added)
720715
.andAlso:
721716
if mapped.isConst then CompareResult.OK
722717
else if mapped.asVar.recordDepsState() then { addAsDependentTo(mapped); CompareResult.OK }
723718
else CompareResult.Fail(this :: Nil)
719+
.andAlso:
720+
propagateIf(!added.isEmpty)
724721
else if accountsFor(elem) then
725722
CompareResult.OK
726723
else if variance > 0 then

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import StdNames.nme
99
import ast.tpd.*
1010
import Decorators.*
1111
import typer.ErrorReporting.errorType
12+
import Names.TermName
1213
import NameKinds.ExistentialBinderName
1314
import NameOps.isImpureFunction
1415
import reporting.Message
@@ -204,8 +205,10 @@ object Existential:
204205
case _ => None
205206

206207
/** Create method type in the refinement of an existential type */
207-
private def exMethodType(mk: TermParamRef => Type)(using Context): MethodType =
208-
val boundName = ExistentialBinderName.fresh()
208+
private def exMethodType(using Context)(
209+
mk: TermParamRef => Type,
210+
boundName: TermName = ExistentialBinderName.fresh()
211+
): MethodType =
209212
MethodType(boundName :: Nil)(
210213
mt => defn.Caps_Exists.typeRef :: Nil,
211214
mt => mk(mt.paramRefs.head))
@@ -332,6 +335,8 @@ object Existential:
332335
mapFunOrMethod(t, args, res)
333336
case CapturingType(parent, refs) =>
334337
t.derivedCapturingType(this(parent), refs)
338+
case Existential(_, _) =>
339+
t
335340
case t: (LazyRef | TypeVar) =>
336341
mapConserveSuper(t)
337342
case _ =>
@@ -348,4 +353,11 @@ object Existential:
348353
case ref: TermParamRef => isExistentialMethod(ref.binder)
349354
case _ => false
350355

356+
def isBadExistential(ref: CaptureRef) = ref match
357+
case ref: TermParamRef => ref.paramName == nme.OOS_EXISTENTIAL
358+
case _ => false
359+
360+
def badExistential(using Context): TermParamRef =
361+
exMethodType(identity, nme.OOS_EXISTENTIAL).paramRefs.head
362+
351363
end Existential

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,11 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
262262
end apply
263263
end mapInferred
264264

265-
try mapInferred(refine = true)(tp)
265+
try
266+
val tp1 = mapInferred(refine = true)(tp)
267+
val tp2 = Existential.mapCapInResults(_ => assert(false))(tp1)
268+
if tp2 ne tp then capt.println(i"expanded implicit in ${ctx.owner}: $tp --> $tp1 --> $tp2")
269+
tp2
266270
catch case ex: AssertionError =>
267271
println(i"error while mapping inferred $tp")
268272
throw ex
@@ -323,7 +327,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
323327

324328
val tp1 = toCapturing(tp)
325329
val tp2 = Existential.mapCapInResults(fail)(tp1)
326-
if tp2 ne tp then capt.println(i"expanded in ${ctx.owner}: $tp --> $tp1 --> $tp2")
330+
if tp2 ne tp then capt.println(i"expanded explicit in ${ctx.owner}: $tp --> $tp1 --> $tp2")
327331
tp2
328332
end transformExplicitType
329333

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ object StdNames {
294294
val EVT2U: N = "evt2u$"
295295
val EQEQ_LOCAL_VAR: N = "eqEqTemp$"
296296
val LAZY_FIELD_OFFSET: N = "OFFSET$"
297+
val OOS_EXISTENTIAL: N = "<out-of-scope-existential>"
297298
val OUTER: N = "$outer"
298299
val REFINE_CLASS: N = "<refinement>"
299300
val ROOTPKG: N = "_root_"

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

Lines changed: 96 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import collection.mutable
1111
import util.{Stats, NoSourcePosition, EqHashMap}
1212
import config.Config
1313
import config.Feature.{betterMatchTypeExtractorsEnabled, migrateTo3, sourceVersion}
14-
import config.Printers.{subtyping, gadts, matchTypes, noPrinter}
14+
import config.Printers.{subtyping, gadts, matchTypes, capt, noPrinter}
1515
import config.SourceVersion
1616
import TypeErasure.{erasedLub, erasedGlb}
1717
import TypeApplications.*
@@ -47,7 +47,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
4747
GADTused = false
4848
opaquesUsed = false
4949
openedExistentials = Nil
50-
assocExistentials = Map.empty
50+
assocExistentials = Nil
5151
recCount = 0
5252
needsGc = false
5353
if Config.checkTypeComparerReset then checkReset()
@@ -76,7 +76,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
7676
* Each existential gets mapped to the opened existentials to which it
7777
* may resolve at this point.
7878
*/
79-
private var assocExistentials: Map[TermParamRef, List[TermParamRef]] = Map.empty
79+
private var assocExistentials: ExAssoc = Nil
8080

8181
private var myInstance: TypeComparer = this
8282
def currentInstance: TypeComparer = myInstance
@@ -358,7 +358,6 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
358358
isMatchedByProto(tp2, tp1)
359359
case tp2: BoundType =>
360360
tp2 == tp1
361-
|| existentialVarsConform(tp1, tp2)
362361
|| secondTry
363362
case tp2: TypeVar =>
364363
recur(tp1, typeVarInstance(tp2))
@@ -2787,6 +2786,13 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
27872786
false
27882787
}
27892788

2789+
// ----------- Capture checking -----------------------------------------------
2790+
2791+
/** A type associating instantiatable existentials on the right of a comparison
2792+
* with the existentials they can be instantiated with.
2793+
*/
2794+
type ExAssoc = List[(TermParamRef, List[TermParamRef])]
2795+
27902796
private def compareExistentialLeft(boundVar: TermParamRef, tp1unpacked: Type, tp2: Type)(using Context): Boolean =
27912797
val saved = openedExistentials
27922798
try
@@ -2798,16 +2804,32 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
27982804
private def compareExistentialRight(tp1: Type, boundVar: TermParamRef, tp2unpacked: Type)(using Context): Boolean =
27992805
val saved = assocExistentials
28002806
try
2801-
assocExistentials = assocExistentials.updated(boundVar, openedExistentials)
2807+
assocExistentials = (boundVar, openedExistentials) :: assocExistentials
28022808
recur(tp1, tp2unpacked)
28032809
finally
28042810
assocExistentials = saved
28052811

2806-
def canSubsumeExistentially(tp1: TermParamRef, tp2: CaptureRef)(using Context): Boolean =
2807-
Existential.isExistentialVar(tp1)
2808-
&& assocExistentials.get(tp1).match
2809-
case Some(xs) => !Existential.isExistentialVar(tp2) || xs.contains(tp2)
2810-
case None => false
2812+
/** Is `tp1` an existential var that subsumes `tp2`? This is the case if `tp1` is
2813+
* instantiatable (i.e. it's a key in `assocExistentials`) and one of the
2814+
* following is true:
2815+
* - `tp2` is not an existential var,
2816+
* - `tp1` is associated via `assocExistentials` with `tp2`,
2817+
* - `tp2` appears as key in `assocExistentials` further out than `tp1`.
2818+
* The third condition allows to instantiate c2 to c1 in
2819+
* EX c1: A -> Ex c2. B
2820+
*/
2821+
def subsumesExistentially(tp1: TermParamRef, tp2: CaptureRef)(using Context): Boolean =
2822+
def canInstantiateWith(assoc: ExAssoc): Boolean = assoc match
2823+
case (bv, bvs) :: assoc1 =>
2824+
if bv == tp1 then
2825+
!Existential.isExistentialVar(tp2)
2826+
|| bvs.contains(tp2)
2827+
|| assoc1.exists(_._1 == tp2)
2828+
else
2829+
canInstantiateWith(assoc1)
2830+
case Nil =>
2831+
false
2832+
Existential.isExistentialVar(tp1) && canInstantiateWith(assocExistentials)
28112833

28122834
/** Are tp1, tp2 termRefs that can be linked? This should never be called
28132835
* normally, since exietential variables appear only in capture sets
@@ -2817,16 +2839,70 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
28172839
private def existentialVarsConform(tp1: Type, tp2: Type) =
28182840
tp2 match
28192841
case tp2: TermParamRef => tp1 match
2820-
case tp1: CaptureRef => canSubsumeExistentially(tp2, tp1)
2842+
case tp1: CaptureRef => subsumesExistentially(tp2, tp1)
28212843
case _ => false
28222844
case _ => false
28232845

2846+
/** bi-map taking existentials to the left of a comparison to matching
2847+
* existentials on the right. This is not a bijection. However
2848+
* we have `forwards(backwards(bv)) == bv` for an existentially bound `bv`.
2849+
* That's enough to qualify as a BiTypeMap.
2850+
*/
2851+
private class MapExistentials(assoc: ExAssoc)(using Context) extends BiTypeMap:
2852+
2853+
private def bad(t: Type) =
2854+
Existential.badExistential
2855+
.showing(i"existential match not found for $t in $assoc", capt)
2856+
2857+
def apply(t: Type) = t match
2858+
case t: TermParamRef if Existential.isExistentialVar(t) =>
2859+
// Find outermost existential on the right that can be instantiated to `t`,
2860+
// or `badExistential` if none exists.
2861+
def findMapped(assoc: ExAssoc): CaptureRef = assoc match
2862+
case (bv, assocBvs) :: assoc1 =>
2863+
val outer = findMapped(assoc1)
2864+
if !Existential.isBadExistential(outer) then outer
2865+
else if assocBvs.contains(t) then bv
2866+
else bad(t)
2867+
case Nil =>
2868+
bad(t)
2869+
findMapped(assoc)
2870+
case _ =>
2871+
mapOver(t)
2872+
2873+
/** The inverse takes existentials on the right to the innermost existential
2874+
* on the left to which they can be instantiated.
2875+
*/
2876+
lazy val inverse = new BiTypeMap:
2877+
def apply(t: Type) = t match
2878+
case t: TermParamRef if Existential.isExistentialVar(t) =>
2879+
assoc.find(_._1 == t) match
2880+
case Some((_, bvs)) if bvs.nonEmpty => bvs.head
2881+
case _ => bad(t)
2882+
case _ =>
2883+
mapOver(t)
2884+
2885+
def inverse = MapExistentials.this
2886+
override def toString = "MapExistentials.inverse"
2887+
end inverse
2888+
end MapExistentials
2889+
28242890
protected def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult =
2825-
try refs1.subCaptures(refs2, frozen)
2891+
try
2892+
if assocExistentials.isEmpty then
2893+
refs1.subCaptures(refs2, frozen)
2894+
else
2895+
val mapped = refs1.map(MapExistentials(assocExistentials))
2896+
if mapped.elems.exists(Existential.isBadExistential)
2897+
then CaptureSet.CompareResult.Fail(refs2 :: Nil)
2898+
else subCapturesMapped(mapped, refs2, frozen)
28262899
catch case ex: AssertionError =>
28272900
println(i"fail while subCaptures $refs1 <:< $refs2")
28282901
throw ex
28292902

2903+
protected def subCapturesMapped(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult =
2904+
refs1.subCaptures(refs2, frozen)
2905+
28302906
/** Is the boxing status of tp1 and tp2 the same, or alternatively, is
28312907
* the capture sets `refs1` of `tp1` a subcapture of the empty set?
28322908
* In the latter case, boxing status does not matter.
@@ -3291,9 +3367,6 @@ object TypeComparer {
32913367
def lub(tp1: Type, tp2: Type, canConstrain: Boolean = false, isSoft: Boolean = true)(using Context): Type =
32923368
comparing(_.lub(tp1, tp2, canConstrain = canConstrain, isSoft = isSoft))
32933369

3294-
def canSubsumeExistentially(tp1: TermParamRef, tp2: CaptureRef)(using Context) =
3295-
comparing(_.canSubsumeExistentially(tp1, tp2))
3296-
32973370
/** The least upper bound of a list of types */
32983371
final def lub(tps: List[Type])(using Context): Type =
32993372
tps.foldLeft(defn.NothingType: Type)(lub(_,_))
@@ -3366,6 +3439,9 @@ object TypeComparer {
33663439

33673440
def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult =
33683441
comparing(_.subCaptures(refs1, refs2, frozen))
3442+
3443+
def subsumesExistentially(tp1: TermParamRef, tp2: CaptureRef)(using Context) =
3444+
comparing(_.subsumesExistentially(tp1, tp2))
33693445
}
33703446

33713447
object MatchReducer:
@@ -3881,5 +3957,10 @@ class ExplainingTypeComparer(initctx: Context, short: Boolean) extends TypeCompa
38813957
super.subCaptures(refs1, refs2, frozen)
38823958
}
38833959

3960+
override def subCapturesMapped(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult =
3961+
traceIndented(i"subcaptures mapped $refs1 <:< $refs2 ${if frozen then "frozen" else ""}") {
3962+
super.subCapturesMapped(refs1, refs2, frozen)
3963+
}
3964+
38843965
def lastTrace(header: String): String = header + { try b.toString finally b.clear() }
38853966
}

tests/neg-custom-args/captures/heal-tparam-cs.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ def main(io: Capp^, net: Capp^): Unit = {
1111
}
1212

1313
val test2: (c: Capp^) -> () => Unit =
14-
localCap { c => // should work
14+
localCap { c => // error
1515
(c1: Capp^) => () => { c1.use() }
1616
}
1717

1818
val test3: (c: Capp^{io}) -> () ->{io} Unit =
19-
localCap { c => // should work
19+
localCap { c => // error
2020
(c1: Capp^{io}) => () => { c1.use() }
2121
}
2222

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
8 | ps.map((x, y) => compose1(x, y)) // error // error
33
| ^
44
|reference (ps : List[(box A => A, box A => A)]) @reachCapability is not included in the allowed capture set {}
5-
|of an enclosing function literal with expected type ((box A ->{ps*} A, box A ->{ps*} A)) -> box (x$0: A^?) ->? A^?
5+
|of an enclosing function literal with expected type ((box A ->{ps*} A, box A ->{ps*} A)) -> box (x$0: A^?) ->? (ex$15: caps.Exists) -> A^?
66
-- Error: tests/neg-custom-args/captures/reaches2.scala:8:13 -----------------------------------------------------------
77
8 | ps.map((x, y) => compose1(x, y)) // error // error
88
| ^
99
|reference (ps : List[(box A => A, box A => A)]) @reachCapability is not included in the allowed capture set {}
10-
|of an enclosing function literal with expected type ((box A ->{ps*} A, box A ->{ps*} A)) -> box (x$0: A^?) ->? A^?
10+
|of an enclosing function literal with expected type ((box A ->{ps*} A, box A ->{ps*} A)) -> box (x$0: A^?) ->? (ex$15: caps.Exists) -> A^?

0 commit comments

Comments
 (0)