Skip to content

Commit 5ae3655

Browse files
committed
Fix #10129: Fix comparison of applied type aliases
Given type F[Xs] = ... F[As] <: F[Bs] Do we dealias `F` or check the arguments directly? It turns out that neither choice is correct in all instances. Checking the arguments directly is necessary for hk type inference, but may narrow the constraint too much. The solution is to go to an either that tries both options.
1 parent 6033fce commit 5ae3655

File tree

2 files changed

+63
-4
lines changed

2 files changed

+63
-4
lines changed

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

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -971,7 +971,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
971971
*/
972972
def isMatchingApply(tp1: Type): Boolean = tp1 match {
973973
case tp1 as AppliedType(tycon1, args1) =>
974-
// We intentionally do not dealias `tycon1` or `tycon2` here.
974+
// We intentionally do not automatically dealias `tycon1` or `tycon2` here.
975975
// `TypeApplications#appliedTo` already takes care of dealiasing type
976976
// constructors when this can be done without affecting type
977977
// inference, doing it here would not only prevent code from compiling
@@ -997,6 +997,25 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
997997
// ?F := [X] =>> (Int, X)
998998
//
999999
// Which is not what we wanted!
1000+
// On the other hand, we are not allowed to stop at the present arguments either.
1001+
// An example is i10129.scala. Here, we have the following situation:
1002+
//
1003+
// type Lifted[A] = Err | A
1004+
// def point[O](o: O): Lifted[O] = o
1005+
// extension [O, U](o: Lifted[O]) def map(f: O => U): Lifted[U] = ???
1006+
// point("a").map(_ => if true then 1 else error)
1007+
//
1008+
// This leads to the constraint Lifted[U] <: Lifted[Int]. If we just
1009+
// check the arguments this gives `U <: Int`. But this is wrong. Dealiasing
1010+
// `Lifted` gives `Err | U <: Err | Int`, hence it should be `U <: Err | Int`.
1011+
//
1012+
// So it's a conundrum. We need to check the immediate arguments for hk type inference,
1013+
// but this could narrow the constraint too much. The solution is to do an
1014+
// either with two alternatives when encountering an applied type alis.
1015+
// The first alternative checks the arguments, the second alternative checks the
1016+
// dealiased types. We pick the alternative that constrains least, or if there
1017+
// is no winner, the first one. We are forced to use a `sufficientEither`
1018+
// in the comparison, which is still not quite right. See the comment below.
10001019
def loop(tycon1: Type, args1: List[Type]): Boolean = tycon1 match {
10011020
case tycon1: TypeParamRef =>
10021021
(tycon1 == tycon2 ||
@@ -1039,9 +1058,35 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
10391058
(tycon1sym.isClass || tycon2sym.isClass)
10401059
&& (!touchedGADTs || gadtIsInstantiated)
10411060

1042-
inFrozenGadtIf(!tyconIsInjective) {
1043-
isSubArgs(args1, args2, tp1, tparams)
1044-
}
1061+
def checkArgs(): Boolean =
1062+
if tycon1sym == tycon2sym && tycon1sym.isAliasType then
1063+
// if types are simple forwarding aliases then try the dealiased types
1064+
tp1.superType match
1065+
case tp1a as AppliedType(_, args1a) if args1a.eqElements(args1) =>
1066+
tp2.superType match
1067+
case tp2a as AppliedType(_, args2a) if args2a.eqElements(args2) =>
1068+
return recur(tp1a, tp2a)
1069+
case _ =>
1070+
case _ =>
1071+
// Otherwise either compare arguments or compare dealiased types.
1072+
// Note: This test should be done with an `either`, but this fails
1073+
// for hk-alias-unification.scala The problem is not GADT boundschecking;
1074+
// that is taken care of by the outer inFrozenGadtIf test. The problem is
1075+
// that hk-alias-unification.scala relies on the right isSubArgs being performed
1076+
// when constraining the result type of a method. But there we are only allowed
1077+
// to use a necessaryEither, since it might be that an implicit conversion will
1078+
// be inserted on the result. So strictly speaking by going to a sufficientEither
1079+
// we overshoot and narrow the constraint unnecessarily. On the other hand, this
1080+
// is probably an extreme corner case. If we exclude implicit conversions, the
1081+
// problem goes away since in that case we would have a normal subtype test here,
1082+
// which demands a sufficientEither anyway.
1083+
sufficientEither(
1084+
isSubArgs(args1, args2, tp1, tparams),
1085+
recur(tp1.superType, tp2.superType))
1086+
else
1087+
isSubArgs(args1, args2, tp1, tparams)
1088+
1089+
inFrozenGadtIf(!tyconIsInjective)(checkArgs())
10451090
}
10461091
if (res && touchedGADTs) GADTused = true
10471092
res

tests/pos/i10129.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
class Err
2+
3+
type Lifted[A] = Err | A
4+
5+
def point[O](o: O): Lifted[O] = o
6+
extension [O, U](o: Lifted[O]) def map(f: O => U): Lifted[U] = ???
7+
8+
val error: Err = Err()
9+
10+
def ok: Int | Err =
11+
point("a").map(_ => if true then 1 else error)
12+
13+
def fail: Lifted[Int] =
14+
point("a").map(_ => if true then 1 else error) // error

0 commit comments

Comments
 (0)