Skip to content

Commit f810de3

Browse files
committed
Handle existential types in subtyping
An existential type is represented as <typelambda> [ wildcard-args ] This commit makes subtyping between such types work as intended.
1 parent bc44444 commit f810de3

File tree

5 files changed

+139
-71
lines changed

5 files changed

+139
-71
lines changed

src/dotty/tools/dotc/core/ConstraintHandling.scala

Lines changed: 92 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ trait ConstraintHandling {
3232

3333
private var addConstraintInvocations = 0
3434

35-
private var needsCleanup = false
35+
/** Does `constraint` contain parameters that are instantiated but
36+
* that have not yet been checked for removal? In that case we should
37+
* remove them when we are at a top-level subtype check.
38+
*/
39+
private var hasInstantiations = false
3640

3741
/** If the constraint is frozen we cannot add new bounds to the constraint. */
3842
protected var frozenConstraint = false
@@ -115,7 +119,7 @@ trait ConstraintHandling {
115119
constr.println(s"unifying $p1 $p2")
116120
val down = constraint.exclusiveLower(p2, p1)
117121
val up = constraint.exclusiveUpper(p1, p2)
118-
needsCleanup = true
122+
hasInstantiations = true
119123
constraint = constraint.unify(p1, p2)
120124
val bounds = constraint.nonParamBounds(p1)
121125
val lo = bounds.lo
@@ -176,6 +180,83 @@ trait ConstraintHandling {
176180
inst
177181
}
178182

183+
/** The instance type of `param` in the current constraint (which contains `param`).
184+
* `fromBelow` is true, the isntance type is the lub of the parameter's
185+
* lower bounds; otherwise it is the glb of its upper bounds. However,
186+
* a lower bound instantiation can be a singleton type only if the upper bound
187+
* is also a singleton type.
188+
*/
189+
def instanceType(param: PolyParam, fromBelow: Boolean): Type = {
190+
def upperBound = ctx.typerState.constraint.fullUpperBound(param)
191+
def isSingleton(tp: Type): Boolean = tp match {
192+
case tp: SingletonType => true
193+
case AndType(tp1, tp2) => isSingleton(tp1) | isSingleton(tp2)
194+
case OrType(tp1, tp2) => isSingleton(tp1) & isSingleton(tp2)
195+
case _ => false
196+
}
197+
def isFullyDefined(tp: Type): Boolean = tp match {
198+
case tp: TypeVar => tp.isInstantiated && isFullyDefined(tp.instanceOpt)
199+
case tp: TypeProxy => isFullyDefined(tp.underlying)
200+
case tp: AndOrType => isFullyDefined(tp.tp1) && isFullyDefined(tp.tp2)
201+
case _ => true
202+
}
203+
def isOrType(tp: Type): Boolean = tp.stripTypeVar.dealias match {
204+
case tp: OrType => true
205+
case tp: RefinedOrRecType => isOrType(tp.parent)
206+
case AndType(tp1, tp2) => isOrType(tp1) | isOrType(tp2)
207+
case WildcardType(bounds: TypeBounds) => isOrType(bounds.hi)
208+
case _ => false
209+
}
210+
211+
// First, solve the constraint.
212+
var inst = approximation(param, fromBelow)
213+
214+
// Then, approximate by (1.) - (3.) and simplify as follows.
215+
// 1. If instance is from below and is a singleton type, yet
216+
// upper bound is not a singleton type, widen the instance.
217+
if (fromBelow && isSingleton(inst) && !isSingleton(upperBound))
218+
inst = inst.widen
219+
220+
inst = inst.simplified
221+
222+
// 2. If instance is from below and is a fully-defined union type, yet upper bound
223+
// is not a union type, approximate the union type from above by an intersection
224+
// of all common base types.
225+
if (fromBelow && isOrType(inst) && isFullyDefined(inst) && !isOrType(upperBound))
226+
inst = inst.approximateUnion
227+
228+
// 3. If instance is from below, and upper bound has open named parameters
229+
// make sure the instance has all named parameters of the bound.
230+
if (fromBelow) inst = inst.widenToNamedTypeParams(param.namedTypeParams)
231+
232+
if (ctx.typerState.isGlobalCommittable)
233+
assert(!inst.isInstanceOf[PolyParam], i"bad inst $this := $inst, constr = ${ctx.typerState.constraint}")
234+
// If this fails, you might want to turn on Config.debugCheckConstraintsClosed
235+
// to help find the root of the problem.
236+
inst
237+
}
238+
239+
def instantiateLambdaParam(param: PolyParam, variance: Int): Type = {
240+
val inst = instanceType(param, fromBelow = variance >= 0)
241+
hk.println(i"remove type lambda param $param with inst = $inst in $constraint")
242+
constraint = constraint.replace(param, inst, canRemove = true)
243+
inst
244+
}
245+
246+
/*
247+
def dropConstrainedLambdas() =
248+
if (hasConstrainedLambdas) {
249+
constraint.domainPolys.foreach {
250+
case tl: TypeLambda =>
251+
tl.paramRefs.foreach { param =>
252+
if (constraint.contains(param)) instantiateLambdaParam(param, 0)
253+
}
254+
case _ =>
255+
}
256+
hasConstrainedLambdas = false
257+
}
258+
*/
259+
179260
/** Constraint `c1` subsumes constraint `c2`, if under `c2` as constraint we have
180261
* for all poly params `p` defined in `c2` as `p >: L2 <: U2`:
181262
*
@@ -320,13 +401,21 @@ trait ConstraintHandling {
320401
constraint =
321402
if (addConstraint(param, tp, fromBelow = true) &&
322403
addConstraint(param, tp, fromBelow = false)) {
323-
needsCleanup = true
404+
hasInstantiations = true
324405
constraint.replace(param, tp, canRemove = false)
325406
}
326407
else saved
327408
constraint ne saved
328409
}
329410

411+
/** A new constraint with all polytypes that only have instantiated parameters removed */
412+
protected def cleanupConstraint() =
413+
if (hasInstantiations) {
414+
constraint =
415+
(constraint /: constraint.domainPolys.filter(constraint.isRemovable(_)))(_.remove(_))
416+
hasInstantiations = false
417+
}
418+
330419
/** Check that constraint is fully propagated. See comment in Config.checkConstraintsPropagated */
331420
def checkPropagated(msg: => String)(result: Boolean): Boolean = {
332421
if (Config.checkConstraintsPropagated && result && addConstraintInvocations == 0) {
@@ -346,12 +435,4 @@ trait ConstraintHandling {
346435
}
347436
result
348437
}
349-
350-
/** A new constraint with all polytypes that only have instantiated parameters removed */
351-
protected def cleanup(): Unit =
352-
if (needsCleanup) {
353-
constraint =
354-
(constraint /: constraint.domainPolys.filter(constraint.isRemovable(_)))(_.remove(_))
355-
needsCleanup = false
356-
}
357438
}

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import config.Config
1212
import config.Printers._
1313
import TypeErasure.{erasedLub, erasedGlb}
1414
import TypeApplications._
15+
import typer.ProtoTypes.constrained
1516
import scala.util.control.NonFatal
1617

1718
/** Provides methods to compare types.
@@ -102,7 +103,7 @@ class TypeComparer(initctx: Context) extends DotClass with ConstraintHandling {
102103
recCount = recCount - 1
103104
if (!result) constraint = saved
104105
else if (recCount == 0)
105-
cleanup()
106+
cleanupConstraint()
106107
if (needsGc) {
107108
state.gc()
108109
needsGc = false
@@ -587,6 +588,8 @@ class TypeComparer(initctx: Context) extends DotClass with ConstraintHandling {
587588
}
588589
case tycon1: TypeVar =>
589590
isMatchingApply(tycon1.underlying)
591+
case tycon1: LazyRef =>
592+
isMatchingApply(tycon1.underlying)
590593
case tycon1: AnnotatedType =>
591594
isMatchingApply(tycon1.underlying)
592595
case _ =>
@@ -656,8 +659,14 @@ class TypeComparer(initctx: Context) extends DotClass with ConstraintHandling {
656659
case tycon2: TypeRef =>
657660
isMatchingApply(tp1) ||
658661
compareLower(tycon2.info.bounds)
662+
case tycon2: TypeLambda =>
663+
val combined = constrained(mergeLambdaAndArgs(tycon2, args2))
664+
typr.println(i"new lambda: $combined")
665+
isSubType(tp1, combined.resultType)
659666
case tycon2: TypeVar =>
660667
isSubType(tp1, tycon2.underlying.appliedTo(args2))
668+
case tycon2: LazyRef =>
669+
compareHkApply2(tp1, tp2, tycon2.underlying, args2)
661670
case tycon2: AnnotatedType =>
662671
compareHkApply2(tp1, tp2, tycon2.underlying, args2)
663672
case _ =>
@@ -678,12 +687,24 @@ class TypeComparer(initctx: Context) extends DotClass with ConstraintHandling {
678687
}
679688
canConstrain(param1) && canInstantiate ||
680689
isSubType(bounds(param1).hi.applyIfParameterized(args1), tp2)
690+
case tycon1: TypeLambda =>
691+
val combined = mergeLambdaAndArgs(tycon1, args1)
692+
isSubType(combined.resultType, tp2)
681693
case tycon1: TypeProxy =>
682694
isSubType(tycon1.superType.applyIfParameterized(args1), tp2)
683695
case _ =>
684696
false
685697
}
686698

699+
/** A fresh type lambda like `tl`, but with bounds coming from `args` added to
700+
* the bounds of its type parameters.
701+
*/
702+
def mergeLambdaAndArgs(tl: TypeLambda, args: List[Type]): TypeLambda =
703+
tl.duplicate( // important to use duplicate here, as we need a fresh lambda in the constraint set.
704+
tl.paramNames,
705+
tl.paramBounds.zipWithConserve(args)((bounds, arg) => bounds & arg.bounds),
706+
tl.resType).asInstanceOf[TypeLambda]
707+
687708
/** Subtype test for corresponding arguments in `args1`, `args2` according to
688709
* variances in type parameters `tparams`.
689710
*/

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,11 @@ trait TypeOps { this: Context => // TODO: Make standalone object.
152152
tp2
153153
case tp1 => tp1
154154
}
155-
case tp: PolyParam =>
156-
typerState.constraint.typeVarOfParam(tp) orElse tp
155+
case param: PolyParam =>
156+
if (param.binder.isInstanceOf[TypeLambda] && typerState.constraint.contains(param))
157+
typeComparer.instantiateLambdaParam(param, if (theMap == null) 1 else theMap.currentVariance)
158+
else
159+
typerState.constraint.typeVarOfParam(param) orElse param
157160
case _: ThisType | _: BoundType | NoPrefix =>
158161
tp
159162
case tp: RefinedType =>
@@ -169,6 +172,7 @@ trait TypeOps { this: Context => // TODO: Make standalone object.
169172
}
170173

171174
class SimplifyMap extends TypeMap {
175+
def currentVariance: Int = variance
172176
def apply(tp: Type) = simplify(tp, this)
173177
}
174178

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

Lines changed: 3 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2870,63 +2870,9 @@ object Types {
28702870
tp
28712871
}
28722872

2873-
/** Instantiate variable from the constraints over its `origin`.
2874-
* If `fromBelow` is true, the variable is instantiated to the lub
2875-
* of its lower bounds in the current constraint; otherwise it is
2876-
* instantiated to the glb of its upper bounds. However, a lower bound
2877-
* instantiation can be a singleton type only if the upper bound
2878-
* is also a singleton type.
2879-
*/
2880-
def instantiate(fromBelow: Boolean)(implicit ctx: Context): Type = {
2881-
def upperBound = ctx.typerState.constraint.fullUpperBound(origin)
2882-
def isSingleton(tp: Type): Boolean = tp match {
2883-
case tp: SingletonType => true
2884-
case AndType(tp1, tp2) => isSingleton(tp1) | isSingleton(tp2)
2885-
case OrType(tp1, tp2) => isSingleton(tp1) & isSingleton(tp2)
2886-
case _ => false
2887-
}
2888-
def isFullyDefined(tp: Type): Boolean = tp match {
2889-
case tp: TypeVar => tp.isInstantiated && isFullyDefined(tp.instanceOpt)
2890-
case tp: TypeProxy => isFullyDefined(tp.underlying)
2891-
case tp: AndOrType => isFullyDefined(tp.tp1) && isFullyDefined(tp.tp2)
2892-
case _ => true
2893-
}
2894-
def isOrType(tp: Type): Boolean = tp.stripTypeVar.dealias match {
2895-
case tp: OrType => true
2896-
case tp: RefinedOrRecType => isOrType(tp.parent)
2897-
case AndType(tp1, tp2) => isOrType(tp1) | isOrType(tp2)
2898-
case WildcardType(bounds: TypeBounds) => isOrType(bounds.hi)
2899-
case _ => false
2900-
}
2901-
2902-
// First, solve the constraint.
2903-
var inst = ctx.typeComparer.approximation(origin, fromBelow)
2904-
2905-
// Then, approximate by (1.) - (3.) and simplify as follows.
2906-
// 1. If instance is from below and is a singleton type, yet
2907-
// upper bound is not a singleton type, widen the instance.
2908-
if (fromBelow && isSingleton(inst) && !isSingleton(upperBound))
2909-
inst = inst.widen
2910-
2911-
inst = inst.simplified
2912-
2913-
// 2. If instance is from below and is a fully-defined union type, yet upper bound
2914-
// is not a union type, approximate the union type from above by an intersection
2915-
// of all common base types.
2916-
if (fromBelow && isOrType(inst) && isFullyDefined(inst) && !isOrType(upperBound))
2917-
inst = inst.approximateUnion
2918-
2919-
// 3. If instance is from below, and upper bound has open named parameters
2920-
// make sure the instance has all named parameters of the bound.
2921-
if (fromBelow) inst = inst.widenToNamedTypeParams(this.namedTypeParams)
2922-
2923-
if (ctx.typerState.isGlobalCommittable)
2924-
assert(!inst.isInstanceOf[PolyParam], i"bad inst $this := $inst, constr = ${ctx.typerState.constraint}")
2925-
// If this fails, you might want to turn on Config.debugCheckConstraintsClosed
2926-
// to help find the root of the problem.
2927-
2928-
instantiateWith(inst)
2929-
}
2873+
/** Instantiate variable from the constraints over its `origin`. */
2874+
def instantiate(fromBelow: Boolean)(implicit ctx: Context): Type =
2875+
instantiateWith(ctx.typeComparer.instanceType(origin, fromBelow))
29302876

29312877
/** Unwrap to instance (if instantiated) or origin (if not), until result
29322878
* is no longer a TypeVar

tests/pos/existentials.scala

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
object TestList {
2+
3+
var x: ([X] -> List[List[X]])[_] = List(List(1))
4+
5+
var y: List[_] = x
6+
7+
}
8+
object TestSet {
9+
10+
class Set[S](x: S)
11+
12+
var x: ([Y] -> Set[Set[Y]])[_] = new Set(new Set("a"))
13+
14+
var y: Set[_] = x
15+
16+
}

0 commit comments

Comments
 (0)