From 0ef4944d6032ecb93b9f58a17b6eedf8a30edd79 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 18 Jul 2023 11:12:49 +0200 Subject: [PATCH 1/6] Improve handling of AndTypes on the LHS of subtype comparisons Fixes #18226 Might fix some other reported issues with AndTypes as well. --- .../dotty/tools/dotc/core/TypeComparer.scala | 38 ++++++++++++++----- .../allow-deep-subtypes/i5877.scala | 6 +-- tests/pos/i18226.scala | 11 ++++++ 3 files changed, 43 insertions(+), 12 deletions(-) create mode 100644 tests/pos/i18226.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index db1bf85ade93..bc51106189ef 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -478,7 +478,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling tp2.isRef(AnyClass, skipRefined = false) || !tp1.evaluating && recur(tp1.ref, tp2) case AndType(tp11, tp12) => - if (tp11.stripTypeVar eq tp12.stripTypeVar) recur(tp11, tp2) + if tp11.stripTypeVar eq tp12.stripTypeVar then recur(tp11, tp2) else thirdTry case tp1 @ OrType(tp11, tp12) => compareAtoms(tp1, tp2) match @@ -898,8 +898,27 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling canWidenAbstract && acc(true, tp) - def tryBaseType(cls2: Symbol) = { - val base = nonExprBaseType(tp1, cls2).boxedIfTypeParam(tp1.typeSymbol) + def tryBaseType(cls2: Symbol) = + + def computeBase(tp: Type): Type = tp.widenDealias match + case tp @ AndType(tp1, tp2) => + // We have to treat AndTypes specially, since the normal treatment + // of `(T1 & T2).baseType(C)` combines the base types of T1 and T2 via glb + // which drops any types that don't exist. That forgets possible solutions. + // For instance, in i18266.scala, we get to a subgoal `R & Row[Int] <: Row[String]` + // where R is an uninstantiated type variable. The base type computation + // of the LHS drops the non-existing base type of R and results in + // `Row[Int]`, which leads to a subtype failure since `Row[Int] <: Row[String]` + // does not hold. The new strategy is to declare that the base type computation + // failed since R does not have a base type, and to proceed to fourthTry instead, + // where we try both sides of an AndType individually. + val b1 = computeBase(tp1) + val b2 = computeBase(tp2) + if b1.exists && b2.exists then tp.derivedAndType(b1, b2) else NoType + case _ => + nonExprBaseType(tp, cls2).boxedIfTypeParam(tp.typeSymbol) + + val base = computeBase(tp1) if base.exists && (base ne tp1) && (!caseLambda.exists || widenAbstractOKFor(tp2) @@ -912,7 +931,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // expands to a match type. In this case, we should try to reduce the type // and compare the redux. This is done in fourthTry else fourthTry - } + end tryBaseType def fourthTry: Boolean = tp1 match { case tp1: TypeRef => @@ -989,7 +1008,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling } } compareHKLambda - case AndType(tp11, tp12) => + case tp1 @ AndType(tp11, tp12) => val tp2a = tp2.dealiasKeepRefiningAnnots if (tp2a ne tp2) // Follow the alias; this might avoid truncating the search space in the either below return recur(tp1, tp2a) @@ -1009,8 +1028,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling return recur(AndType(tp11, tp121), tp2) && recur(AndType(tp11, tp122), tp2) case _ => } - val tp1norm = simplifyAndTypeWithFallback(tp11, tp12, tp1) - if (tp1 ne tp1norm) recur(tp1norm, tp2) + val tp1norm = trySimplify(tp1) + if tp1 ne tp1norm then recur(tp1norm, tp2) else either(recur(tp11, tp2), recur(tp12, tp2)) case tp1: MatchType => def compareMatch = tp2 match { @@ -2506,8 +2525,9 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling final def andType(tp1: Type, tp2: Type, isErased: Boolean = ctx.erasedTypes): Type = andTypeGen(tp1, tp2, AndType.balanced(_, _), isErased = isErased) - final def simplifyAndTypeWithFallback(tp1: Type, tp2: Type, fallback: Type): Type = - andTypeGen(tp1, tp2, (_, _) => fallback) + /** Try to simplify AndType, or return the type itself if no simplifiying opportunities exist. */ + private def trySimplify(tp: AndType): Type = + andTypeGen(tp.tp1, tp.tp2, (_, _) => tp) /** Form a normalized conjunction of two types. * Note: For certain types, `|` is distributed inside the type. This holds for diff --git a/tests/neg-custom-args/allow-deep-subtypes/i5877.scala b/tests/neg-custom-args/allow-deep-subtypes/i5877.scala index dcb89e1b565e..969e270fdaf8 100644 --- a/tests/neg-custom-args/allow-deep-subtypes/i5877.scala +++ b/tests/neg-custom-args/allow-deep-subtypes/i5877.scala @@ -20,12 +20,12 @@ object Main { def testHasThisType(): Unit = { def testSelf[PThis <: HasThisType[_ <: PThis]](that: HasThisType[PThis]): Unit = { - val thatSelf = that.self() + val thatSelf = that.self() // error: recursion limit exceeded // that.self().type <: that.This assert(implicitly[thatSelf.type <:< that.This] != null) } val that: HasThisType[_] = Foo() // null.asInstanceOf - testSelf(that) // error + testSelf(that) // error: recursion limit exceeded } @@ -36,7 +36,7 @@ object Main { } val that: HasThisType[_] = Foo() // null.asInstanceOf // this line of code makes Dotty compiler infinite recursion (stopped only by overflow) - comment it to make it compilable again - testSelf(that) // error + testSelf(that) // error: recursion limit exceeded } // ---- ---- ---- ---- diff --git a/tests/pos/i18226.scala b/tests/pos/i18226.scala new file mode 100644 index 000000000000..b583b94744a7 --- /dev/null +++ b/tests/pos/i18226.scala @@ -0,0 +1,11 @@ +trait F[-R] + +trait Row[A] + +def eliminateInt[R](f: F[R & Row[Int]]): F[R] = new F[R] {} + +val x = new F[Row[Int] & Row[String]] {} + +val _ = eliminateInt[Row[String]](x) // compiles OK when given explicit type +val y = eliminateInt(x) // was error +val _: F[Row[String]] = y \ No newline at end of file From 71dd329be2aa5b81d6a9c990b07b1795898685b7 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 18 Jul 2023 11:45:23 +0200 Subject: [PATCH 2/6] Test case #12077 --- tests/pos/i12077.scala | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 tests/pos/i12077.scala diff --git a/tests/pos/i12077.scala b/tests/pos/i12077.scala new file mode 100644 index 000000000000..bab233143c8b --- /dev/null +++ b/tests/pos/i12077.scala @@ -0,0 +1,7 @@ +trait Wrapper[K] +trait Has0[T] + +def test[R](v: Wrapper[Has0[String] with R]):R = ??? + +val zz:Wrapper[Has0[String] with Has0[Int]] = ??? +val _ = test(zz) From 3a252a7ec45393d99d3319252619892bb0db5d3c Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 18 Jul 2023 17:29:05 +0200 Subject: [PATCH 3/6] Fix new problem uncovered by AndType fix Fixes a new problem exhibited in the ZIO 1 code base that got uncovered by the previous fix to AndTypes. --- .../dotty/tools/dotc/core/TypeComparer.scala | 7 +++++-- .../allow-deep-subtypes/i5877.scala | 2 +- tests/pos/i18226a.scala | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 tests/pos/i18226a.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index bc51106189ef..e99adcf4945c 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -926,10 +926,13 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling then isSubType(base, tp2, if (tp1.isRef(cls2)) approx else approx.addLow) && recordGadtUsageIf { MatchType.thatReducesUsingGadt(tp1) } - || base.isInstanceOf[OrType] && fourthTry - // if base is a disjunction, this might have come from a tp1 type that + || base.isInstanceOf[AndOrType] && fourthTry + // If base is a disjunction, this might have come from a tp1 type that // expands to a match type. In this case, we should try to reduce the type // and compare the redux. This is done in fourthTry + // If base is a conjunction, it could be that one of the original + // branches of the AndType tp1 conforms to tp2, but its base type does + // not. So we need to also fall back to fourthTry. Test case is i18226a.scala. else fourthTry end tryBaseType diff --git a/tests/neg-custom-args/allow-deep-subtypes/i5877.scala b/tests/neg-custom-args/allow-deep-subtypes/i5877.scala index 969e270fdaf8..c1d5ea4a31c1 100644 --- a/tests/neg-custom-args/allow-deep-subtypes/i5877.scala +++ b/tests/neg-custom-args/allow-deep-subtypes/i5877.scala @@ -20,7 +20,7 @@ object Main { def testHasThisType(): Unit = { def testSelf[PThis <: HasThisType[_ <: PThis]](that: HasThisType[PThis]): Unit = { - val thatSelf = that.self() // error: recursion limit exceeded + val thatSelf = that.self() // that.self().type <: that.This assert(implicitly[thatSelf.type <:< that.This] != null) } diff --git a/tests/pos/i18226a.scala b/tests/pos/i18226a.scala new file mode 100644 index 000000000000..e8db52adbc72 --- /dev/null +++ b/tests/pos/i18226a.scala @@ -0,0 +1,19 @@ +class Has[A] +trait Foo + +class TestAspect[+LowerR, -UpperR] + +class Spec[-R] { + def foo[R1 <: R](aspect: TestAspect[R1, R1]): Unit = {} +} + +class SuiteBuilder[R <: Has[_]] { + def toSpec( + spec: Spec[R & Has[Foo]], + aspect: TestAspect[ + R & Has[Foo], + R & Has[Foo] + ] + ) = + spec.foo(aspect) +} \ No newline at end of file From 9b734937ca42da37f2f45d1c83d2a3d7cdf40244 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 18 Jul 2023 19:42:51 +0200 Subject: [PATCH 4/6] Consistent type naming in computeBase to avoid shadowing problems --- compiler/src/dotty/tools/dotc/core/TypeComparer.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index e99adcf4945c..13a5f28cfe1a 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -900,8 +900,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling def tryBaseType(cls2: Symbol) = - def computeBase(tp: Type): Type = tp.widenDealias match - case tp @ AndType(tp1, tp2) => + def computeBase(tp1: Type): Type = tp1.widenDealias match + case tp @ AndType(tp11, tp12) => // We have to treat AndTypes specially, since the normal treatment // of `(T1 & T2).baseType(C)` combines the base types of T1 and T2 via glb // which drops any types that don't exist. That forgets possible solutions. @@ -912,11 +912,11 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // does not hold. The new strategy is to declare that the base type computation // failed since R does not have a base type, and to proceed to fourthTry instead, // where we try both sides of an AndType individually. - val b1 = computeBase(tp1) - val b2 = computeBase(tp2) + val b1 = computeBase(tp11) + val b2 = computeBase(tp12) if b1.exists && b2.exists then tp.derivedAndType(b1, b2) else NoType case _ => - nonExprBaseType(tp, cls2).boxedIfTypeParam(tp.typeSymbol) + nonExprBaseType(tp1, cls2).boxedIfTypeParam(tp1.typeSymbol) val base = computeBase(tp1) if base.exists && (base ne tp1) From 655851cbb892ccfd0f6e1c6d3e8768cf4d8fa151 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 19 Jul 2023 11:11:02 +0200 Subject: [PATCH 5/6] Simplify logic in TypeComparer --- .../dotty/tools/dotc/core/TypeComparer.scala | 39 ++++++------------- tests/pos/intersection.scala | 13 ++++++- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 13a5f28cfe1a..b1ceacf8c748 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -899,42 +899,27 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling canWidenAbstract && acc(true, tp) def tryBaseType(cls2: Symbol) = - - def computeBase(tp1: Type): Type = tp1.widenDealias match - case tp @ AndType(tp11, tp12) => - // We have to treat AndTypes specially, since the normal treatment - // of `(T1 & T2).baseType(C)` combines the base types of T1 and T2 via glb - // which drops any types that don't exist. That forgets possible solutions. - // For instance, in i18266.scala, we get to a subgoal `R & Row[Int] <: Row[String]` - // where R is an uninstantiated type variable. The base type computation - // of the LHS drops the non-existing base type of R and results in - // `Row[Int]`, which leads to a subtype failure since `Row[Int] <: Row[String]` - // does not hold. The new strategy is to declare that the base type computation - // failed since R does not have a base type, and to proceed to fourthTry instead, - // where we try both sides of an AndType individually. - val b1 = computeBase(tp11) - val b2 = computeBase(tp12) - if b1.exists && b2.exists then tp.derivedAndType(b1, b2) else NoType - case _ => - nonExprBaseType(tp1, cls2).boxedIfTypeParam(tp1.typeSymbol) - - val base = computeBase(tp1) + val base = nonExprBaseType(tp1, cls2).boxedIfTypeParam(tp1.typeSymbol) if base.exists && (base ne tp1) && (!caseLambda.exists || widenAbstractOKFor(tp2) || tp1.widen.underlyingClassRef(refinementOK = true).exists) then - isSubType(base, tp2, if (tp1.isRef(cls2)) approx else approx.addLow) - && recordGadtUsageIf { MatchType.thatReducesUsingGadt(tp1) } - || base.isInstanceOf[AndOrType] && fourthTry + if isSubType(base, tp2, if tp1.isRef(cls2) then approx else approx.addLow) then + recordGadtUsageIf { MatchType.thatReducesUsingGadt(tp1) } + else if tp1.widenDealias.isInstanceOf[AndType] || base.isInstanceOf[OrType] then + // If tp1 is a intersection, it could be that one of the original + // branches of the AndType tp1 conforms to tp2, but its base type does + // not, or else that its base type for cls2 does not exist, in which case + // it would not show up in `base`. In either case, we need to also fall back + // to fourthTry. Test case is i18226a.scala. // If base is a disjunction, this might have come from a tp1 type that // expands to a match type. In this case, we should try to reduce the type // and compare the redux. This is done in fourthTry - // If base is a conjunction, it could be that one of the original - // branches of the AndType tp1 conforms to tp2, but its base type does - // not. So we need to also fall back to fourthTry. Test case is i18226a.scala. + fourthTry + else + false else fourthTry - end tryBaseType def fourthTry: Boolean = tp1 match { case tp1: TypeRef => diff --git a/tests/pos/intersection.scala b/tests/pos/intersection.scala index 094e6a4e9e8e..9b7e15b317e3 100644 --- a/tests/pos/intersection.scala +++ b/tests/pos/intersection.scala @@ -40,4 +40,15 @@ object Test { def fooAB1: Int = fooAB def fooBA = (??? : B with A).f def fooBA1: Int = fooBA -} \ No newline at end of file +} + +object Test2: + class Row[+X] + class A + class B + class C extends Row[A] + class D extends Row[B] + val x: C & D = ??? + val y: Row[A & B] = x + + From 5ecb8c0b138278dd99021f34f94afa8ff3ea2331 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 19 Jul 2023 11:25:36 +0200 Subject: [PATCH 6/6] Generalize logic by using `either` --- .../src/dotty/tools/dotc/core/TypeComparer.scala | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index b1ceacf8c748..2b5a6334ba00 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -905,20 +905,21 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling || widenAbstractOKFor(tp2) || tp1.widen.underlyingClassRef(refinementOK = true).exists) then - if isSubType(base, tp2, if tp1.isRef(cls2) then approx else approx.addLow) then - recordGadtUsageIf { MatchType.thatReducesUsingGadt(tp1) } - else if tp1.widenDealias.isInstanceOf[AndType] || base.isInstanceOf[OrType] then + def checkBase = + isSubType(base, tp2, if tp1.isRef(cls2) then approx else approx.addLow) + && recordGadtUsageIf { MatchType.thatReducesUsingGadt(tp1) } + if tp1.widenDealias.isInstanceOf[AndType] || base.isInstanceOf[OrType] then // If tp1 is a intersection, it could be that one of the original // branches of the AndType tp1 conforms to tp2, but its base type does // not, or else that its base type for cls2 does not exist, in which case // it would not show up in `base`. In either case, we need to also fall back - // to fourthTry. Test case is i18226a.scala. + // to fourthTry. Test cases are i18266.scala and i18226a.scala. // If base is a disjunction, this might have come from a tp1 type that // expands to a match type. In this case, we should try to reduce the type // and compare the redux. This is done in fourthTry - fourthTry + either(checkBase, fourthTry) else - false + checkBase else fourthTry def fourthTry: Boolean = tp1 match {