Skip to content

Commit 5fb13aa

Browse files
committed
Don't dealias when matching type constructors
Not only can this prevent some constraints from being inferred, it can lead to the wrong constraints being inferred due to partial unification, see comment. This change broke one test, in i6565.scala we had: type Lifted[A] = Err | A extension [O, U](o: Lifted[O]) def map(f: O => U): Lifted[U] = ??? val error: Err = Err() lazy val ok: Lifted[String] = { point("a").map(_ => if true then "foo" else error) // now fails because error does not have type String } Because `isMatchingApply` can now deal with aliased types, when typing the lambda we get the constraint `U <: String` from `Lifted[U] <: Lifted[String]`, so `Err` is no longer a subtype of the expected result type of the lambda. This can be fixed in a number of ways: I changed the test to use `flatMap` instead of `map`, but one could also remove the expected type, or replace it by `Lifted[Lifted[String]` or `Err | String`. I think the new behavior is arguably better since using type aliases now gives you more control on how type inference proceeds, even if it means that some things that used to typecheck don't anymore. This change is also necessary to get shapeless to compile after the following commit which makes partial unification work in more situation.
1 parent 1fb2082 commit 5fb13aa

File tree

3 files changed

+50
-5
lines changed

3 files changed

+50
-5
lines changed

Diff for: compiler/src/dotty/tools/dotc/core/TypeComparer.scala

+24-2
Original file line numberDiff line numberDiff line change
@@ -860,13 +860,35 @@ class TypeComparer(using val comparerCtx: Context) extends ConstraintHandling wi
860860
*/
861861
def isMatchingApply(tp1: Type): Boolean = tp1 match {
862862
case AppliedType(tycon1, args1) =>
863-
def loop(tycon1: Type, args1: List[Type]): Boolean = tycon1.dealiasKeepRefiningAnnots match {
863+
// We intentionally do not dealias `tycon1` or `tycon2` here.
864+
// `TypeApplications#appliedTo` already takes care of dealiasing type
865+
// constructors when this can be done without affecting type
866+
// inference, doing it here would not only prevent code from compiling
867+
// but could also result in the wrong thing being inferred later, for example
868+
// in `tests/run/hk-alias-unification.scala` we end up checking:
869+
//
870+
// Foo[?F, ?T] <:< Foo[[X] =>> (X, String), Int]
871+
//
872+
// Naturally, we'd like to infer:
873+
//
874+
// ?F := [X] => (X, String)
875+
//
876+
// but if we dealias `Foo` then we'll end up trying to check:
877+
//
878+
// ErasedFoo[?F[?T]] <:< ErasedFoo[(Int, String)]
879+
//
880+
// Because of partial unification, this will succeed, but will produce the constraint:
881+
//
882+
// ?F := [X] =>> (Int, X)
883+
//
884+
// Which is not what we wanted!
885+
def loop(tycon1: Type, args1: List[Type]): Boolean = tycon1 match {
864886
case tycon1: TypeParamRef =>
865887
(tycon1 == tycon2 ||
866888
canConstrain(tycon1) && isSubType(tycon1, tycon2)) &&
867889
isSubArgs(args1, args2, tp1, tparams)
868890
case tycon1: TypeRef =>
869-
tycon2.dealiasKeepRefiningAnnots match {
891+
tycon2 match {
870892
case tycon2: TypeRef =>
871893
val tycon1sym = tycon1.symbol
872894
val tycon2sym = tycon2.symbol

Diff for: tests/pos/i6565.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ extension [O, U](o: Lifted[O]) def flatMap(f: O => Lifted[U]): Lifted[U] = ???
88

99
val error: Err = Err()
1010

11-
lazy val ok: Lifted[String] = { // ok despite map returning a union
12-
point("a").map(_ => if true then "foo" else error) // ok
11+
lazy val ok: Lifted[String] = {
12+
point("a").flatMap(_ => if true then "foo" else error)
1313
}
1414

1515
lazy val nowAlsoOK: Lifted[String] = {
16-
point("a").flatMap(_ => point("b").map(_ => if true then "foo" else error))
16+
point("a").flatMap(_ => point("b").flatMap(_ => if true then "foo" else error))
1717
}

Diff for: tests/run/hk-alias-unification.scala

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
trait Bla[T]
2+
object Bla {
3+
implicit def blaInt: Bla[Int] = new Bla[Int] {}
4+
implicit def blaString: Bla[String] = new Bla[String] {
5+
assert(false, "I should not be summoned!")
6+
}
7+
}
8+
9+
trait ErasedFoo[FT]
10+
object Test {
11+
type Foo[F[_], T] = ErasedFoo[F[T]]
12+
type Foo2[F[_], T] = Foo[F, T]
13+
14+
def mkFoo[F[_], T](implicit gen: Bla[T]): Foo[F, T] = new Foo[F, T] {}
15+
def mkFoo2[F[_], T](implicit gen: Bla[T]): Foo2[F, T] = new Foo2[F, T] {}
16+
17+
def main(args: Array[String]): Unit = {
18+
val a: Foo[[X] =>> (X, String), Int] = mkFoo
19+
val b: Foo2[[X] =>> (X, String), Int] = mkFoo
20+
val c: Foo[[X] =>> (X, String), Int] = mkFoo2
21+
}
22+
}
23+

0 commit comments

Comments
 (0)