From bda676381a1d00724130be2a1a9669441528ab7a Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 23 Mar 2024 13:15:45 +0100 Subject: [PATCH 1/3] Delay hard argument comparisons When comparing arguments of two applied types, perform hard comparisons after easy ones. A comparison if hard if it entails a subtype test A <: B where A is an AndType or B is an OrType. Such comparisons need to perform an either, which might lose solutions. Fixes #19999 --- .../dotty/tools/dotc/core/TypeComparer.scala | 50 +++++++++++++++++-- tests/pos/i19999.scala | 6 +++ 2 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 tests/pos/i19999.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 302ad7987889..ab783ad82ddc 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -1683,6 +1683,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling * @param tparams2 The type parameters of the type constructor applied to `args2` */ def isSubArgs(args1: List[Type], args2: List[Type], tp1: Type, tparams2: List[ParamInfo]): Boolean = { + /** The bounds of parameter `tparam`, where all references to type paramneters * are replaced by corresponding arguments (or their approximations in the case of * wildcard arguments). @@ -1690,12 +1691,35 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling def paramBounds(tparam: Symbol): TypeBounds = tparam.info.substApprox(tparams2.asInstanceOf[List[Symbol]], args2).bounds - def recurArgs(args1: List[Type], args2: List[Type], tparams2: List[ParamInfo]): Boolean = - if (args1.isEmpty) args2.isEmpty + /** Test all arguments. Hard argument tests (according to isHard) are deferred in + * the first run and picked up in the second. + */ + def recurArgs(args1: List[Type], args2: List[Type], tparams2: List[ParamInfo], + canDefer: Boolean, + deferred1: List[Type], deferred2: List[Type], deferredTparams2: List[ParamInfo]): Boolean = + if args1.isEmpty then + args2.isEmpty + && (deferred1.isEmpty + || recurArgs( + deferred1.reverse, deferred2.reverse, deferredTparams2.reverse, + canDefer = false, Nil, Nil, Nil)) else args2.nonEmpty && tparams2.nonEmpty && { val tparam = tparams2.head val v = tparam.paramVarianceSign + /** An argument test is hard if it implies a comparison A <: B where + * A is an AndType or B is an OrType. In these cases we need to run an + * either, which can lose solutions if there are type variables involved. + * So we defer such tests to run last, on the chance that some other argument + * comparison will instantiate or constrain type variables first. + */ + def isHard(arg1: Type, arg2: Type): Boolean = + val arg1d = arg1.stripped + val arg2d = arg2.stripped + (v >= 0) && (arg1d.isInstanceOf[AndType] || arg2d.isInstanceOf[OrType]) + || + (v <= 0) && (arg1d.isInstanceOf[OrType] || arg2d.isInstanceOf[AndType]) + /** Try a capture conversion: * If the original left-hand type `leftRoot` is a path `p.type`, * and the current widened left type is an application with wildcard arguments @@ -1781,10 +1805,26 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling else if v > 0 then isSubType(arg1, arg2) else isSameType(arg2, arg1) - isSubArg(args1.head, args2.head) - } && recurArgs(args1.tail, args2.tail, tparams2.tail) + val arg1 = args1.head + val arg2 = args2.head + val rest1 = args1.tail + if !canDefer + || rest1.isEmpty && deferred1.isEmpty + // skip the hardness test if this is the last argument and no previous arguments were hard + || !isHard(arg1, arg2) + then + isSubArg(arg1, arg2) + && recurArgs( + rest1, args2.tail, tparams2.tail, canDefer, + deferred1, deferred2, deferredTparams2) + else + recurArgs( + rest1, args2.tail, tparams2.tail, canDefer, + arg1 :: deferred1, arg2 :: deferred2, tparams2.head :: deferredTparams2) + } + + recurArgs(args1, args2, tparams2, canDefer = true, Nil, Nil, Nil) - recurArgs(args1, args2, tparams2) } /** Test whether `tp1` has a base type of the form `B[T1, ..., Tn]` where diff --git a/tests/pos/i19999.scala b/tests/pos/i19999.scala new file mode 100644 index 000000000000..3fdac7b080b4 --- /dev/null +++ b/tests/pos/i19999.scala @@ -0,0 +1,6 @@ +class InIntersection[I, A] + +def derived[A, R0]: InIntersection[A & R0, A] = new InIntersection[A & R0, A] + +var x: InIntersection[Int & String, Int] = derived + From e07d50eadb1e532d4c4381777d7d815c2e77a5c2 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 25 Mar 2024 18:05:40 +0100 Subject: [PATCH 2/3] Rename isHard --> isIncomplete --- compiler/src/dotty/tools/dotc/core/TypeComparer.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index ab783ad82ddc..b8732d8edd34 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -1707,13 +1707,13 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling val tparam = tparams2.head val v = tparam.paramVarianceSign - /** An argument test is hard if it implies a comparison A <: B where + /** An argument test is incomplete if it implies a comparison A <: B where * A is an AndType or B is an OrType. In these cases we need to run an * either, which can lose solutions if there are type variables involved. * So we defer such tests to run last, on the chance that some other argument * comparison will instantiate or constrain type variables first. */ - def isHard(arg1: Type, arg2: Type): Boolean = + def isIncomplete(arg1: Type, arg2: Type): Boolean = val arg1d = arg1.stripped val arg2d = arg2.stripped (v >= 0) && (arg1d.isInstanceOf[AndType] || arg2d.isInstanceOf[OrType]) @@ -1811,7 +1811,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling if !canDefer || rest1.isEmpty && deferred1.isEmpty // skip the hardness test if this is the last argument and no previous arguments were hard - || !isHard(arg1, arg2) + || !isIncomplete(arg1, arg2) then isSubArg(arg1, arg2) && recurArgs( From 0f30c9800725f889bb91f05aae692495f1387bf6 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 25 Mar 2024 18:17:36 +0100 Subject: [PATCH 3/3] Apply suggestions from code review Co-authored-by: Guillaume Martres --- compiler/src/dotty/tools/dotc/core/TypeComparer.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index b8732d8edd34..9fe3a1daf9e1 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -1691,7 +1691,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling def paramBounds(tparam: Symbol): TypeBounds = tparam.info.substApprox(tparams2.asInstanceOf[List[Symbol]], args2).bounds - /** Test all arguments. Hard argument tests (according to isHard) are deferred in + /** Test all arguments. Incomplete argument tests (according to isIncomplete) are deferred in * the first run and picked up in the second. */ def recurArgs(args1: List[Type], args2: List[Type], tparams2: List[ParamInfo], @@ -1714,8 +1714,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling * comparison will instantiate or constrain type variables first. */ def isIncomplete(arg1: Type, arg2: Type): Boolean = - val arg1d = arg1.stripped - val arg2d = arg2.stripped + val arg1d = arg1.strippedDealias + val arg2d = arg2.strippedDealias (v >= 0) && (arg1d.isInstanceOf[AndType] || arg2d.isInstanceOf[OrType]) || (v <= 0) && (arg1d.isInstanceOf[OrType] || arg2d.isInstanceOf[AndType]) @@ -1810,7 +1810,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling val rest1 = args1.tail if !canDefer || rest1.isEmpty && deferred1.isEmpty - // skip the hardness test if this is the last argument and no previous arguments were hard + // skip the incompleteness test if this is the last argument and no previous argument tests were incomplete || !isIncomplete(arg1, arg2) then isSubArg(arg1, arg2)